# v2 git bundle
e38053487dc20b8fd710fc105f3bda10dbcd80c0 refs/heads/bundle
e38053487dc20b8fd710fc105f3bda10dbcd80c0 HEAD

PACK      
x Xtree 7ef4bdb52d6f19494b093713d12b07752ab7b954
author Lufaneng <Lufaneng@foxmail.com> 1785204704 +0800
committer Lufaneng <Lufaneng@foxmail.com> 1785204704 +0800

init
Ǭ2x $100644 .gitignore -WDf-Df`40000 backend +"	沈:+్100644 docker-compose.yml P@0x58p>40000 front Hn:C®n40000 pc-admin  b^zQhf40000 scripts xܸ(D@zK$\x!# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
.DS_Store
dist
release/
.codex-tmp
*.local

# Editor directories and files
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

.env
.env.prod
backend/uploads/*
!backend/uploads/.gitkeep
`x 100644 .env.example _A6R~<ڈI{100644 README.md Fb<YNA100644 package-lock.json ,]Xk}E,ҶR*100644 package.json B3̍k/*@R40000 sql PFۖ
Z40000 src qe_*X^.	Қ40000 uploads dм=h^7m[^jfxaNODE_ENV=production
PORT=3100
PUBLIC_BASE_URL=http://127.0.0.1:3100
# Optional. Leave empty to use backend/public/pc-admin.
PC_ADMIN_DIST_DIR=
# Reserved H5 preview config. Keep disabled for the current PC-only release.
ENABLE_H5_PREVIEW=false
# Optional. When H5 preview is enabled later, leave empty to use backend/public/h5.
H5_DIST_DIR=

# Signed login session. Replace with a long random value before production deployment.
AUTH_SESSION_SECRET=replace_with_at_least_32_random_characters
# Default: 168 hours (7 days).
AUTH_SESSION_TTL_HOURS=168

DB_CLIENT=mysql
MYSQL_HOST=127.0.0.1
MYSQL_PORT=3306
MYSQL_USER=lifanghe
MYSQL_PASSWORD=lifanghe123
MYSQL_DATABASE=lifanghe_education
MYSQL_CONNECTION_LIMIT=10

# WeChat Mini Program config for login, phone binding, and mini program code generation.
WECHAT_APP_ID=wx_your_app_id
WECHAT_APP_SECRET=your_wechat_app_secret
# Use release for production, trial for experience version, develop for development version.
WECHAT_ENV_VERSION=release

# SMS verification config.
# In production set SMS_ALLOW_TEST_CODE=false so the fixed test code cannot pass.
SMS_PROVIDER=aliyun
SMS_ALLOW_TEST_CODE=true
SMS_TEST_CODE=111111
SMS_CODE_LENGTH=6
SMS_CODE_TTL_SECONDS=300
SMS_CODE_COOLDOWN_SECONDS=60

# Aliyun SMS OpenAPI config. The SMS template must contain a variable named ${code}
# unless ALIYUN_SMS_TEMPLATE_PARAM_KEY is changed.
ALIYUN_ACCESS_KEY_ID=your_aliyun_access_key_id
ALIYUN_ACCESS_KEY_SECRET=your_aliyun_access_key_secret
ALIYUN_SMS_ENDPOINT=dysmsapi.aliyuncs.com
ALIYUN_SMS_SIGN_NAME=your_sms_sign_name
ALIYUN_SMS_TEMPLATE_CODE=SMS_000000000
ALIYUN_SMS_TEMPLATE_PARAM_KEY=code
~'x# 立方和教育推广机制后端

Node + Express 分层接口，用内存仓库模拟数据库，方便后续替换为 MySQL、PostgreSQL 或 MongoDB。

## Scripts

```bash
npm install
npm run dev
```

默认服务地址：

```text
http://localhost:3100/api
```

## Main Modules

- `controllers/`: HTTP 入参和出参
- `services/`: 推荐、积分、课程、管理台业务规则
- `repositories/`: 数据访问层
- `data/seed.js`: 初始模拟数据
- `routes/`: API 路由聚合
ox   {
  "name": "uni-preset-vue",
  "version": "0.0.0",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "uni-preset-vue",
      "version": "0.0.0",
      "dependencies": {
        "@dcloudio/uni-app": "3.0.0-5000720260410001",
        "@dcloudio/uni-app-harmony": "3.0.0-5000720260410001",
        "@dcloudio/uni-app-plus": "3.0.0-5000720260410001",
        "@dcloudio/uni-components": "3.0.0-5000720260410001",
        "@dcloudio/uni-h5": "3.0.0-5000720260410001",
        "@dcloudio/uni-mp-alipay": "3.0.0-5000720260410001",
        "@dcloudio/uni-mp-baidu": "3.0.0-5000720260410001",
        "@dcloudio/uni-mp-harmony": "3.0.0-5000720260410001",
        "@dcloudio/uni-mp-jd": "3.0.0-5000720260410001",
        "@dcloudio/uni-mp-kuaishou": "3.0.0-5000720260410001",
        "@dcloudio/uni-mp-lark": "3.0.0-5000720260410001",
        "@dcloudio/uni-mp-qq": "3.0.0-5000720260410001",
        "@dcloudio/uni-mp-toutiao": "3.0.0-5000720260410001",
        "@dcloudio/uni-mp-weixin": "3.0.0-5000720260410001",
        "@dcloudio/uni-mp-xhs": "3.0.0-5000720260410001",
        "@dcloudio/uni-quickapp-webview": "3.0.0-5000720260410001",
        "uview-plus": "^3.4.41",
        "vue": "^3.4.21",
        "vue-i18n": "^9.1.9"
      },
      "devDependencies": {
        "@dcloudio/types": "^3.4.8",
        "@dcloudio/uni-automator": "3.0.0-5000720260410001",
        "@dcloudio/uni-cli-shared": "3.0.0-5000720260410001",
        "@dcloudio/uni-stacktracey": "3.0.0-5000720260410001",
        "@dcloudio/vite-plugin-uni": "3.0.0-5000720260410001",
        "@vue/runtime-core": "^3.4.21",
        "sass": "^1.89.2",
        "vite": "5.2.8"
      }
    },
    "node_modules/@ampproject/remapping": {
      "version": "2.3.0",
      "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
      "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
      "license": "Apache-2.0",
      "dependencies": {
        "@jridgewell/gen-mapping": "^0.3.5",
        "@jridgewell/trace-mapping": "^0.3.24"
      },
      "engines": {
        "node": ">=6.0.0"
      }
    },
    "node_modules/@babel/code-frame": {
      "version": "7.24.7",
      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz",
      "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==",
      "license": "MIT",
      "dependencies": {
        "@babel/highlight": "^7.24.7",
        "picocolors": "^1.0.0"
      },
      "engines": {
        "node": ">=6.9.0"
      }
    },
    "node_modules/@babel/compat-data": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
      "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
      "license": "MIT",
      "engines": {
        "node": ">=6.9.0"
      }
    },
    "node_modules/@babel/core": {
      "version": "7.25.2",
      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz",
      "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==",
      "license": "MIT",
      "dependencies": {
        "@ampproject/remapping": "^2.2.0",
        "@babel/code-frame": "^7.24.7",
        "@babel/generator": "^7.25.0",
        "@babel/helper-compilation-targets": "^7.25.2",
        "@babel/helper-module-transforms": "^7.25.2",
        "@babel/helpers": "^7.25.0",
        "@babel/parser": "^7.25.0",
        "@babel/template": "^7.25.0",
        "@babel/traverse": "^7.25.2",
        "@babel/types": "^7.25.2",
        "convert-source-map": "^2.0.0",
        "debug": "^4.1.0",
        "gensync": "^1.0.0-beta.2",
        "json5": "^2.2.3",
        "semver": "^6.3.1"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/babel"
      }
    },
    "node_modules/@babel/generator": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz",
      "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==",
      "license": "MIT",
      "dependencies": {
        "@babel/parser": "^7.29.7",
        "@babel/types": "^7.29.7",
        "@jridgewell/gen-mapping": "^0.3.12",
        "@jridgewell/trace-mapping": "^0.3.28",
        "jsesc": "^3.0.2"
      },
      "engines": {
        "node": ">=6.9.0"
      }
    },
    "node_modules/@babel/generator/node_modules/@babel/parser": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
      "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
      "license": "MIT",
      "dependencies": {
        "@babel/types": "^7.29.7"
      },
      "bin": {
        "parser": "bin/babel-parser.js"
      },
      "engines": {
        "node": ">=6.0.0"
      }
    },
    "node_modules/@babel/generator/node_modules/@babel/types": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
      "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
      "license": "MIT",
      "dependencies": {
        "@babel/helper-string-parser": "^7.29.7",
        "@babel/helper-validator-identifier": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      }
    },
    "node_modules/@babel/helper-annotate-as-pure": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz",
      "integrity": "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/types": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      }
    },
    "node_modules/@babel/helper-annotate-as-pure/node_modules/@babel/types": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
      "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-string-parser": "^7.29.7",
        "@babel/helper-validator-identifier": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      }
    },
    "node_modules/@babel/helper-compilation-targets": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
      "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
      "license": "MIT",
      "dependencies": {
        "@babel/compat-data": "^7.29.7",
        "@babel/helper-validator-option": "^7.29.7",
        "browserslist": "^4.24.0",
        "lru-cache": "^5.1.1",
        "semver": "^6.3.1"
      },
      "engines": {
        "node": ">=6.9.0"
      }
    },
    "node_modules/@babel/helper-create-class-features-plugin": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.7.tgz",
      "integrity": "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-annotate-as-pure": "^7.29.7",
        "@babel/helper-member-expression-to-functions": "^7.29.7",
        "@babel/helper-optimise-call-expression": "^7.29.7",
        "@babel/helper-replace-supers": "^7.29.7",
        "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7",
        "@babel/traverse": "^7.29.7",
        "semver": "^6.3.1"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0"
      }
    },
    "node_modules/@babel/helper-create-regexp-features-plugin": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.29.7.tgz",
      "integrity": "sha512-907Uymvqgg1dwUA+7IGwFAOSYzQOuzPXKNJ1yxzwPffzkYFg2q2eHi1fIOs6sXkG9NbIUMunnUlkYsfRFNvomg==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-annotate-as-pure": "^7.29.7",
        "regexpu-core": "^6.3.1",
        "semver": "^6.3.1"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0"
      }
    },
    "node_modules/@babel/helper-define-polyfill-provider": {
      "version": "0.6.8",
      "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.8.tgz",
      "integrity": "sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-compilation-targets": "^7.28.6",
        "@babel/helper-plugin-utils": "^7.28.6",
        "debug": "^4.4.3",
        "lodash.debounce": "^4.0.8",
        "resolve": "^1.22.11"
      },
      "peerDependencies": {
        "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
      }
    },
    "node_modules/@babel/helper-define-polyfill-provider/node_modules/debug": {
      "version": "4.4.3",
      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "ms": "^2.1.3"
      },
      "engines": {
        "node": ">=6.0"
      },
      "peerDependenciesMeta": {
        "supports-color": {
          "optional": true
        }
      }
    },
    "node_modules/@babel/helper-define-polyfill-provider/node_modules/resolve": {
      "version": "1.22.12",
      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
      "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "es-errors": "^1.3.0",
        "is-core-module": "^2.16.1",
        "path-parse": "^1.0.7",
        "supports-preserve-symlinks-flag": "^1.0.0"
      },
      "bin": {
        "resolve": "bin/resolve"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/@babel/helper-globals": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
      "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
      "license": "MIT",
      "engines": {
        "node": ">=6.9.0"
      }
    },
    "node_modules/@babel/helper-member-expression-to-functions": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.29.7.tgz",
      "integrity": "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/traverse": "^7.29.7",
        "@babel/types": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      }
    },
    "node_modules/@babel/helper-member-expression-to-functions/node_modules/@babel/types": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
      "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-string-parser": "^7.29.7",
        "@babel/helper-validator-identifier": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      }
    },
    "node_modules/@babel/helper-module-imports": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
      "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
      "license": "MIT",
      "dependencies": {
        "@babel/traverse": "^7.29.7",
        "@babel/types": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      }
    },
    "node_modules/@babel/helper-module-imports/node_modules/@babel/types": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
      "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
      "license": "MIT",
      "dependencies": {
        "@babel/helper-string-parser": "^7.29.7",
        "@babel/helper-validator-identifier": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      }
    },
    "node_modules/@babel/helper-module-transforms": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
      "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
      "license": "MIT",
      "dependencies": {
        "@babel/helper-module-imports": "^7.29.7",
        "@babel/helper-validator-identifier": "^7.29.7",
        "@babel/traverse": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0"
      }
    },
    "node_modules/@babel/helper-optimise-call-expression": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.29.7.tgz",
      "integrity": "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/types": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      }
    },
    "node_modules/@babel/helper-optimise-call-expression/node_modules/@babel/types": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
      "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-string-parser": "^7.29.7",
        "@babel/helper-validator-identifier": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      }
    },
    "node_modules/@babel/helper-plugin-utils": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz",
      "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=6.9.0"
      }
    },
    "node_modules/@babel/helper-remap-async-to-generator": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.29.7.tgz",
      "integrity": "sha512-16AMiW26DbXWBbr3B8wNozKM0ydMLB892vaOaJW/fPJdnT8vJk5sdkQcU/isqUxyCE0cEoa8wZOcbgDuC4b6Og==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-annotate-as-pure": "^7.29.7",
        "@babel/helper-wrap-function": "^7.29.7",
        "@babel/traverse": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0"
      }
    },
    "node_modules/@babel/helper-replace-supers": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.29.7.tgz",
      "integrity": "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-member-expression-to-functions": "^7.29.7",
        "@babel/helper-optimise-call-expression": "^7.29.7",
        "@babel/traverse": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0"
      }
    },
    "node_modules/@babel/helper-skip-transparent-expression-wrappers": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.29.7.tgz",
      "integrity": "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/traverse": "^7.29.7",
        "@babel/types": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      }
    },
    "node_modules/@babel/helper-skip-transparent-expression-wrappers/node_modules/@babel/types": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
      "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-string-parser": "^7.29.7",
        "@babel/helper-validator-identifier": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      }
    },
    "node_modules/@babel/helper-string-parser": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@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.npmjs.org/@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/helper-validator-option": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
      "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
      "license": "MIT",
      "engines": {
        "node": ">=6.9.0"
      }
    },
    "node_modules/@babel/helper-wrap-function": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.29.7.tgz",
      "integrity": "sha512-iES0Skag9ERIF68aXadpO6dbXa03mNWK3sEqJaMnLNs/eC3l0lkImdfoy6Y09/SfkpawdAB4RjQ7PVA7TcVGdw==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/template": "^7.29.7",
        "@babel/traverse": "^7.29.7",
        "@babel/types": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      }
    },
    "node_modules/@babel/helper-wrap-function/node_modules/@babel/types": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
      "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-string-parser": "^7.29.7",
        "@babel/helper-validator-identifier": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      }
    },
    "node_modules/@babel/helpers": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
      "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
      "license": "MIT",
      "dependencies": {
        "@babel/template": "^7.29.7",
        "@babel/types": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      }
    },
    "node_modules/@babel/helpers/node_modules/@babel/types": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
      "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
      "license": "MIT",
      "dependencies": {
        "@babel/helper-string-parser": "^7.29.7",
        "@babel/helper-validator-identifier": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      }
    },
    "node_modules/@babel/highlight": {
      "version": "7.25.9",
      "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.25.9.tgz",
      "integrity": "sha512-llL88JShoCsth8fF8R4SJnIn+WLvR6ccFxu1H3FlMhDontdcmZWf2HgIZ7AIqV3Xcck1idlohrN4EUBQz6klbw==",
      "license": "MIT",
      "dependencies": {
        "@babel/helper-validator-identifier": "^7.25.9",
        "chalk": "^2.4.2",
        "js-tokens": "^4.0.0",
        "picocolors": "^1.0.0"
      },
      "engines": {
        "node": ">=6.9.0"
      }
    },
    "node_modules/@babel/parser": {
      "version": "7.25.6",
      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.6.tgz",
      "integrity": "sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==",
      "license": "MIT",
      "dependencies": {
        "@babel/types": "^7.25.6"
      },
      "bin": {
        "parser": "bin/babel-parser.js"
      },
      "engines": {
        "node": ">=6.0.0"
      }
    },
    "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.29.7.tgz",
      "integrity": "sha512-j8SrR0zLZrRsC09DlszEx8FpMiwukKffYXMK0d5LmOglO7vGG6sz/BR/20yHqWH+Lnn31JTt2PE3hIWNgM2J6w==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.29.7",
        "@babel/traverse": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0"
      }
    },
    "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.29.7.tgz",
      "integrity": "sha512-r8j8escF+U2FUHo0KOhPUdMzUO+jp9fInva6+ACVAF3Y97Ev+5iNZwiqTghmzNeWwDkOPlYuTcfb1vDaoZKmAQ==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0"
      }
    },
    "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.29.7.tgz",
      "integrity": "sha512-GE1TFSiuFeGsCxmYXZl8HwoPrVlwe4rHPFE8weieGKZqnDORK+Ar3vgWMgW+AOxQ6/2TgLSKx9p6W7O4rC6qgQ==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0"
      }
    },
    "node_modules/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array/-/plugin-bugfix-safari-rest-destructuring-rhs-array-7.29.7.tgz",
      "integrity": "sha512-oBNVCvnO5tND+xSopWvV8WNGfpTfgP4Zr/YXXSj8zfmcPktp5Ku/aZlsIowgSD4fjmgHn6sGmB9APVsU5zOdhA==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.29.7",
        "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0"
      }
    },
    "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.29.7.tgz",
      "integrity": "sha512-QQt9qKHZ2sg/kivaLr7lnQr8HVrQDdBNSfCsTjiDxRuX/K5ORyKq+Bu8Xr0cDE3Dfkv0cw28Ve0EKyKMvulkOw==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.29.7",
        "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7",
        "@babel/plugin-transform-optional-chaining": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.13.0"
      }
    },
    "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.29.7.tgz",
      "integrity": "sha512-pn6QacGLgvCcwc+syUhKE/qSjV2D1IHDB84RNxWYSt1mW3K/SCtjinZ2p0cETJxAWBjPy3K/1lHwG5BjjPxNlw==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.29.7",
        "@babel/traverse": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0"
      }
    },
    "node_modules/@babel/plugin-proposal-private-property-in-object": {
      "version": "7.21.0-placeholder-for-preset-env.2",
      "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz",
      "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-syntax-async-generators": {
      "version": "7.8.4",
      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz",
      "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.8.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-syntax-bigint": {
      "version": "7.8.3",
      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz",
      "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.8.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-syntax-class-properties": {
      "version": "7.12.13",
      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz",
      "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.12.13"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-syntax-class-static-block": {
      "version": "7.14.5",
      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz",
      "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.14.5"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-syntax-import-assertions": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.29.7.tgz",
      "integrity": "sha512-/An1OCBN93thpBAGyfsK2pcf0jvju1SAtKkL2Ny++B5Sy6sqgzXDQH1cZxWbF96Wuk+bn41MDA9bLd4VVAw6rw==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-syntax-import-attributes": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.29.7.tgz",
      "integrity": "sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-syntax-import-meta": {
      "version": "7.10.4",
      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz",
      "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.10.4"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-syntax-json-strings": {
      "version": "7.8.3",
      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz",
      "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.8.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-syntax-jsx": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz",
      "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-syntax-logical-assignment-operators": {
      "version": "7.10.4",
      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz",
      "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.10.4"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": {
      "version": "7.8.3",
      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz",
      "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.8.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-syntax-numeric-separator": {
      "version": "7.10.4",
      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz",
      "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.10.4"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-syntax-object-rest-spread": {
      "version": "7.8.3",
      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz",
      "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.8.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-syntax-optional-catch-binding": {
      "version": "7.8.3",
      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz",
      "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.8.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-syntax-optional-chaining": {
      "version": "7.8.3",
      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz",
      "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.8.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-syntax-private-property-in-object": {
      "version": "7.14.5",
      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz",
      "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.14.5"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-syntax-top-level-await": {
      "version": "7.14.5",
      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz",
      "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.14.5"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-syntax-typescript": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz",
      "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-syntax-unicode-sets-regex": {
      "version": "7.18.6",
      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz",
      "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-create-regexp-features-plugin": "^7.18.6",
        "@babel/helper-plugin-utils": "^7.18.6"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0"
      }
    },
    "node_modules/@babel/plugin-transform-arrow-functions": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.29.7.tgz",
      "integrity": "sha512-N7zArUXWzAMzm+/N0uPBeVB3Fam5lMxtUwMmDK5f/IBBS7a7p1qeUoxd/6CckXoxUdgsntq1Dh8xNW06maZbDQ==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-transform-async-generator-functions": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.7.tgz",
      "integrity": "sha512-d98gXZkgswvkyohMBABkhm3GeXhYj8psWfwQ2C7gtfrKGTykQa/iOIi+JJhwMjPlZ6Vm2XN+DCf3Es1EoG4ZLA==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.29.7",
        "@babel/helper-remap-async-to-generator": "^7.29.7",
        "@babel/traverse": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-transform-async-to-generator": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.29.7.tgz",
      "integrity": "sha512-pcUb2SS+RMo9TWVBwKGI5ShtoG7R+zBsFmCKDa6fe8c+hPr3XJlZgoE5j6i8W7gDjhyvy+85vmYexanvXh3d1w==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-module-imports": "^7.29.7",
        "@babel/helper-plugin-utils": "^7.29.7",
        "@babel/helper-remap-async-to-generator": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-transform-block-scoped-functions": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.29.7.tgz",
      "integrity": "sha512-cUSmjh72N+rN4PrkFlN1dJwNCwjVp5d38/CQrEsFggkD10UiFlBFgdH3tv5dNsLuHY+3S8db2xCHjhZcv5WgvA==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-transform-block-scoping": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.29.7.tgz",
      "integrity": "sha512-ONyr4+AZhKh8yKWInVxU9AXA9EbsyeLcL6V0dJy6M2/62vuvpGm29zzuymbTpdc451GEpDIdAyPLP3r+P61yKQ==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-transform-class-properties": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.29.7.tgz",
      "integrity": "sha512-GtcpjFvanPfzNQi3eTitsCqtRRmmqzpy/A+yhTR1HaZo1Ly3EA8ZXxlPyHdR8/IuRMYc3E4wdGBewB2QKQjAaA==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-create-class-features-plugin": "^7.29.7",
        "@babel/helper-plugin-utils": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-transform-class-static-block": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.29.7.tgz",
      "integrity": "sha512-kibJgmEdX2iMwsHY2tSZNDgj8PwIlCQz7FK9KuGKO8zsuoUwSEhoNnNVp/emKWrbY4HeO6kkXfdMqRKKKXBm2A==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-create-class-features-plugin": "^7.29.7",
        "@babel/helper-plugin-utils": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.12.0"
      }
    },
    "node_modules/@babel/plugin-transform-classes": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.29.7.tgz",
      "integrity": "sha512-qV0OGGBVacduzQHE649JyCneOFI/maT+YKsO+K4Yi3xv2wTPNjM/W2o2gdzMwEAZz7fXNTHAe0NcSg30bIN69g==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-annotate-as-pure": "^7.29.7",
        "@babel/helper-compilation-targets": "^7.29.7",
        "@babel/helper-globals": "^7.29.7",
        "@babel/helper-plugin-utils": "^7.29.7",
        "@babel/helper-replace-supers": "^7.29.7",
        "@babel/traverse": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-transform-computed-properties": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.29.7.tgz",
      "integrity": "sha512-RK7/IyU5phpuCdBAuig5VkzG/EnbDaui5SQGdU9BFrHdV+mV4cUjLMQ9lJDjLNtWHsqtiefpGZUXQP2BiTYMsA==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.29.7",
        "@babel/template": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-transform-destructuring": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.29.7.tgz",
      "integrity": "sha512-iPX8aD6H9zV5s7ZsqTdNocPN/MGQ5sSMnElKrktxjJRMnB2jN/1p2+R7GkfD6CAYoVFqy5A4XnSIUeGgJzIWpg==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.29.7",
        "@babel/traverse": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-transform-dotall-regex": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.29.7.tgz",
      "integrity": "sha512-3qc18hsD2RdZiyJNDNc7HQpv6xbncwh8FYtxNFFzclSyh/trPD9KkVR9BDECUjDLvb7yJVF15GfYUuC+LMkkiQ==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-create-regexp-features-plugin": "^7.29.7",
        "@babel/helper-plugin-utils": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-transform-duplicate-keys": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.29.7.tgz",
      "integrity": "sha512-6IvRRriEMqnBwD6chtxdLpMYCHWEzN+oL5cyQtjykya19UgzbmKhxmhZgKC/LHxS2nYr9Q/qYPZ5Lr6jOL9+yQ==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.29.7.tgz",
      "integrity": "sha512-2wiIyo2BjtgU7HufSeDnL9L2O7zr8jmhFKuSr65VpRkUiRKRNpb0mdlk56+XPPKoIrfHqzbMuglDvZun0RISsA==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-create-regexp-features-plugin": "^7.29.7",
        "@babel/helper-plugin-utils": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0"
      }
    },
    "node_modules/@babel/plugin-transform-dynamic-import": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.29.7.tgz",
      "integrity": "sha512-giOlEm/EFjfjr+te9NsdjkUo2v4f8rS/SXPumRVHAtbNcyNlvtREkU1dZzaIDclNpnaVhlCqRdFKhJBjBikzLg==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-transform-explicit-resource-management": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.29.7.tgz",
      "integrity": "sha512-Rstj7coNz8sE+7Ju7ihpHLI564lsK5pUpNNlvptCIC/16E/S5hbl6n3kESPKdNRmqEWlpn5xpS5Q2dvXBsySLw==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.29.7",
        "@babel/plugin-transform-destructuring": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-transform-exponentiation-operator": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.29.7.tgz",
      "integrity": "sha512-zFpMOTLZBdW5LfObqcSbL6kefg4R4eLdmvS0wbN9M6D5Mym/sKm9toOoWyVOa+xDjvCnuWcHls2YonXwHvH3CQ==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-transform-export-namespace-from": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.29.7.tgz",
      "integrity": "sha512-24B2nOy2TeJSMheqwPD4DDQOV/elLSIlKxjZt4i05H5AgdPdWR3n18HnNrcJ+j76WJd9gbwb9jPjNYUy6RautA==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-transform-for-of": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.29.7.tgz",
      "integrity": "sha512-zeSIHh0+E1Um1WJRXCFlHQYu2ieJNdivLLjlBEp+dIBu3S51n+SZZmIXjxnItw6pz56Cn+KvK68BIBVsxq2JiQ==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.29.7",
        "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-transform-function-name": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.29.7.tgz",
      "integrity": "sha512-otRWaHXE6fbAGkePvaj/kvs3HsqXfPhlnzwSOlnFgbqCPMd975dW+4wZ00WFBt+/YlBGcJwNrARQTOJOb4ZrIg==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-compilation-targets": "^7.29.7",
        "@babel/helper-plugin-utils": "^7.29.7",
        "@babel/traverse": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-transform-json-strings": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.29.7.tgz",
      "integrity": "sha512-RRnE2+eon1rJAq8MnoF1b5kTpY1vU88twHcvcKMrsqP/jxIRqDVs9iJB5fqPuqyeFAW0wJo4MlUIPpQCq/aRsg==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-transform-literals": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.29.7.tgz",
      "integrity": "sha512-DZ/oLP21ZuWx1vKqnoNv6/tvEK48AQOBRai40CX9dTjGluvT/YZCyY3rryDtyUqCEoyNroy5KKPwX2iQCiRvyw==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-transform-logical-assignment-operators": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.29.7.tgz",
      "integrity": "sha512-A0H91hh6W8MFRkp5TqJmMr39jzGD1A1E1Ysiv2O06Sfbhkapm+XyIzxWCEh5kqwOZ1/8QZ0dY3SeQ7XBqfJd5Q==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-transform-member-expression-literals": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.29.7.tgz",
      "integrity": "sha512-hl1kwFZCCiDyfH25Xmco9jTrkPgnS9pmOzSG7W5I4SaGbLeqKv417hcU2RKmaxoPEgsoJh7ZPOrnPGq99bHoUg==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-transform-modules-amd": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.29.7.tgz",
      "integrity": "sha512-fxtQoH3m5ywUSIfaH0FGCzWu4McsYon5bD3K4XnskC7f+OyQMj7rsOMi4NvvmJ83WwBAg4UCe+ov4VZlqEvyew==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-module-transforms": "^7.29.7",
        "@babel/helper-plugin-utils": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-transform-modules-commonjs": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.29.7.tgz",
      "integrity": "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-module-transforms": "^7.29.7",
        "@babel/helper-plugin-utils": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-transform-modules-systemjs": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.7.tgz",
      "integrity": "sha512-TM2ZcQLoG2/y4HODiStCo10DibYhWhGWAwVv+EQKmG/7GFl0N+AAmUiXOMKM+aiJ9XBJ9AHVZBvTzMnJ2sM3cQ==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-module-transforms": "^7.29.7",
        "@babel/helper-plugin-utils": "^7.29.7",
        "@babel/helper-validator-identifier": "^7.29.7",
        "@babel/traverse": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-transform-modules-umd": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.29.7.tgz",
      "integrity": "sha512-B4UkaTK3QpgCwJnrxKfMPKdo92CN7OKXAlpAAnM3UPu0Q0lCCk57ylA9AJbRy2v8dDKOPAAWcoR6CMyeoHwRCA==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-module-transforms": "^7.29.7",
        "@babel/helper-plugin-utils": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-transform-named-capturing-groups-regex": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.7.tgz",
      "integrity": "sha512-vuFoLwr4qnv2xbZ16SQd6uPcH5FNrLHhk/Jzo++0XJFcaDsr4gjJVg6j398oMHiC+83k/GiBzviwF5KBJkPUtQ==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-create-regexp-features-plugin": "^7.29.7",
        "@babel/helper-plugin-utils": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0"
      }
    },
    "node_modules/@babel/plugin-transform-new-target": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.29.7.tgz",
      "integrity": "sha512-fEo41GmsOUhOBlw8ioo6zvjX5Xc2Lqkzlyfqbpsk3eB6TReV18uhxZ0esfEokVbY2+PVJAQHNKxER6lGrzNd3A==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-transform-nullish-coalescing-operator": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.29.7.tgz",
      "integrity": "sha512-idmp1dFaekP9GbcMvG24Kvw2BfhFZjHnNJCkV4WuIY4PskJzwI3f1N5OdgYke38T7rftO6ERulFRn2cFeZwRkg==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-transform-numeric-separator": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.29.7.tgz",
      "integrity": "sha512-zR7fv/z14OjgHl4AgRtkDBvBMhIzCxqV/qN/2BCRC7LjFwvuzjYe7gDWxC4Wl/SNsLM6SE1IWvRPYMgSJaUvNw==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-transform-object-rest-spread": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.29.7.tgz",
      "integrity": "sha512-Ld98jn4c0smUywL57m7SgsHq3OpThOa6LqZJif3G6jYOovPleoFhVrBJ1WegRApSFB2wu4+RelAj9AC9G08Z4A==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-compilation-targets": "^7.29.7",
        "@babel/helper-plugin-utils": "^7.29.7",
        "@babel/plugin-transform-destructuring": "^7.29.7",
        "@babel/plugin-transform-parameters": "^7.29.7",
        "@babel/traverse": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-transform-object-super": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.29.7.tgz",
      "integrity": "sha512-Ea/diGcw0twB5IlZPO5sgET6fJsLJqPABqTuFWIR+iMPGPZJkATEIWx0wa+aEQ5UY1CBQyP/gkAiLEqn1vBiQA==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.29.7",
        "@babel/helper-replace-supers": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-transform-optional-catch-binding": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.29.7.tgz",
      "integrity": "sha512-sLsyndxK2VwX6yNUOakMb7Sh553ZTe/vVM1XJ+9Z5aW1ytsc8xOIwmyk05NNjN60vkc5/KqoTH6hB4V41LJhng==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-transform-optional-chaining": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.29.7.tgz",
      "integrity": "sha512-6GM1dhvK3gNODkXcEcMCOLEDCLSoZ/sBbro2Ax8HURyasQ4NshagQixkRFdh5niI6E4gmA/jYI/4aT7rRos3ZQ==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.29.7",
        "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-transform-parameters": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.29.7.tgz",
      "integrity": "sha512-ZDOBqV/qLYJI0YElr8DcENEyARsFQeESqWXH6gZlghYXuPPjvweuDhP4VyEi4BlUBlLRFZVjxoZDMjxhLW766g==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-transform-private-methods": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.29.7.tgz",
      "integrity": "sha512-/6Rz4DK1ETDEM/bWHsPHcaEe7ZaT1EqSXjtSP/L0DijOYuaUhiRiOKcwpZ8P7zR4xXEHc2ITdiCgBm9Tpyv9ug==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-create-class-features-plugin": "^7.29.7",
        "@babel/helper-plugin-utils": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-transform-private-property-in-object": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.29.7.tgz",
      "integrity": "sha512-+BNo06dnrzdNNqCm1X6YUaVv0DKk8Q+JYcoZfOkLhYWNCXzlwTSRq8zGWayT1csjcpNXV9CQTBRRbmTLZac5cA==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-annotate-as-pure": "^7.29.7",
        "@babel/helper-create-class-features-plugin": "^7.29.7",
        "@babel/helper-plugin-utils": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-transform-property-literals": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.29.7.tgz",
      "integrity": "sha512-bOMRLQuI0A5ZqHq3OWJ89/rXpJ/NJrbVhXiP4zwPGMs6kpcVsuTUNjwoE30K0Qm3mf48a/TnRYYD6vPNqcg6jA==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-transform-regenerator": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.7.tgz",
      "integrity": "sha512-rNNFV0DBAJp988xW2DOntfDoYn1eR8GGF5AT5vYc+rjyfaQkM242c9tZUHHPe7KYaiJizXPWhQTzzdbXySyhBw==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-transform-regexp-modifiers": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.29.7.tgz",
      "integrity": "sha512-mB5Fs0VWrJ42ZCmc8114v60qetdaUVNkj9PmSZRmanCZM3S9hm0CFRLjRmYIsuXav14l2jvZ+4T8iiCGnhj3nQ==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-create-regexp-features-plugin": "^7.29.7",
        "@babel/helper-plugin-utils": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0"
      }
    },
    "node_modules/@babel/plugin-transform-reserved-words": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.29.7.tgz",
      "integrity": "sha512-5+YhdpVgmfSmwZyLMftfaiffLRMHjzIRHFHHLdibcSyJm2pasMrKHrO3Ptrt2DRshjvpgjEJJ1zVW14WPq/6QA==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-transform-shorthand-properties": {
      "version": "7.29.7",
      "resolved": "https:/   /registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.29.7.tgz",
      "integrity": "sha512-I+WYbGBAiCn7nA6xBrlgPH+MB7HWb4u8pv5S0Pv7OtwNvIFvCCb24YlttKEeUFVurfBCEaOTnuhlqsb7f0Z5Dg==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-transform-spread": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.29.7.tgz",
      "integrity": "sha512-/u5K1QWada7tbYNqTjMh96718g9NTwh9tfPJMsSmVsQwGT447FskV+KcfeXkXq2GWki4EM/MuTdmBec+hOuVTQ==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.29.7",
        "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-transform-sticky-regex": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.29.7.tgz",
      "integrity": "sha512-BCHzNYJGe9l7EpwwDBN/ztlL2NYFFq8hp9ddjtUEM9f2O7S7kKV/lL6Fwo7IF7NSkYhPK2vO+86nIGltA90MsA==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-transform-template-literals": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.29.7.tgz",
      "integrity": "sha512-NCSEJ4sLFU2gqAub45HYh4fus2yQ36rr6ei6vpU7NdoJqCpxvEG8E6eJpscGyXP3VHD2Ny+fSXr04k1hoUrFqA==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-transform-typeof-symbol": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.29.7.tgz",
      "integrity": "sha512-223mNGoTkBiTEWFoK+Q6Go3tueMRclO8vxxxxquNCYuNI4jWOofFKJRRDu6SDrB8Sgo1UEGW9T4GAQ8ZyRso1A==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-transform-typescript": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.29.7.tgz",
      "integrity": "sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-annotate-as-pure": "^7.29.7",
        "@babel/helper-create-class-features-plugin": "^7.29.7",
        "@babel/helper-plugin-utils": "^7.29.7",
        "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7",
        "@babel/plugin-syntax-typescript": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-transform-unicode-escapes": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.29.7.tgz",
      "integrity": "sha512-jCfXxSjf94lf4E0hKE0AByxF6F3/pVFqRdUUNkDJhsY0m1ZKjnN6ZYyMeHNpzflxb/0q5b7t3p+BE+SLF1WOtA==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-transform-unicode-property-regex": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.29.7.tgz",
      "integrity": "sha512-OgZ+zoAJgZLUCunsTRQ5LAjOywDv5zzZ2/hQ5aMw1pGXyY2rtE8/chXYUmu3AlVHKpm10KEdG9aMwbI/K76ZGw==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-create-regexp-features-plugin": "^7.29.7",
        "@babel/helper-plugin-utils": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-transform-unicode-regex": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.29.7.tgz",
      "integrity": "sha512-7D/x/23/d/3VqZ0QA+LGbZMlGwZjztBygSWWWsfTPoQ1oQ6Q1P6Mr3d0kk42XabyUVw+fha3LqdRsFqeKqvCyA==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-create-regexp-features-plugin": "^7.29.7",
        "@babel/helper-plugin-utils": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/plugin-transform-unicode-sets-regex": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.29.7.tgz",
      "integrity": "sha512-BLOhLht9DOJwIxlmp91wHvkXv1lguuHS3/FwUO8HL1H0u8s4hR1gASVFyilu9iGtcTRYqjTZmlsFFeQletntEg==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-create-regexp-features-plugin": "^7.29.7",
        "@babel/helper-plugin-utils": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0"
      }
    },
    "node_modules/@babel/preset-env": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.7.tgz",
      "integrity": "sha512-GYzX36n1nsciIb0uyH0GHwxwtNwPQIcpxSeiVLDtG/B7jB5xXgchnmL1f/jCX5o+pwnaDBtO60ONSJhEBJfxYA==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/compat-data": "^7.29.7",
        "@babel/helper-compilation-targets": "^7.29.7",
        "@babel/helper-plugin-utils": "^7.29.7",
        "@babel/helper-validator-option": "^7.29.7",
        "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.29.7",
        "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.29.7",
        "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.29.7",
        "@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": "^7.29.7",
        "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.29.7",
        "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.29.7",
        "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2",
        "@babel/plugin-syntax-import-assertions": "^7.29.7",
        "@babel/plugin-syntax-import-attributes": "^7.29.7",
        "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6",
        "@babel/plugin-transform-arrow-functions": "^7.29.7",
        "@babel/plugin-transform-async-generator-functions": "^7.29.7",
        "@babel/plugin-transform-async-to-generator": "^7.29.7",
        "@babel/plugin-transform-block-scoped-functions": "^7.29.7",
        "@babel/plugin-transform-block-scoping": "^7.29.7",
        "@babel/plugin-transform-class-properties": "^7.29.7",
        "@babel/plugin-transform-class-static-block": "^7.29.7",
        "@babel/plugin-transform-classes": "^7.29.7",
        "@babel/plugin-transform-computed-properties": "^7.29.7",
        "@babel/plugin-transform-destructuring": "^7.29.7",
        "@babel/plugin-transform-dotall-regex": "^7.29.7",
        "@babel/plugin-transform-duplicate-keys": "^7.29.7",
        "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.29.7",
        "@babel/plugin-transform-dynamic-import": "^7.29.7",
        "@babel/plugin-transform-explicit-resource-management": "^7.29.7",
        "@babel/plugin-transform-exponentiation-operator": "^7.29.7",
        "@babel/plugin-transform-export-namespace-from": "^7.29.7",
        "@babel/plugin-transform-for-of": "^7.29.7",
        "@babel/plugin-transform-function-name": "^7.29.7",
        "@babel/plugin-transform-json-strings": "^7.29.7",
        "@babel/plugin-transform-literals": "^7.29.7",
        "@babel/plugin-transform-logical-assignment-operators": "^7.29.7",
        "@babel/plugin-transform-member-expression-literals": "^7.29.7",
        "@babel/plugin-transform-modules-amd": "^7.29.7",
        "@babel/plugin-transform-modules-commonjs": "^7.29.7",
        "@babel/plugin-transform-modules-systemjs": "^7.29.7",
        "@babel/plugin-transform-modules-umd": "^7.29.7",
        "@babel/plugin-transform-named-capturing-groups-regex": "^7.29.7",
        "@babel/plugin-transform-new-target": "^7.29.7",
        "@babel/plugin-transform-nullish-coalescing-operator": "^7.29.7",
        "@babel/plugin-transform-numeric-separator": "^7.29.7",
        "@babel/plugin-transform-object-rest-spread": "^7.29.7",
        "@babel/plugin-transform-object-super": "^7.29.7",
        "@babel/plugin-transform-optional-catch-binding": "^7.29.7",
        "@babel/plugin-transform-optional-chaining": "^7.29.7",
        "@babel/plugin-transform-parameters": "^7.29.7",
        "@babel/plugin-transform-private-methods": "^7.29.7",
        "@babel/plugin-transform-private-property-in-object": "^7.29.7",
        "@babel/plugin-transform-property-literals": "^7.29.7",
        "@babel/plugin-transform-regenerator": "^7.29.7",
        "@babel/plugin-transform-regexp-modifiers": "^7.29.7",
        "@babel/plugin-transform-reserved-words": "^7.29.7",
        "@babel/plugin-transform-shorthand-properties": "^7.29.7",
        "@babel/plugin-transform-spread": "^7.29.7",
        "@babel/plugin-transform-sticky-regex": "^7.29.7",
        "@babel/plugin-transform-template-literals": "^7.29.7",
        "@babel/plugin-transform-typeof-symbol": "^7.29.7",
        "@babel/plugin-transform-unicode-escapes": "^7.29.7",
        "@babel/plugin-transform-unicode-property-regex": "^7.29.7",
        "@babel/plugin-transform-unicode-regex": "^7.29.7",
        "@babel/plugin-transform-unicode-sets-regex": "^7.29.7",
        "@babel/preset-modules": "0.1.6-no-external-plugins",
        "babel-plugin-polyfill-corejs2": "^0.4.15",
        "babel-plugin-polyfill-corejs3": "^0.14.0",
        "babel-plugin-polyfill-regenerator": "^0.6.6",
        "core-js-compat": "^3.48.0",
        "semver": "^6.3.1"
      },
      "engines": {
        "node": ">=6.9.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/preset-modules": {
      "version": "0.1.6-no-external-plugins",
      "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz",
      "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.0.0",
        "@babel/types": "^7.4.4",
        "esutils": "^2.0.2"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0"
      }
    },
    "node_modules/@babel/runtime": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
      "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
      "license": "MIT",
      "engines": {
        "node": ">=6.9.0"
      }
    },
    "node_modules/@babel/runtime-corejs3": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.29.7.tgz",
      "integrity": "sha512-ppj9ouYku+RX0ljtgZd+KMO5mkM2bCqg8H2PYAFWnLsHEIKIdRojqbJ2i3eVHrisuxy7nOFCmngTDdWtUCdXUQ==",
      "license": "MIT",
      "dependencies": {
        "core-js-pure": "^3.48.0"
      },
      "engines": {
        "node": ">=6.9.0"
      }
    },
    "node_modules/@babel/template": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
      "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
      "license": "MIT",
      "dependencies": {
        "@babel/code-frame": "^7.29.7",
        "@babel/parser": "^7.29.7",
        "@babel/types": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      }
    },
    "node_modules/@babel/template/node_modules/@babel/code-frame": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
      "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
      "license": "MIT",
      "dependencies": {
        "@babel/helper-validator-identifier": "^7.29.7",
        "js-tokens": "^4.0.0",
        "picocolors": "^1.1.1"
      },
      "engines": {
        "node": ">=6.9.0"
      }
    },
    "node_modules/@babel/template/node_modules/@babel/parser": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
      "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
      "license": "MIT",
      "dependencies": {
        "@babel/types": "^7.29.7"
      },
      "bin": {
        "parser": "bin/babel-parser.js"
      },
      "engines": {
        "node": ">=6.0.0"
      }
    },
    "node_modules/@babel/template/node_modules/@babel/types": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
      "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
      "license": "MIT",
      "dependencies": {
        "@babel/helper-string-parser": "^7.29.7",
        "@babel/helper-validator-identifier": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      }
    },
    "node_modules/@babel/template/node_modules/picocolors": {
      "version": "1.1.1",
      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
      "license": "ISC"
    },
    "node_modules/@babel/traverse": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz",
      "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==",
      "license": "MIT",
      "dependencies": {
        "@babel/code-frame": "^7.29.7",
        "@babel/generator": "^7.29.7",
        "@babel/helper-globals": "^7.29.7",
        "@babel/parser": "^7.29.7",
        "@babel/template": "^7.29.7",
        "@babel/types": "^7.29.7",
        "debug": "^4.3.1"
      },
      "engines": {
        "node": ">=6.9.0"
      }
    },
    "node_modules/@babel/traverse/node_modules/@babel/code-frame": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
      "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
      "license": "MIT",
      "dependencies": {
        "@babel/helper-validator-identifier": "^7.29.7",
        "js-tokens": "^4.0.0",
        "picocolors": "^1.1.1"
      },
      "engines": {
        "node": ">=6.9.0"
      }
    },
    "node_modules/@babel/traverse/node_modules/@babel/parser": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
      "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
      "license": "MIT",
      "dependencies": {
        "@babel/types": "^7.29.7"
      },
      "bin": {
        "parser": "bin/babel-parser.js"
      },
      "engines": {
        "node": ">=6.0.0"
      }
    },
    "node_modules/@babel/traverse/node_modules/@babel/types": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
      "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
      "license": "MIT",
      "dependencies": {
        "@babel/helper-string-parser": "^7.29.7",
        "@babel/helper-validator-identifier": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      }
    },
    "node_modules/@babel/traverse/node_modules/picocolors": {
      "version": "1.1.1",
      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
      "license": "ISC"
    },
    "node_modules/@babel/types": {
      "version": "7.25.6",
      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.6.tgz",
      "integrity": "sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==",
      "license": "MIT",
      "dependencies": {
        "@babel/helper-string-parser": "^7.24.8",
        "@babel/helper-validator-identifier": "^7.24.7",
        "to-fast-properties": "^2.0.0"
      },
      "engines": {
        "node": ">=6.9.0"
      }
    },
    "node_modules/@bcoe/v8-coverage": {
      "version": "0.2.3",
      "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz",
      "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
      "dev": true,
      "license": "MIT",
      "peer": true
    },
    "node_modules/@dcloudio/types": {
      "version": "3.4.28",
      "resolved": "https://registry.npmjs.org/@dcloudio/types/-/types-3.4.28.tgz",
      "integrity": "sha512-uVIRp1VLBkrL0LaGLgIS/sT3bl1zzVTKZQbfqJEQcSAvBffRdirbSh5OvOHfA1WV5lmCAGfjhKsUQouNEVUQHg==",
      "license": "Apache-2.0"
    },
    "node_modules/@dcloudio/uni-app": {
      "version": "3.0.0-5000720260410001",
      "resolved": "https://registry.npmjs.org/@dcloudio/uni-app/-/uni-app-3.0.0-5000720260410001.tgz",
      "integrity": "sha512-1JWt4rSnYDjj6bSThDKcW8OhP68qQlsY1Zd/GBIUsiRTVSrkvA3xVwmeV5odXirpZeH5xthus4eIRdv/Qmmheg==",
      "license": "Apache-2.0",
      "dependencies": {
        "@dcloudio/uni-cloud": "3.0.0-5000720260410001",
        "@dcloudio/uni-components": "3.0.0-5000720260410001",
        "@dcloudio/uni-console": "3.0.0-5000720260410001",
        "@dcloudio/uni-i18n": "3.0.0-5000720260410001",
        "@dcloudio/uni-push": "3.0.0-5000720260410001",
        "@dcloudio/uni-shared": "3.0.0-5000720260410001",
        "@dcloudio/uni-stat": "3.0.0-5000720260410001",
        "@vue/shared": "3.4.21"
      },
      "peerDependencies": {
        "@dcloudio/types": "3.4.28"
      }
    },
    "node_modules/@dcloudio/uni-app-harmony": {
      "version": "3.0.0-5000720260410001",
      "resolved": "https://registry.npmjs.org/@dcloudio/uni-app-harmony/-/uni-app-harmony-3.0.0-5000720260410001.tgz",
      "integrity": "sha512-pzhLF9Xiqc4kx4SbR4+9JS0QFIuoglWw5w8gC4WtTK8qyOvpQqYOWQNPAqw72IvVo/nj85b/MzWWr7eDfm39NQ==",
      "license": "Apache-2.0",
      "dependencies": {
        "@dcloudio/uni-app-uts": "3.0.0-5000720260410001",
        "@dcloudio/uni-app-vite": "3.0.0-5000720260410001",
        "debug": "4.3.7",
        "fs-extra": "10.1.0",
        "licia": "1.41.1",
        "postcss-selector-parser": "6.1.2"
      }
    },
    "node_modules/@dcloudio/uni-app-plus": {
      "version": "3.0.0-5000720260410001",
      "resolved": "https://registry.npmjs.org/@dcloudio/uni-app-plus/-/uni-app-plus-3.0.0-5000720260410001.tgz",
      "integrity": "sha512-8i8I0d2JwrbP9ShRNmmuiw2sC3aaUciTx1Uqqzx6OwKZm4zwAiUzp4dbWadyKxjdnuwzfNF4v664hAgLOoiUdg==",
      "license": "Apache-2.0",
      "dependencies": {
        "@dcloudio/uni-app-uts": "3.0.0-5000720260410001",
        "@dcloudio/uni-app-vite": "3.0.0-5000720260410001",
        "@dcloudio/uni-app-vue": "3.0.0-5000720260410001",
        "debug": "4.3.7",
        "fs-extra": "10.1.0",
        "licia": "1.41.1",
        "postcss-selector-parser": "6.1.2"
      }
    },
    "node_modules/@dcloudio/uni-app-uts": {
      "version": "3.0.0-5000720260410001",
      "resolved": "https://registry.npmjs.org/@dcloudio/uni-app-uts/-/uni-app-uts-3.0.0-5000720260410001.tgz",
      "integrity": "sha512-SRtr9V7oSSWT3gmD72co36XLcUQrKWxA5YN9LWs6zfDaaOLHHNDnzDaf/Ozn57p+ixpfusawvJd06bS3r65Bag==",
      "license": "Apache-2.0",
      "dependencies": {
        "@babel/parser": "7.25.6",
        "@babel/types": "7.25.6",
        "@dcloudio/uni-cli-shared": "3.0.0-5000720260410001",
        "@dcloudio/uni-console": "3.0.0-5000720260410001",
        "@dcloudio/uni-i18n": "3.0.0-5000720260410001",
        "@dcloudio/uni-nvue-styler": "3.0.0-5000720260410001",
        "@dcloudio/uni-shared": "3.0.0-5000720260410001",
        "@jridgewell/gen-mapping": "^0.3.3",
        "@jridgewell/trace-mapping": "^0.3.19",
        "@rollup/pluginutils": "5.1.0",
        "@vue/compiler-core": "3.4.21",
        "@vue/compiler-dom": "3.4.21",
        "@vue/compiler-sfc": "3.4.21",
        "@vue/consolidate": "1.0.0",
        "@vue/shared": "3.4.21",
        "debug": "4.3.7",
        "es-module-lexer": "1.5.4",
        "estree-walker": "2.0.2",
        "fast-glob": "3.3.3",
        "fs-extra": "10.1.0",
        "magic-string": "0.30.11",
        "picocolors": "1.1.0",
        "source-map-js": "1.2.1",
        "unimport": "4.1.1"
      }
    },
    "node_modules/@dcloudio/uni-app-vite": {
      "version": "3.0.0-5000720260410001",
      "resolved": "https://registry.npmjs.org/@dcloudio/uni-app-vite/-/uni-app-vite-3.0.0-5000720260410001.tgz",
      "integrity": "sha512-qJJBIzBzhe9ogG3CTIANru5cBxGwHt4Qv+izrBD3bff03Zxq91z5LULcY+HrlFgRIDl7p7oCFpMyMQdEaIeDGw==",
      "license": "Apache-2.0",
      "dependencies": {
        "@dcloudio/uni-cli-shared": "3.0.0-5000720260410001",
        "@dcloudio/uni-i18n": "3.0.0-5000720260410001",
        "@dcloudio/uni-nvue-styler": "3.0.0-5000720260410001",
        "@dcloudio/uni-shared": "3.0.0-5000720260410001",
        "@rollup/pluginutils": "5.1.0",
        "@vitejs/plugin-vue": "5.2.4",
        "@vue/compiler-dom": "3.4.21",
        "@vue/compiler-sfc": "3.4.21",
        "debug": "4.3.7",
        "fs-extra": "10.1.0",
        "picocolors": "1.1.0"
      }
    },
    "node_modules/@dcloudio/uni-app-vue": {
      "version": "3.0.0-5000720260410001",
      "resolved": "https://registry.npmjs.org/@dcloudio/uni-app-vue/-/uni-app-vue-3.0.0-5000720260410001.tgz",
      "integrity": "sha512-o/svSkAGrwnPOnM/WQfpLyiCt8sgogzubow9ze+h7f50kcTSTgJl1130Kk56D+YCLZBKMwXYXHWXyEZj+haYCA==",
      "license": "Apache-2.0"
    },
    "node_modules/@dcloudio/uni-automator": {
      "version": "3.0.0-5000720260410001",
      "resolved": "https://registry.npmjs.org/@dcloudio/uni-automator/-/uni-automator-3.0.0-5000720260410001.tgz",
      "integrity": "sha512-H1HOVwEdpALzPsADObkfTXAW8Uo5V2XFAsXNOPhoYYQmzs8cjHJU2UljgpMO1itvZO1O6oIMsGEH5E4kQdrkmg==",
      "dev": true,
      "license": "Apache-2.0",
      "dependencies": {
        "@dcloudio/uni-cli-shared": "3.0.0-5000720260410001",
        "address": "^1.1.2",
        "cross-env": "^7.0.3",
        "debug": "4.3.7",
        "default-gateway": "^6.0.3",
        "fs-extra": "10.1.0",
        "jsonc-parser": "3.3.1",
        "licia": "1.41.1",
        "merge": "2.1.1",
        "qrcode-reader": "1.0.4",
        "qrcode-terminal": "0.12.0",
        "ws": "8.18.0"
      },
      "peerDependencies": {
        "jest": "27.0.4",
        "jest-environment-node": "27.5.1"
      }
    },
    "node_modules/@dcloudio/uni-cli-shared": {
      "version": "3.0.0-5000720260410001",
      "resolved": "https://registry.npmjs.org/@dcloudio/uni-cli-shared/-/uni-cli-shared-3.0.0-5000720260410001.tgz",
      "integrity": "sha512-UW0eEjO+GgmHixnSfBJxpI1fTW+0VgyjPTFUL+L7zNtIpI8eLEQHrGEbAC8IIOi4hAkVN3riDk80Dg2i3KSGqA==",
      "license": "Apache-2.0",
      "dependencies": {
        "@ampproject/remapping": "^2.1.2",
        "@babel/code-frame": "7.24.7",
        "@babel/core": "7.25.2",
        "@babel/parser": "7.25.6",
        "@babel/types": "7.25.6",
        "@dcloudio/uni-i18n": "3.0.0-5000720260410001",
        "@dcloudio/uni-nvue-styler": "3.0.0-5000720260410001",
        "@dcloudio/uni-shared": "3.0.0-5000720260410001",
        "@intlify/core-base": "9.1.9",
        "@intlify/shared": "9.1.9",
        "@intlify/vue-devtools": "9.1.9",
        "@rollup/pluginutils": "5.1.0",
        "@vue/compiler-core": "3.4.21",
        "@vue/compiler-dom": "3.4.21",
        "@vue/compiler-sfc": "3.4.21",
        "@vue/compiler-ssr": "3.4.21",
        "@vue/server-renderer": "3.4.21",
        "@vue/shared": "3.4.21",
        "adm-zip": "0.5.16",
        "autoprefixer": "10.4.20",
        "base64url": "^3.0.1",
        "chokidar": "3.6.0",
        "compare-versions": "^3.6.0",
        "debug": "4.3.7",
        "entities": "^7.0.0",
        "es-module-lexer": "1.5.4",
        "esbuild": "0.20.2",
        "estree-walker": "2.0.2",
        "fast-glob": "3.3.3",
        "fs-extra": "10.1.0",
        "hash-sum": "2.0.0",
        "isbinaryfile": "5.0.2",
        "jsonc-parser": "3.3.1",
        "lines-and-columns": "^2.0.4",
        "magic-string": "0.30.11",
        "merge": "2.1.1",
        "mime": "3.0.0",
        "module-alias": "2.2.3",
        "os-locale-s-fix": "^1.0.8-fix-1",
        "picocolors": "1.1.0",
        "postcss-import": "^14.0.2",
        "postcss-load-config": "^3.1.1",
        "postcss-modules": "^4.3.0",
        "postcss-selector-parser": "6.1.2",
        "resolve": "1.22.8",
        "source-map-js": "1.2.1",
        "tapable": "^2.2.0",
        "tinycolor2": "1.6.0",
        "unimport": "4.1.1",
        "unplugin-auto-import": "19.1.0",
        "xregexp": "5.1.2"
      },
      "engines": {
        "node": "^14.18.0 || >=16.0.0"
      }
    },
    "node_modules/@dcloudio/uni-cli-shared/node_modules/picomatch": {
      "version": "4.0.4",
      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
      "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
      "license": "MIT",
      "engines": {
        "node": ">=12"
      },
      "funding": {
        "url": "https://github.com/sponsors/jonschlinkert"
      }
    },
    "node_modules/@dcloudio/uni-cli-shared/node_modules/unplugin-auto-import": {
      "version": "19.1.0",
      "resolved": "https://registry.npmjs.org/unplugin-auto-import/-/unplugin-auto-import-19.1.0.tgz",
      "integrity": "sha512-B+TGBEBHqY9aR+7YfShfLujETOHstzpV+yaqgy5PkfV0QG7Py+TYMX7vJ9W4SrysHR+UzR+gzcx/nuZjmPeclA==",
      "license": "MIT",
      "dependencies": {
        "local-pkg": "^1.0.0",
        "magic-string": "^0.30.17",
        "picomatch": "^4.0.2",
        "unimport": "^4.1.1",
        "unplugin": "^2.2.0",
        "unplugin-utils": "^0.2.4"
      },
      "engines": {
        "node": ">=14"
      },
      "funding": {
        "url": "https://github.com/sponsors/antfu"
      },
      "peerDependencies": {
        "@nuxt/kit": "^3.2.2",
        "@vueuse/core": "*"
      },
      "peerDependenciesMeta": {
        "@nuxt/kit": {
          "optional": true
        },
        "@vueuse/core": {
          "optional": true
        }
      }
    },
    "node_modules/@dcloudio/uni-cli-shared/node_modules/unplugin-auto-import/node_modules/magic-string": {
      "version": "0.30.21",
      "resolved": "https://registry.npmjs.org/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/@dcloudio/uni-cloud": {
      "version": "3.0.0-5000720260410001",
      "resolved": "https://registry.npmjs.org/@dcloudio/uni-cloud/-/uni-cloud-3.0.0-5000720260410001.tgz",
      "integrity": "sha512-VGM8jhB3ol80mjvpSf+9gZApkYc2QDEk12ODTQKdQZbmjR33K7eqt8mpefErNYumnRmdQ4iL4oPArJ+O4GZpog==",
      "license": "Apache-2.0",
      "dependencies": {
        "@dcloudio/uni-cli-shared": "3.0.0-5000720260410001",
        "@dcloudio/uni-i18n": "3.0.0-5000720260410001",
        "@dcloudio/uni-shared": "3.0.0-5000720260410001",
        "@vue/shared": "3.4.21",
        "fast-glob": "3.3.3"
      }
    },
    "node_modules/@dcloudio/uni-components": {
      "version": "3.0.0-5000720260410001",
      "resolved": "https://registry.npmjs.org/@dcloudio/uni-components/-/uni-components-3.0.0-5000720260410001.tgz",
      "integrity": "sha512-ZwGKA1/mucO6SufgfN6JL1MFmeXFaEAoZUbXck8mvk/vwVduZHJeocWousF6bJtGMm23FM5ItG31KjcnwQpPjQ==",
      "license": "Apache-2.0",
      "dependencies": {
        "@dcloudio/uni-cloud": "3.0.0-5000720260410001",
        "@dcloudio/uni-h5": "3.0.0-5000720260410001",
        "@dcloudio/uni-i18n": "3.0.0-5000720260410001"
      }
    },
    "node_modules/@dcloudio/uni-console": {
      "version": "3.0.0-5000720260410001",
      "resolved": "https://registry.npmjs.org/@dcloudio/uni-console/-/uni-console-3.0.0-5000720260410001.tgz",
      "integrity": "sha512-ropH7p+fk4tjnFEex2FRqNPzK2g8Fs9OixJmDoR7th+7VZJ13LICR1DMd9cbwtERDECSbs5AJSQ/GU6/UP4P0Q==",
      "license": "Apache-2.0",
      "dependencies": {
        "@dcloudio/uni-cli-shared": "3.0.0-5000720260410001",
        "fs-extra": "10.1.0"
      }
    },
    "node_modules/@dcloudio/uni-h5": {
      "version": "3.0.0-5000720260410001",
      "resolved": "https://registry.npmjs.org/@dcloudio/uni-h5/-/uni-h5-3.0.0-5000720260410001.tgz",
      "integrity": "sha512-N2Cp08qRfICs5yjNQ3Jnz3E4iM0EsDsCbyISOfHDcXeBeDNHAV57ZFMwI0yB4OpGOFp+th8dW2V50t/LlIcqfw==",
      "license": "Apache-2.0",
      "dependencies": {
        "@dcloudio/uni-h5-vite": "3.0.0-5000720260410001",
        "@dcloudio/uni-h5-vue": "3.0.0-5000720260410001",
        "@dcloudio/uni-i18n": "3.0.0-5000720260410001",
        "@dcloudio/uni-shared": "3.0.0-5000720260410001",
        "@vue/server-renderer": "3.4.21",
        "@vue/shared": "3.4.21",
        "debug": "4.3.7",
        "localstorage-polyfill": "^1.0.1",
        "postcss-selector-parser": "6.1.2",
        "safe-area-insets": "1.4.1",
        "vue-router": "4.4.4",
        "xmlhttprequest": "^1.8.0"
      }
    },
    "node_modules/@dcloudio/uni-h5-vite": {
      "version": "3.0.0-5000720260410001",
      "resolved": "https://registry.npmjs.org/@dcloudio/uni-h5-vite/-/uni-h5-vite-3.0.0-5000720260410001.tgz",
      "integrity": "sha512-wqbr6VMH89RPbtWGL1EkCNyz9krtbd1XeobfwWkHjlJZnrgw0kf5KJQcS1Pi5v+mSoUi2L+jloOzCUyGKVZ4VQ==",
      "license": "Apache-2.0",
      "dependencies": {
        "@dcloudio/uni-cli-shared": "3.0.0-5000720260410001",
        "@dcloudio/uni-shared": "3.0.0-5000720260410001",
        "@rollup/pluginutils": "5.1.0",
        "@vue/compiler-dom": "3.4.21",
        "@vue/compiler-sfc": "3.4.21",
        "@vue/server-renderer": "3.4.21",
        "@vue/shared": "3.4.21",
        "debug": "4.3.7",
        "fs-extra": "10.1.0",
        "mime": "3.0.0",
        "module-alias": "2.2.3"
      }
    },
    "node_modules/@dcloudio/uni-h5-vue": {
      "version": "3.0.0-5000720260410001",
      "resolved": "https://registry.npmjs.org/@dcloudio/uni-h5-vue/-/uni-h5-vue-3.0.0-5000720260410001.tgz",
      "integrity": "sha512-69fkZ1rMFE+RBGbQeBBOI/fa0jKBbJLZN3HSu/bZqGGeqjGZQz22FdH5luYXyQuNxxT+rXTIFQlV5Z+NM2hr/w==",
      "license": "Apache-2.0",
      "dependencies": {
        "@dcloudio/uni-shared": "3.0.0-5000720260410001",
        "@vue/server-renderer": "3.4.21"
      }
    },
    "node_modules/@dcloudio/uni-i18n": {
      "version": "3.0.0-5000720260410001",
      "resolved": "https://registry.npmjs.org/@dcloudio/uni-i18n/-/uni-i18n-3.0.0-5000720260410001.tgz",
      "integrity": "sha512-6X/2eRqbAmQR1bUJpzCjr2xKXDY2+3rQWsvQ5RjwKQVFsz641GlVlUdxI6YwIZumGDRiCIz8T9eGfyIC6pGt9Q==",
      "license": "Apache-2.0"
    },
    "node_modules/@dcloudio/uni-mp-alipay": {
      "version": "3.0.0-5000720260410001",
      "resolved": "https://registry.npmjs.org/@dcloudio/uni-mp-alipay/-/uni-mp-alipay-3.0.0-5000720260410001.tgz",
      "integrity": "sha512-09T0my/A3Vm6Qocuq7g6b/nvUECJF1cnEkfKuyrF9SAVwVOSAdsYdltoUpo3vkz0ktladKdRBwWXk26sQUz27A==",
      "license": "Apache-2.0",
      "dependencies": {
        "@dcloudio/uni-cli-shared": "3.0.0-5000720260410001",
        "@dcloudio/uni-mp-compiler": "3.0.0-5000720260410001",
        "@dcloudio/uni-mp-vite": "3.0.0-5000720260410001",
        "@dcloudio/uni-mp-vue": "3.0.0-5000720260410001",
        "@dcloudio/uni-shared": "3.0.0-5000720260410001",
        "@vue/compiler-core": "3.4.21",
        "@vue/shared": "3.4.21"
      }
    },
    "node_modules/@dcloudio/uni-mp-baidu": {
      "version": "3.0.0-5000720260410001",
      "resolved": "https://registry.npmjs.org/@dcloudio/uni-mp-baidu/-/uni-mp-baidu-3.0.0-5000720260410001.tgz",
      "integrity": "sha512-pD8p7t6scgNcyI2uzgkyktxL8yCxQPTs88byW5s/zPbnT4NMJHCf/49UNA6qJQdpcVtvtiPEsj1tmFbAyGaszw==",
      "license": "Apache-2.0",
      "dependencies": {
        "@dcloudio/uni-app": "3.0.0-5000720260410001",
        "@dcloudio/uni-cli-shared": "3.0.0-5000720260410001",
        "@dcloudio/uni-mp-compiler": "3.0.0-5000720260410001",
        "@dcloudio/uni-mp-vite": "3.0.0-5000720260410001",
        "@dcloudio/uni-mp-vue": "3.0.0-5000720260410001",
        "@dcloudio/uni-mp-weixin": "3.0.0-5000720260410001",
        "@dcloudio/uni-shared": "3.0.0-5000720260410001",
        "@vue/compiler-core": "3.4.21",
        "@vue/shared": "3.4.21",
        "jimp": "0.10.3",
        "licia": "1.41.1",
        "qrcode-reader": "1.0.4",
        "qrcode-terminal": "0.12.0",
        "ws": "8.18.0"
      }
    },
    "node_modules/@dcloudio/uni-mp-compiler": {
      "version": "3.0.0-5000720260410001",
      "resolved": "https://registry.npmjs.org/@dcloudio/uni-mp-compiler/-/uni-mp-compiler-3.0.0-5000720260410001.tgz",
      "integrity": "sha512-2+61GVODCLj8EOV3dhKQq8db8Ru8AHHv04Xm1bW188m3PbD6lh5hgGUc5HeKuQFdVxUzS4U0UtUtwGavPRc9ww==",
      "license": "Apache-2.0",
      "dependencies": {
        "@babel/generator": "7.25.6",
        "@babel/parser": "7.25.6",
        "@babel/types": "7.25.6",
        "@dcloudio/uni-cli-shared": "3.0.0-5000720260410001",
        "@dcloudio/uni-shared": "3.0.0-5000720260410001",
        "@vue/compiler-core": "3.4.21",
        "@vue/compiler-dom": "3.4.21",
        "@vue/shared": "3.4.21",
        "estree-walker": "2.0.2"
      }
    },
    "node_modules/@dcloudio/uni-mp-compiler/node_modules/@babel/generator": {
      "version": "7.25.6",
      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.6.tgz",
      "integrity": "sha512-VPC82gr1seXOpkjAAKoLhP50vx4vGNlF4msF64dSFq1P8RfB+QAuJWGHPXXPc8QyfVWwwB/TNNU4+ayZmHNbZw==",
      "license": "MIT",
      "dependencies": {
        "@babel/types": "^7.25.6",
        "@jridgewell/gen-mapping": "^0.3.5",
        "@jridgewell/trace-mapping": "^0.3.25",
        "jsesc": "^2.5.1"
      },
      "engines": {
        "node": ">=6.9.0"
      }
    },
    "node_modules/@dcloudio/uni-mp-compiler/node_modules/jsesc": {
      "version": "2.5.2",
      "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
      "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
      "license": "MIT",
      "bin": {
        "jsesc": "bin/jsesc"
      },
      "engines": {
        "node": ">=4"
      }
    },
    "node_modules/@dcloudio/uni-mp-harmony": {
      "version": "3.0.0-5000720260410001",
      "resolved": "https://registry.npmjs.org/@dcloudio/uni-mp-harmony/-/uni-mp-harmony-3.0.0-5000720260410001.tgz",
      "integrity": "sha512-nQV5UTrgRMdf5AtOAIwNDkRUBXvLQLIrilUTCMB2E1ILgrmwf5jO/1+ghUQRiBRL5UeXF+o7GRpSN572bcBtcg==",
      "license": "Apache-2.0",
      "dependencies": {
        "@dcloudio/uni-cli-shared": "3.0.0-5000720260410001",
        "@dcloudio/uni-mp-toutiao": "3.0.0-5000720260410001",
        "@dcloudio/uni-mp-vite": "3.0.0-5000720260410001",
        "@dcloudio/uni-mp-vue": "3.0.0-5000720260410001",
        "@dcloudio/uni-quickapp-webview": "3.0.0-5000720260410001",
        "@dcloudio/uni-shared": "3.0.0-5000720260410001",
        "@vue/shared": "3.4.21"
      }
    },
    "node_modules/@dcloudio/uni-mp-jd": {
      "version": "3.0.0-5000720260410001",
      "resolved": "https://registry.npmjs.org/@dcloudio/uni-mp-jd/-/uni-mp-jd-3.0.0-5000720260410001.tgz",
      "integrity": "sha512-RNawlyPrJVTH4VHCXhJcokSZ5XS4niBCvgEI4h96qTT1SfyMXveJwATGlc3n80mVeqKjJy5FnGdUjPSsMO5L4Q==",
      "license": "Apache-2.0",
      "dependencies": {
        "@dcloudio/uni-cli-shared": "3.0.0-5000720260410001",
        "@dcloudio/uni-mp-compiler": "3.0.0-5000720260410001",
        "@dcloudio/uni-mp-vite": "3.0.0-5000720260410001",
        "@dcloudio/uni-mp-vue": "3.0.0-5000720260410001",
        "@dcloudio/uni-shared": "3.0.0-5000720260410001",
        "@vue/shared": "3.4.21"
      }
    },
    "node_modules/@dcloudio/uni-mp-kuaishou": {
      "version": "3.0.0-5000720260410001",
      "resolved": "https://registry.npmjs.org/@dcloudio/uni-mp-kuaishou/-/uni-mp-kuaishou-3.0.0-5000720260410001.tgz",
      "integrity": "sha512-4lybG8gYPbrU2Gdjg5Q3IJxB9Ac8AH5k6CJVM9t0mvRzc8TBFrFGn30qnpzvGOgXDgWaRVjkHouiLB36SPFBLw==",
      "license": "Apache-2.0",
      "dependencies": {
        "@dcloudio/uni-cli-shared": "3.0.0-5000720260410001",
        "@dcloudio/uni-mp-compiler": "3.0.0-5000720260410001",
        "@dcloudio/uni-mp-vite": "3.0.0-5000720260410001",
        "@dcloudio/uni-mp-vue": "3.0.0-5000720260410001",
        "@dcloudio/uni-mp-weixin": "3.0.0-5000720260410001",
        "@dcloudio/uni-shared": "3.0.0-5000720260410001",
        "@vue/compiler-core": "3.4.21",
        "@vue/shared": "3.4.21"
      }
    },
    "node_modules/@dcloudio/uni-mp-lark": {
      "version": "3.0.0-5000720260410001",
      "resolved": "https://registry.npmjs.org/@dcloudio/uni-mp-lark/-/uni-mp-lark-3.0.0-5000720260410001.tgz",
      "integrity": "sha512-8POfMKrFgRcOrWJLGmklXvNPhDjm5ZKHjcRyg00d5QFyh2mQRq27X6zNlSU9F/ecJoVLxgFxkRyjK8cJYRPfxA==",
      "license": "Apache-2.0",
      "dependencies": {
        "@dcloudio/uni-cli-shared": "3.0.0-5000720260410001",
        "@dcloudio/uni-mp-compiler": "3.0.0-5000720260410001",
        "@dcloudio/uni-mp-toutiao": "3.0.0-5000720260410001",
        "@dcloudio/uni-mp-vite": "3.0.0-5000720260410001",
        "@dcloudio/uni-mp-vue": "3.0.0-5000720260410001",
        "@dcloudio/uni-shared": "3.0.0-5000720260410001",
        "@vue/compiler-core": "3.4.21",
        "@vue/shared": "3.4.21"
      }
    },
    "node_modules/@dcloudio/uni-mp-qq": {
      "version": "3.0.0-5000720260410001",
      "resolved": "https://registry.npmjs.org/@dcloudio/uni-mp-qq/-/uni-mp-qq-3.0.0-5000720260410001.tgz",
      "integrity": "sha512-vt6XcFGEwY5S9n6UvndKZb3YT6jTpQcctzVzSZI12cu8/nxCVGbrnZosyxNKf2g8sRLlFkN2VahJaFXzWBYn9A==",
      "license": "Apache-2.0",
      "dependencies": {
        "@dcloudio/uni-cli-shared": "3.0.0-5000720260410001",
        "@dcloudio/uni-mp-compiler": "3.0.0-5000720260410001",
        "@dcloudio/uni-mp-vite": "3.0.0-5000720260410001",
        "@dcloudio/uni-mp-vue": "3.0.0-5000720260410001",
        "@dcloudio/uni-shared": "3.0.0-5000720260410001",
        "@vue/shared": "3.4.21",
        "fs-extra": "10.1.0"
      }
    },
    "node_modules/@dcloudio/uni-mp-toutiao": {
      "version": "3.0.0-5000720260410001",
      "resolved": "https://registry.npmjs.org/@dcloudio/uni-mp-toutiao/-/uni-mp-toutiao-3.0.0-5000720260410001.tgz",
      "integrity": "sha512-obnJZ+lASqWFybC72DewGiN44TYJpcVEcQJL0CQYtv77+N5mLa7eiTd0H4vsl8IRF84qVNsz+QuBQgAn/w42QA==",
      "license": "Apache-2.0",
      "dependencies": {
        "@dcloudio/uni-cli-shared": "3.0.0-5000720260410001",
        "@dcloudio/uni-mp-compiler": "3.0.0-5000720260410001",
        "@dcloudio/uni-mp-vite": "3.0.0-5000720260410001",
        "@dcloudio/uni-mp-vue": "3.0.0-5000720260410001",
        "@dcloudio/uni-shared": "3.0.0-5000720260410001",
        "@vue/compiler-core": "3.4.21",
        "@vue/shared": "3.4.21"
      }
    },
    "node_modules/@dcloudio/uni-mp-vite": {
      "version": "3.0.0-5000720260410001",
      "resolved": "https://registry.npmjs.org/@dcloudio/uni-mp-vite/-/uni-mp-vite-3.0.0-5000720260410001.tgz",
      "integrity": "sha512-SH0WF3RNiANzhankhqLJT0dPDnbty6mTjj8JUibrDcj3MwplO/jgmiRxECMFKJGmE//xfRlaZgDCAUX5tHj76w==",
      "license": "Apache-2.0",
      "dependencies": {
        "@dcloudio/uni-cli-shared": "3.0.0-5000720260410001",
        "@dcloudio/uni-i18n": "3.0.0-5000720260410001",
        "@dcloudio/uni-mp-compiler": "3.0.0-5000720260410001",
        "@dcloudio/uni-mp-vue": "3.0.0-5000720260410001",
        "@dcloudio/uni-shared": "3.0.0-5000720260410001",
        "@vue/compiler-dom": "3.4.21",
        "@vue/compiler-sfc": "3.4.21",
        "@vue/shared": "3.4.21",
        "debug": "4.3.7"
      }
    },
    "node_modules/@dcloudio/uni-mp-vue": {
      "version": "3.0.0-5000720260410001",
      "resolved": "https://registry.npmjs.org/@dcloudio/uni-mp-vue/-/uni-mp-vue-3.0.0-5000720260410001.tgz",
      "integrity": "sha512-IzPJ94VJdeo0OOdZ+Knz7RKXvxyw5t2gFo3ti072Xfh/nXIv4KgfcgkrofKHz1iYBwRZE/MYW92ZYwJt/2Q+1g==",
      "license": "Apache-2.0",
      "dependencies": {
        "@dcloudio/uni-shared": "3.0.0-5000720260410001",
        "@vue/shared": "3.4.21"
      }
    },
    "node_modules/@dcloudio/uni-mp-weixin": {
      "version": "3.0.0-5000720260410001",
      "resolved": "https://registry.npmjs.org/@dcloudio/uni-mp-weixin/-/uni-mp-weixin-3.0.0-5000720260410001.tgz",
      "integrity": "sha512-mS9yLbBbPn2OWtMnKZgjuQuiJW/f1GW51qTqblWz/T6viHeYoReAge2UfOGCHEAgO/HVLK5vlm8eZLP3jnQOBA==",
      "license": "Apache-2.0",
      "dependencies": {
        "@dcloudio/uni-cli-shared": "3.0.0-5000720260410001",
        "@dcloudio/uni-mp-compiler": "3.0.0-5000720260410001",
        "@dcloudio/uni-mp-vite": "3.0.0-5000720260410001",
        "@dcloudio/uni-mp-vue": "3.0.0-5000720260410001",
        "@dcloudio/uni-shared": "3.0.0-5000720260410001",
        "@vue/shared": "3.4.21",
        "jimp": "0.10.3",
        "licia": "1.41.1",
        "qrcode-reader": "1.0.4",
        "qrcode-terminal": "0.12.0",
        "ws": "8.18.0"
      }
    },
    "node_modules/@dcloudio/uni-mp-xhs": {
      "version": "3.0.0-5000720260410001",
      "resolved": "https://registry.npmjs.org/@dcloudio/uni-mp-xhs/-/uni-mp-xhs-3.0.0-5000720260410001.tgz",
      "integrity": "sha512-ambnwwAJJ9xeT13wvHTuf+FCWBH/VqaHTPpziJowFDTnqNkXGqFLRrMAd/EPj3gIxUjW7zvspsudTlUvqjAv1g==",
      "license": "Apache-2.0",
      "dependencies": {
        "@dcloudio/uni-cli-shared": "3.0.0-5000720260410001",
        "@dcloudio/uni-mp-compiler": "3.0.0-5000720260410001",
        "@dcloudio/uni-mp-vite": "3.0.0-5000720260410001",
        "@dcloudio/uni-mp-vue": "3.0.0-5000720260410001",
        "@dcloudio/uni-shared": "3.0.0-5000720260410001",
        "@vue/shared": "3.4.21"
      }
    },
    "node_modules/@dcloudio/uni-nvue-styler": {
      "version": "3.0.0-5000720260410001",
      "resolved": "https://registry.npmjs.org/@dcloudio/uni-nvue-styler/-/uni-nvue-styler-3.0.0-5000720260410001.tgz",
      "integrity": "sha512-7eFvhU67IDBxfyLP5zUyRTQPzJFoJ0WeV7QNpgvJf4FznynAGLfZ0Rh8Zgan8vxWa+PSSbpooVQbjCYJXhq5lg==",
      "license": "Apache-2.0",
      "dependencies": {
        "@vue/shared": "3.4.21",
        "parse-css-font": "^4.0.0",
        "postcss": "8.4.45",
        "tinycolor2": "1.6.0"
      }
    },
    "node_modules/@dcloudio/uni-nvue-styler/node_modules/postcss": {
      "version": "8.4.45",
      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.45.tgz",
      "integrity": "sha512-7KTLTdzdZZYscUc65XmjFiB73vBhBfbPztCYdUNvlaso9PrzjzcmjqBPR0lNGkcVlcO4BjiO5rK/qNz+XAen1Q==",
      "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.7",
        "picocolors": "^1.0.1",
        "source-map-js": "^1.2.0"
      },
      "engines": {
        "node": "^10 || ^12 || >=14"
      }
    },
    "node_modules/@dcloudio/uni-push": {
      "version": "3.0.0-5000720260410001",
      "resolved": "https://registry.npmjs.org/@dcloudio/uni-push/-/uni-push-3.0.0-5000720260410001.tgz",
      "integrity": "sha512-oCmufDQ20AFxwBs7Aoz9BDWx1KMSW76bKU6AQ8YqM1MPxCDLif5d830Gwivc4rQFGlc3x7hYGh30r8/kl8FLbw==",
      "license": "Apache-2.0",
      "dependencies": {
        "@dcloudio/uni-cli-shared": "3.0.0-5000720260410001"
      }
    },
    "node_modules/@dcloudio/uni-quickapp-webview": {
      "version": "3.0.0-5000720260410001",
      "resolved": "https://registry.npmjs.org/@dcloudio/uni-quickapp-webview/-/uni-quickapp-webview-3.0.0-5000720260410001.tgz",
      "integrity": "sha512-X1mIwQgqv/KMYfcbw6BnXA9+h7pg1CDyj9F0v6fhgAcHmKZOczYh+VgVPiQ5RTBNj/KkGO/ea+enP79TFdCqtA==",
      "license": "Apache-2.0",
      "dependencies": {
        "@dcloudio/uni-cli-shared": "3.0.0-5000720260410001",
        "@dcloudio/uni-mp-vite": "3.0.0-5000720260410001",
        "@dcloudio/uni-mp-vue": "3.0.0-5000720260410001",
        "@dcloudio/uni-shared": "3.0.0-5000720260410001",
        "@vue/shared": "3.4.21"
      }
    },
    "node_modules/@dcloudio/uni-shared": {
      "version": "3.0.0-5000720260410001",
      "resolved": "https://registry.npmjs.org/@dcloudio/uni-shared/-/uni-shared-3.0.0-5000720260410001.tgz",
      "integrity": "sha512-B51i5GohH5D6v4OgXQToESihIpmssBNSQAbiKdV10uQ/6u5TtyQkTtUKwdhjm+SY5xQSluV9sBpnB+LvYyWakg==",
      "license": "Apache-2.0",
      "dependencies": {
        "@vue/shared": "3.4.21"
      }
    },
    "node_modules/@dcloudio/uni-stacktracey": {
      "version": "3.0.0-5000720260410001",
      "resolved": "https://registry.npmjs.org/@dcloudio/uni-stacktracey/-/uni-stacktracey-3.0.0-5000720260410001.tgz",
      "integrity": "sha512-/PDrXioKUsxXUSIwcnjnJhbr7URyDfGH2skImF7KrDn3dYG+dN0WJQqPsWVwr5zr0p3kpr+uO42ce2fsT6yv5Q==",
      "dev": true,
      "license": "Apache-2.0"
    },
    "node_modules/@dcloudio/uni-stat": {
      "version": "3.0.0-5000720260410001",
      "resolved": "https://registry.npmjs.org/@dcloudio/uni-stat/-/uni-stat-3.0.0-5000720260410001.tgz",
      "integrity": "sha512-6jDWTufAjGsdOPcfnSsB2ylaBosFj5+UqZch56NgDrHejasdJ5Pb+peqOP7zn/mlRCiYk0dWTi6ZhcUFJ8ZKDA==",
      "license": "Apache-2.0",
      "dependencies": {
        "@dcloudio/uni-cli-shared": "3.0.0-5000720260410001",
        "@dcloudio/uni-shared": "3.0.0-5000720260410001",
        "debug": "4.3.7"
      }
    },
    "node_modules/@dcloudio/vite-plugin-uni": {
      "version": "3.0.0-5000720260410001",
      "resolved": "https://registry.npmjs.org/@dcloudio/vite-plugin-uni/-/vite-plugin-uni-3.0.0-5000720260410001.tgz",
      "integrity": "sha512-wVNF7QRImQkAOwtRLrZ80z5KnF+F7P+cBV/BH16aA/Upj1xyFcihRF4QENt9PyhO0syg5n+Dl4Xhm7Oz+6mjPA==",
      "dev": true,
      "license": "Apache-2.0",
      "dependencies": {
        "@babel/core": "7.25.2",
        "@babel/plugin-syntax-import-meta": "^7.10.4",
        "@babel/plugin-transform-typescript": "^7.23.3",
        "@dcloudio/uni-cli-shared": "3.0.0-5000720260410001",
        "@dcloudio/uni-nvue-styler": "3.0.0-5000720260410001",
        "@dcloudio/uni-shared": "3.0.0-5000720260410001",
        "@rollup/pluginutils": "5.1.0",
        "@vitejs/plugin-legacy": "5.3.2",
        "@vitejs/plugin-vue": "5.2.4",
        "@vitejs/plugin-vue-jsx": "3.1.0",
        "@vue/compiler-core": "3.4.21",
        "@vue/compiler-dom": "3.4.21",
        "@vue/compiler-sfc": "3.4.21",
        "@vue/shared": "3.4.21",
        "cac": "6.7.9",
        "debug": "4.3.7",
        "estree-walker": "2.0.2",
        "express": "4.20.0",
        "fast-glob": "3.3.3",
        "fs-extra": "10.1.0",
        "hash-sum": "2.0.0",
        "jsonc-parser": "3.3.1",
        "magic-string": "0.30.11",
        "picocolors": "1.1.0",
        "terser": "^5.4.0",
        "unplugin-auto-import": "19.1.0"
      },
      "bin": {
        "uni": "bin/uni.js"
      },
      "engines": {
        "node": "^14.18.0 || >=16.0.0"
      },
      "peerDependencies": {
        "vite": "5.2.8"
      }
    },
    "node_modules/@dcloudio/vite-plugin-uni/node_modules/picomatch": {
      "version": "4.0.4",
      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
      "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=12"
      },
      "funding": {
        "url": "https://github.com/sponsors/jonschlinkert"
      }
    },
    "node_modules/@dcloudio/vite-plugin-uni/node_modules/unplugin-auto-import": {
      "version": "19.1.0",
      "resolved": "https://registry.npmjs.org/unplugin-auto-import/-/unplugin-auto-import-19.1.0.tgz",
      "integrity": "sha512-B+TGBEBHqY9aR+7YfShfLujETOHstzpV+yaqgy5PkfV0QG7Py+TYMX7vJ9W4SrysHR+UzR+gzcx/nuZjmPeclA==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "local-pkg": "^1.0.0",
        "magic-string": "^0.30.17",
        "picomatch": "^4.0.2",
        "unimport": "^4.1.1",
        "unplugin": "^2.2.0",
        "unplugin-utils": "^0.2.4"
      },
      "engines": {
        "node": ">=14"
      },
      "funding": {
        "url": "https://github.com/sponsors/antfu"
      },
      "peerDependencies": {
        "@nuxt/kit": "^3.2.2",
        "@vueuse/core": "*"
      },
      "peerDependenciesMeta": {
        "@nuxt/kit": {
          "optional": true
        },
        "@vueuse/core": {
          "optional": true
        }
      }
    },
    "node_modules/@dcloudio/vite-plugin-uni/node_modules/unplugin-auto-import/node_modules/magic-string": {
      "version": "0.30.21",
      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
      "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@jridgewell/sourcemap-codec": "^1.5.5"
      }
    },
    "node_modules/@esbuild/aix-ppc64": {
      "version": "0.20.2",
      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz",
      "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==",
      "cpu": [
        "ppc64"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "aix"
      ],
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/@esbuild/android-arm": {
      "version": "0.20.2",
      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz",
      "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==",
      "cpu": [
        "arm"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "android"
      ],
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/@esbuild/android-arm64": {
      "version": "0.20.2",
      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz",
      "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==",
      "cpu": [
        "arm64"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "android"
      ],
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/@esbuild/android-x64": {
      "version": "0.20.2",
      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz",
      "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==",
      "cpu": [
        "x64"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "android"
      ],
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/@esbuild/darwin-arm64": {
      "version": "0.20.2",
      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz",
      "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==",
      "cpu": [
        "arm64"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "darwin"
      ],
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/@esbuild/darwin-x64": {
      "version": "0.20.2",
      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz",
      "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==",
      "cpu": [
        "x64"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "darwin"
      ],
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/@esbuild/freebsd-arm64": {
      "version": "0.20.2",
      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz",
      "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==",
      "cpu": [
        "arm64"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "freebsd"
      ],
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/@esbuild/freebsd-x64": {
      "version": "0.20.2",
      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz",
      "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==",
      "cpu": [
        "x64"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "freebsd"
      ],
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/@esbuild/linux-arm": {
      "version": "0.20.2",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz",
      "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==",
      "cpu": [
        "arm"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/@esbuild/linux-arm64": {
      "version": "0.20.2",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz",
      "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==",
      "cpu": [
        "arm64"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/@esbuild/linux-ia32": {
      "version": "0.20.2",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz",
      "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==",
      "cpu": [
        "ia32"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/@esbuild/linux-loong64": {
      "version": "0.20.2",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz",
      "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==",
      "cpu": [
        "loong64"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/@esbuild/linux-mips64el": {
      "version": "0.20.2",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz",
      "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==",
      "cpu": [
        "mips64el"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/@esbuild/linux-ppc64": {
      "version": "0.20.2",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz",
      "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==",
      "cpu": [
        "ppc64"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/@esbuild/linux-riscv64": {
      "version": "0.20.2",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz",
      "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==",
      "cpu": [
        "riscv64"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/@esbuild/linux-s390x": {
      "version": "0.20.2",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz",
      "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==",
      "cpu": [
        "s390x"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/@esbuild/linux-x64": {
      "version": "0.20.2",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz",
      "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==",
      "cpu": [
        "x64"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/@esbuild/netbsd-x64": {
      "version": "0.20.2",
      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz",
      "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==",
      "cpu": [
        "x64"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "netbsd"
      ],
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/@esbuild/openbsd-x64": {
      "version": "0.20.2",
      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz",
      "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==",
      "cpu": [
        "x64"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "openbsd"
      ],
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/@esbuild/sunos-x64": {
      "version": "0.20.2",
      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz",
      "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==",
      "cpu": [
        "x64"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "sunos"
      ],
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/@esbuild/win32-arm64": {
      "version": "0.20.2",
      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz",
      "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==",
      "cpu": [
        "arm64"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "win32"
      ],
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/@esbuild/win32-ia32": {
      "version": "0.20.2",
      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz",
      "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==",
      "cpu": [
        "ia32"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "win32"
      ],
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/@esbuild/win32-x64": {
      "version": "0.20.2",
      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz",
      "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==",
      "cpu": [
        "x64"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "win32"
      ],
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/@intlify/core-base": {
      "version": "9.1.9",
      "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.1.9.tgz",
      "integrity": "sha512-x5T0p/Ja0S8hs5xs+ImKyYckVkL4CzcEXykVYYV6rcbXxJTe2o58IquSqX9bdncVKbRZP7GlBU1EcRaQEEJ+vw==",
      "license": "MIT",
      "dependencies": {
        "@intlify/devtools-if": "9.1.9",
        "@intlify/message-compiler": "9.1.9",
        "@intlify/message-resolver": "9.1.9",
        "@intlify/runtime": "9.1.9",
        "@intlify/shared": "9.1.9",
        "@intlify/vue-devtools": "9.1.9"
      },
      "engines": {
        "node": ">= 10"
      }
    },
    "node_modules/@intlify/devtools-if": {
      "version": "9.1.9",
      "resolved": "https://registry.npmjs.org/@intlify/devtools-if/-/devtools-if-9.1.9.tgz",
      "integrity": "sha512-oKSMKjttG3Ut/1UGEZjSdghuP3fwA15zpDPcjkf/1FjlOIm6uIBGMNS5jXzsZy593u+P/YcnrZD6cD3IVFz9vQ==",
      "license": "MIT",
      "dependencies": {
        "@intlify/shared": "9.1.9"
      },
      "engines": {
        "node": ">= 10"
      }
    },
    "node_modules/@intlify/message-compiler": {
      "version": "9.1.9",
      "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.1.9.tgz",
      "integrity": "sha512-6YgCMF46Xd0IH2hMRLCssZI3gFG4aywidoWQ3QP4RGYQXQYYfFC54DxhSgfIPpVoPLQ+4AD29eoYmhiHZ+qLFQ==",
      "license": "MIT",
      "dependencies": {
        "@intlify/message-resolver": "9.1.9",
        "@intlify/shared": "9.1.9",
        "source-map": "0.6.1"
      },
      "engines": {
        "node": ">= 10"
      }
    },
    "node_modules/@intlify/message-resolver": {
      "version": "9.1.9",
      "resolved": "https://registry.npmjs.org/@intlify/message-resolver/-/message-resolver-9.1.9.tgz",
      "integrity": "sha512-Lx/DBpigeK0sz2BBbzv5mu9/dAlt98HxwbG7xLawC3O2xMF9MNWU5FtOziwYG6TDIjNq0O/3ZbOJAxwITIWXEA==",
      "license": "MIT",
      "engines": {
        "node": ">= 10"
      }
    },
    "node_modules/@intlify/runtime": {
      "version": "9.1.9",
      "resolved": "https://registry.npmjs.org/@intlify/runtime/-/runtime-9.1.9.tgz",
      "integrity": "sha512-XgPw8+UlHCiie3fI41HPVa/VDJb3/aSH7bLhY1hJvlvNV713PFtb4p4Jo+rlE0gAoMsMCGcsiT982fImolSltg==",
      "license": "MIT",
      "dependencies": {
        "@intlify/message-compiler": "9.1.9",
        "@intlify/message-resolver": "9.1.9",
        "@intlify/shared": "9.1.9"
      },
      "engines": {
        "node": ">= 10"
      }
    },
    "node_modules/@intlify/shared": {
      "version": "9.1.9",
      "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.1.9.tgz",
      "integrity": "sha512-xKGM1d0EAxdDFCWedcYXOm6V5Pfw/TMudd6/qCdEb4tv0hk9EKeg7lwQF1azE0dP2phvx0yXxrt7UQK+IZjNdw==",
      "license": "MIT",
      "engines": {
        "node": ">= 10"
      }
    },
    "node_modules/@intlify/vue-devtools": {
      "version": "9.1.9",
      "resolved": "https://registry.npmjs.org/@intlify/vue-devtools/-/vue-devtools-9.1.9.tgz",
      "integrity": "sha512-YPehH9uL4vZcGXky4Ev5qQIITnHKIvsD2GKGXgqf+05osMUI6WSEQHaN9USRa318Rs8RyyPCiDfmA0hRu3k7og==",
      "license": "MIT",
      "dependencies": {
        "@intlify/message-resolver": "9.1.9",
        "@intlify/runtime": "9.1.9",
        "@intlify/shared": "9.1.9"
      },
      "engines": {
        "node": ">= 10"
      }
    },
    "node_modules/@istanbuljs/load-nyc-config": {
      "version": "1.1.0",
      "resol   ved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
      "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==",
      "dev": true,
      "license": "ISC",
      "peer": true,
      "dependencies": {
        "camelcase": "^5.3.1",
        "find-up": "^4.1.0",
        "get-package-type": "^0.1.0",
        "js-yaml": "^3.13.1",
        "resolve-from": "^5.0.0"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/@istanbuljs/schema": {
      "version": "0.1.6",
      "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz",
      "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/@jest/console": {
      "version": "27.5.1",
      "resolved": "https://registry.npmjs.org/@jest/console/-/console-27.5.1.tgz",
      "integrity": "sha512-kZ/tNpS3NXn0mlXXXPNuDZnb4c0oZ20r4K5eemM2k30ZC3G0T02nXUvyhf5YdbXWHPEJLc9qGLxEZ216MdL+Zg==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "@jest/types": "^27.5.1",
        "@types/node": "*",
        "chalk": "^4.0.0",
        "jest-message-util": "^27.5.1",
        "jest-util": "^27.5.1",
        "slash": "^3.0.0"
      },
      "engines": {
        "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
      }
    },
    "node_modules/@jest/console/node_modules/ansi-styles": {
      "version": "4.3.0",
      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "color-convert": "^2.0.1"
      },
      "engines": {
        "node": ">=8"
      },
      "funding": {
        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
      }
    },
    "node_modules/@jest/console/node_modules/chalk": {
      "version": "4.1.2",
      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "ansi-styles": "^4.1.0",
        "supports-color": "^7.1.0"
      },
      "engines": {
        "node": ">=10"
      },
      "funding": {
        "url": "https://github.com/chalk/chalk?sponsor=1"
      }
    },
    "node_modules/@jest/console/node_modules/color-convert": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "color-name": "~1.1.4"
      },
      "engines": {
        "node": ">=7.0.0"
      }
    },
    "node_modules/@jest/console/node_modules/color-name": {
      "version": "1.1.4",
      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
      "dev": true,
      "license": "MIT",
      "peer": true
    },
    "node_modules/@jest/console/node_modules/has-flag": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/@jest/console/node_modules/supports-color": {
      "version": "7.2.0",
      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "has-flag": "^4.0.0"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/@jest/core": {
      "version": "27.5.1",
      "resolved": "https://registry.npmjs.org/@jest/core/-/core-27.5.1.tgz",
      "integrity": "sha512-AK6/UTrvQD0Cd24NSqmIA6rKsu0tKIxfiCducZvqxYdmMisOYAsdItspT+fQDQYARPf8XgjAFZi0ogW2agH5nQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "@jest/console": "^27.5.1",
        "@jest/reporters": "^27.5.1",
        "@jest/test-result": "^27.5.1",
        "@jest/transform": "^27.5.1",
        "@jest/types": "^27.5.1",
        "@types/node": "*",
        "ansi-escapes": "^4.2.1",
        "chalk": "^4.0.0",
        "emittery": "^0.8.1",
        "exit": "^0.1.2",
        "graceful-fs": "^4.2.9",
        "jest-changed-files": "^27.5.1",
        "jest-config": "^27.5.1",
        "jest-haste-map": "^27.5.1",
        "jest-message-util": "^27.5.1",
        "jest-regex-util": "^27.5.1",
        "jest-resolve": "^27.5.1",
        "jest-resolve-dependencies": "^27.5.1",
        "jest-runner": "^27.5.1",
        "jest-runtime": "^27.5.1",
        "jest-snapshot": "^27.5.1",
        "jest-util": "^27.5.1",
        "jest-validate": "^27.5.1",
        "jest-watcher": "^27.5.1",
        "micromatch": "^4.0.4",
        "rimraf": "^3.0.0",
        "slash": "^3.0.0",
        "strip-ansi": "^6.0.0"
      },
      "engines": {
        "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
      },
      "peerDependencies": {
        "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0"
      },
      "peerDependenciesMeta": {
        "node-notifier": {
          "optional": true
        }
      }
    },
    "node_modules/@jest/core/node_modules/ansi-styles": {
      "version": "4.3.0",
      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "color-convert": "^2.0.1"
      },
      "engines": {
        "node": ">=8"
      },
      "funding": {
        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
      }
    },
    "node_modules/@jest/core/node_modules/chalk": {
      "version": "4.1.2",
      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "ansi-styles": "^4.1.0",
        "supports-color": "^7.1.0"
      },
      "engines": {
        "node": ">=10"
      },
      "funding": {
        "url": "https://github.com/chalk/chalk?sponsor=1"
      }
    },
    "node_modules/@jest/core/node_modules/color-convert": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "color-name": "~1.1.4"
      },
      "engines": {
        "node": ">=7.0.0"
      }
    },
    "node_modules/@jest/core/node_modules/color-name": {
      "version": "1.1.4",
      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
      "dev": true,
      "license": "MIT",
      "peer": true
    },
    "node_modules/@jest/core/node_modules/has-flag": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/@jest/core/node_modules/supports-color": {
      "version": "7.2.0",
      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "has-flag": "^4.0.0"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/@jest/environment": {
      "version": "27.5.1",
      "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-27.5.1.tgz",
      "integrity": "sha512-/WQjhPJe3/ghaol/4Bq480JKXV/Rfw8nQdN7f41fM8VDHLcxKXou6QyXAh3EFr9/bVG3x74z1NWDkP87EiY8gA==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "@jest/fake-timers": "^27.5.1",
        "@jest/types": "^27.5.1",
        "@types/node": "*",
        "jest-mock": "^27.5.1"
      },
      "engines": {
        "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
      }
    },
    "node_modules/@jest/fake-timers": {
      "version": "27.5.1",
      "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-27.5.1.tgz",
      "integrity": "sha512-/aPowoolwa07k7/oM3aASneNeBGCmGQsc3ugN4u6s4C/+s5M64MFo/+djTdiwcbQlRfFElGuDXWzaWj6QgKObQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "@jest/types": "^27.5.1",
        "@sinonjs/fake-timers": "^8.0.1",
        "@types/node": "*",
        "jest-message-util": "^27.5.1",
        "jest-mock": "^27.5.1",
        "jest-util": "^27.5.1"
      },
      "engines": {
        "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
      }
    },
    "node_modules/@jest/globals": {
      "version": "27.5.1",
      "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-27.5.1.tgz",
      "integrity": "sha512-ZEJNB41OBQQgGzgyInAv0UUfDDj3upmHydjieSxFvTRuZElrx7tXg/uVQ5hYVEwiXs3+aMsAeEc9X7xiSKCm4Q==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "@jest/environment": "^27.5.1",
        "@jest/types": "^27.5.1",
        "expect": "^27.5.1"
      },
      "engines": {
        "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
      }
    },
    "node_modules/@jest/reporters": {
      "version": "27.5.1",
      "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-27.5.1.tgz",
      "integrity": "sha512-cPXh9hWIlVJMQkVk84aIvXuBB4uQQmFqZiacloFuGiP3ah1sbCxCosidXFDfqG8+6fO1oR2dTJTlsOy4VFmUfw==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "@bcoe/v8-coverage": "^0.2.3",
        "@jest/console": "^27.5.1",
        "@jest/test-result": "^27.5.1",
        "@jest/transform": "^27.5.1",
        "@jest/types": "^27.5.1",
        "@types/node": "*",
        "chalk": "^4.0.0",
        "collect-v8-coverage": "^1.0.0",
        "exit": "^0.1.2",
        "glob": "^7.1.2",
        "graceful-fs": "^4.2.9",
        "istanbul-lib-coverage": "^3.0.0",
        "istanbul-lib-instrument": "^5.1.0",
        "istanbul-lib-report": "^3.0.0",
        "istanbul-lib-source-maps": "^4.0.0",
        "istanbul-reports": "^3.1.3",
        "jest-haste-map": "^27.5.1",
        "jest-resolve": "^27.5.1",
        "jest-util": "^27.5.1",
        "jest-worker": "^27.5.1",
        "slash": "^3.0.0",
        "source-map": "^0.6.0",
        "string-length": "^4.0.1",
        "terminal-link": "^2.0.0",
        "v8-to-istanbul": "^8.1.0"
      },
      "engines": {
        "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
      },
      "peerDependencies": {
        "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0"
      },
      "peerDependenciesMeta": {
        "node-notifier": {
          "optional": true
        }
      }
    },
    "node_modules/@jest/reporters/node_modules/ansi-styles": {
      "version": "4.3.0",
      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "color-convert": "^2.0.1"
      },
      "engines": {
        "node": ">=8"
      },
      "funding": {
        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
      }
    },
    "node_modules/@jest/reporters/node_modules/chalk": {
      "version": "4.1.2",
      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "ansi-styles": "^4.1.0",
        "supports-color": "^7.1.0"
      },
      "engines": {
        "node": ">=10"
      },
      "funding": {
        "url": "https://github.com/chalk/chalk?sponsor=1"
      }
    },
    "node_modules/@jest/reporters/node_modules/color-convert": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "color-name": "~1.1.4"
      },
      "engines": {
        "node": ">=7.0.0"
      }
    },
    "node_modules/@jest/reporters/node_modules/color-name": {
      "version": "1.1.4",
      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
      "dev": true,
      "license": "MIT",
      "peer": true
    },
    "node_modules/@jest/reporters/node_modules/has-flag": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/@jest/reporters/node_modules/supports-color": {
      "version": "7.2.0",
      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "has-flag": "^4.0.0"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/@jest/source-map": {
      "version": "27.5.1",
      "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-27.5.1.tgz",
      "integrity": "sha512-y9NIHUYF3PJRlHk98NdC/N1gl88BL08aQQgu4k4ZopQkCw9t9cV8mtl3TV8b/YCB8XaVTFrmUTAJvjsntDireg==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "callsites": "^3.0.0",
        "graceful-fs": "^4.2.9",
        "source-map": "^0.6.0"
      },
      "engines": {
        "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
      }
    },
    "node_modules/@jest/test-result": {
      "version": "27.5.1",
      "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-27.5.1.tgz",
      "integrity": "sha512-EW35l2RYFUcUQxFJz5Cv5MTOxlJIQs4I7gxzi2zVU7PJhOwfYq1MdC5nhSmYjX1gmMmLPvB3sIaC+BkcHRBfag==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "@jest/console": "^27.5.1",
        "@jest/types": "^27.5.1",
        "@types/istanbul-lib-coverage": "^2.0.0",
        "collect-v8-coverage": "^1.0.0"
      },
      "engines": {
        "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
      }
    },
    "node_modules/@jest/test-sequencer": {
      "version": "27.5.1",
      "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-27.5.1.tgz",
      "integrity": "sha512-LCheJF7WB2+9JuCS7VB/EmGIdQuhtqjRNI9A43idHv3E4KltCTsPsLxvdaubFHSYwY/fNjMWjl6vNRhDiN7vpQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "@jest/test-result": "^27.5.1",
        "graceful-fs": "^4.2.9",
        "jest-haste-map": "^27.5.1",
        "jest-runtime": "^27.5.1"
      },
      "engines": {
        "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
      }
    },
    "node_modules/@jest/transform": {
      "version": "27.5.1",
      "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-27.5.1.tgz",
      "integrity": "sha512-ipON6WtYgl/1329g5AIJVbUuEh0wZVbdpGwC99Jw4LwuoBNS95MVphU6zOeD9pDkon+LLbFL7lOQRapbB8SCHw==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "@babel/core": "^7.1.0",
        "@jest/types": "^27.5.1",
        "babel-plugin-istanbul": "^6.1.1",
        "chalk": "^4.0.0",
        "convert-source-map": "^1.4.0",
        "fast-json-stable-stringify": "^2.0.0",
        "graceful-fs": "^4.2.9",
        "jest-haste-map": "^27.5.1",
        "jest-regex-util": "^27.5.1",
        "jest-util": "^27.5.1",
        "micromatch": "^4.0.4",
        "pirates": "^4.0.4",
        "slash": "^3.0.0",
        "source-map": "^0.6.1",
        "write-file-atomic": "^3.0.0"
      },
      "engines": {
        "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
      }
    },
    "node_modules/@jest/transform/node_modules/ansi-styles": {
      "version": "4.3.0",
      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "color-convert": "^2.0.1"
      },
      "engines": {
        "node": ">=8"
      },
      "funding": {
        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
      }
    },
    "node_modules/@jest/transform/node_modules/chalk": {
      "version": "4.1.2",
      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "ansi-styles": "^4.1.0",
        "supports-color": "^7.1.0"
      },
      "engines": {
        "node": ">=10"
      },
      "funding": {
        "url": "https://github.com/chalk/chalk?sponsor=1"
      }
    },
    "node_modules/@jest/transform/node_modules/color-convert": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "color-name": "~1.1.4"
      },
      "engines": {
        "node": ">=7.0.0"
      }
    },
    "node_modules/@jest/transform/node_modules/color-name": {
      "version": "1.1.4",
      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
      "dev": true,
      "license": "MIT",
      "peer": true
    },
    "node_modules/@jest/transform/node_modules/convert-source-map": {
      "version": "1.9.0",
      "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
      "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
      "dev": true,
      "license": "MIT",
      "peer": true
    },
    "node_modules/@jest/transform/node_modules/has-flag": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/@jest/transform/node_modules/supports-color": {
      "version": "7.2.0",
      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "has-flag": "^4.0.0"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/@jest/types": {
      "version": "27.5.1",
      "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz",
      "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "@types/istanbul-lib-coverage": "^2.0.0",
        "@types/istanbul-reports": "^3.0.0",
        "@types/node": "*",
        "@types/yargs": "^16.0.0",
        "chalk": "^4.0.0"
      },
      "engines": {
        "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
      }
    },
    "node_modules/@jest/types/node_modules/ansi-styles": {
      "version": "4.3.0",
      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "color-convert": "^2.0.1"
      },
      "engines": {
        "node": ">=8"
      },
      "funding": {
        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
      }
    },
    "node_modules/@jest/types/node_modules/chalk": {
      "version": "4.1.2",
      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "ansi-styles": "^4.1.0",
        "supports-color": "^7.1.0"
      },
      "engines": {
        "node": ">=10"
      },
      "funding": {
        "url": "https://github.com/chalk/chalk?sponsor=1"
      }
    },
    "node_modules/@jest/types/node_modules/color-convert": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "color-name": "~1.1.4"
      },
      "engines": {
        "node": ">=7.0.0"
      }
    },
    "node_modules/@jest/types/node_modules/color-name": {
      "version": "1.1.4",
      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
      "dev": true,
      "license": "MIT",
      "peer": true
    },
    "node_modules/@jest/types/node_modules/has-flag": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/@jest/types/node_modules/supports-color": {
      "version": "7.2.0",
      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "has-flag": "^4.0.0"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/@jimp/bmp": {
      "version": "0.10.3",
      "resolved": "https://registry.npmjs.org/@jimp/bmp/-/bmp-0.10.3.tgz",
      "integrity": "sha512-keMOc5woiDmONXsB/6aXLR4Z5Q+v8lFq3EY2rcj2FmstbDMhRuGbmcBxlEgOqfRjwvtf/wOtJ3Of37oAWtVfLg==",
      "license": "MIT",
      "dependencies": {
        "@babel/runtime": "^7.7.2",
        "@jimp/utils": "^0.10.3",
        "bmp-js": "^0.1.0",
        "core-js": "^3.4.1"
      },
      "peerDependencies": {
        "@jimp/custom": ">=0.3.5"
      }
    },
    "node_modules/@jimp/core": {
      "version": "0.10.3",
      "resolved": "https://registry.npmjs.org/@jimp/core/-/core-0.10.3.tgz",
      "integrity": "sha512-Gd5IpL3U2bFIO57Fh/OA3HCpWm4uW/pU01E75rI03BXfTdz3T+J7TwvyG1XaqsQ7/DSlS99GXtLQPlfFIe28UA==",
      "license": "MIT",
      "dependencies": {
        "@babel/runtime": "^7.7.2",
        "@jimp/utils": "^0.10.3",
        "any-base": "^1.1.0",
        "buffer": "^5.2.0",
        "core-js": "^3.4.1",
        "exif-parser": "^0.1.12",
        "file-type": "^9.0.0",
        "load-bmfont": "^1.3.1",
        "mkdirp": "^0.5.1",
        "phin": "^2.9.1",
        "pixelmatch": "^4.0.2",
        "tinycolor2": "^1.4.1"
      }
    },
    "node_modules/@jimp/custom": {
      "version": "0.10.3",
      "resolved": "https://registry.npmjs.org/@jimp/custom/-/custom-0.10.3.tgz",
      "integrity": "sha512-nZmSI+jwTi5IRyNLbKSXQovoeqsw+D0Jn0SxW08wYQvdkiWA8bTlDQFgQ7HVwCAKBm8oKkDB/ZEo9qvHJ+1gAQ==",
      "license": "MIT",
      "dependencies": {
        "@babel/runtime": "^7.7.2",
        "@jimp/core": "^0.10.3",
        "core-js": "^3.4.1"
      }
    },
    "node_modules/@jimp/gif": {
      "version": "0.10.3",
      "resolved": "https://registry.npmjs.org/@jimp/gif/-/gif-0.10.3.tgz",
      "integrity": "sha512-vjlRodSfz1CrUvvrnUuD/DsLK1GHB/yDZXHthVdZu23zYJIW7/WrIiD1IgQ5wOMV7NocfrvPn2iqUfBP81/WWA==",
      "license": "MIT",
      "dependencies": {
        "@babel/runtime": "^7.7.2",
        "@jimp/utils": "^0.10.3",
        "core-js": "^3.4.1",
        "omggif": "^1.0.9"
      },
      "peerDependencies": {
        "@jimp/custom": ">=0.3.5"
      }
    },
    "node_modules/@jimp/jpeg": {
      "version": "0.10.3",
      "resolved": "https://registry.npmjs.org/@jimp/jpeg/-/jpeg-0.10.3.tgz",
      "integrity": "sha512-AAANwgUZOt6f6P7LZxY9lyJ9xclqutYJlsxt3JbriXUGJgrrFAIkcKcqv1nObgmQASSAQKYaMV9KdHjMlWFKlQ==",
      "license": "MIT",
      "dependencies": {
        "@babel/runtime": "^7.7.2",
        "@jimp/utils": "^0.10.3",
        "core-js": "^3.4.1",
        "jpeg-js": "^0.3.4"
      },
      "peerDependencies": {
        "@jimp/custom": ">=0.3.5"
      }
    },
    "node_modules/@jimp/plugin-blit": {
      "version": "0.10.3",
      "resolved": "https://registry.npmjs.org/@jimp/plugin-blit/-/plugin-blit-0.10.3.tgz",
      "integrity": "sha512-5zlKlCfx4JWw9qUVC7GI4DzXyxDWyFvgZLaoGFoT00mlXlN75SarlDwc9iZ/2e2kp4bJWxz3cGgG4G/WXrbg3Q==",
      "license": "MIT",
      "dependencies": {
        "@babel/runtime": "^7.7.2",
        "@jimp/utils": "^0.10.3",
        "core-js": "^3.4.1"
      },
      "peerDependencies": {
        "@jimp/custom": ">=0.3.5"
      }
    },
    "node_modules/@jimp/plugin-blur": {
      "version": "0.10.3",
      "resolved": "https://registry.npmjs.org/@jimp/plugin-blur/-/plugin-blur-0.10.3.tgz",
      "integrity": "sha512-cTOK3rjh1Yjh23jSfA6EHCHjsPJDEGLC8K2y9gM7dnTUK1y9NNmkFS23uHpyjgsWFIoH9oRh2SpEs3INjCpZhQ==",
      "license": "MIT",
      "dependencies": {
        "@babel/runtime": "^7.7.2",
        "@jimp/utils": "^0.10.3",
        "core-js": "^3.4.1"
      },
      "peerDependencies": {
        "@jimp/custom": ">=0.3.5"
      }
    },
    "node_modules/@jimp/plugin-circle": {
      "version": "0.10.3",
      "resolved": "https://registry.npmjs.org/@jimp/plugin-circle/-/plugin-circle-0.10.3.tgz",
      "integrity": "sha512-51GAPIVelqAcfuUpaM5JWJ0iWl4vEjNXB7p4P7SX5udugK5bxXUjO6KA2qgWmdpHuCKtoNgkzWU9fNSuYp7tCA==",
      "license": "MIT",
      "dependencies": {
        "@babel/runtime": "^7.7.2",
        "@jimp/utils": "^0.10.3",
        "core-js": "^3.4.1"
      },
      "peerDependencies": {
        "@jimp/custom": ">=0.3.5"
      }
    },
    "node_modules/@jimp/plugin-color": {
      "version": "0.10.3",
      "resolved": "https://registry.npmjs.org/@jimp/plugin-color/-/plugin-color-0.10.3.tgz",
      "integrity": "sha512-RgeHUElmlTH7vpI4WyQrz6u59spiKfVQbsG/XUzfWGamFSixa24ZDwX/yV/Ts+eNaz7pZeIuv533qmKPvw2ujg==",
      "license": "MIT",
      "dependencies": {
        "@babel/runtime": "^7.7.2",
        "@jimp/utils": "^0.10.3",
        "core-js": "^3.4.1",
        "tinycolor2": "^1.4.1"
      },
      "peerDependencies": {
        "@jimp/custom": ">=0.3.5"
      }
    },
    "node_modules/@jimp/plugin-contain": {
      "version": "0.10.3",
      "resolved": "https://registry.npmjs.org/@jimp/plugin-contain/-/plugin-contain-0.10.3.tgz",
      "integrity": "sha512-bYJKW9dqzcB0Ihc6u7jSyKa3juStzbLs2LFr6fu8TzA2WkMS/R8h+ddkiO36+F9ILTWHP0CIA3HFe5OdOGcigw==",
      "license": "MIT",
      "dependencies": {
        "@babel/runtime": "^7.7.2",
        "@jimp/utils": "^0.10.3",
        "core-js": "^3.4.1"
      },
      "peerDependencies": {
        "@jimp/custom": ">=0.3.5",
        "@jimp/plugin-blit": ">=0.3.5",
        "@jimp/plugin-resize": ">=0.3.5",
        "@jimp/plugin-scale": ">=0.3.5"
      }
    },
    "node_modules/@jimp/plugin-cover": {
      "version": "0.10.3",
      "resolved": "https://registry.npmjs.org/@jimp/plugin-cover/-/plugin-cover-0.10.3.tgz",
      "integrity": "sha512-pOxu0cM0BRPzdV468n4dMocJXoMbTnARDY/EpC3ZW15SpMuc/dr1KhWQHgoQX5kVW1Wt8zgqREAJJCQ5KuPKDA==",
      "license": "MIT",
      "dependencies": {
        "@babel/runtime": "^7.7.2",
        "@jimp/utils": "^0.10.3",
        "core-js": "^3.4.1"
      },
      "peerDependencies": {
        "@jimp/custom": ">=0.3.5",
        "@jimp/plugin-crop": ">=0.3.5",
        "@jimp/plugin-resize": ">=0.3.5",
        "@jimp/plugin-scale": ">=0.3.5"
      }
    },
    "node_modules/@jimp/plugin-crop": {
      "version": "0.10.3",
      "resolved": "https://registry.npmjs.org/@jimp/plugin-crop/-/plugin-crop-0.10.3.tgz",
      "integrity": "sha512-nB7HgOjjl9PgdHr076xZ3Sr6qHYzeBYBs9qvs3tfEEUeYMNnvzgCCGtUl6eMakazZFCMk3mhKmcB9zQuHFOvkg==",
      "license": "MIT",
      "dependencies": {
        "@babel/runtime": "^7.7.2",
        "@jimp/utils": "^0.10.3",
        "core-js": "^3.4.1"
      },
      "peerDependencies": {
        "@jimp/custom": ">=0.3.5"
      }
    },
    "node_modules/@jimp/plugin-displace": {
      "version": "0.10.3",
      "resolved": "https://registry.npmjs.org/@jimp/plugin-displace/-/plugin-displace-0.10.3.tgz",
      "integrity": "sha512-8t3fVKCH5IVqI4lewe4lFFjpxxr69SQCz5/tlpDLQZsrNScNJivHdQ09zljTrVTCSgeCqQJIKgH2Q7Sk/pAZ0w==",
      "license": "MIT",
      "dependencies": {
        "@babel/runtime": "^7.7.2",
        "@jimp/utils": "^0.10.3",
        "core-js": "^3.4.1"
      },
      "peerDependencies": {
        "@jimp/custom": ">=0.3.5"
      }
    },
    "node_modules/@jimp/plugin-dither": {
      "version": "0.10.3",
      "resolved": "https://registry.npmjs.org/@jimp/plugin-dither/-/plugin-dither-0.10.3.tgz",
      "integrity": "sha512-JCX/oNSnEg1kGQ8ffZ66bEgQOLCY3Rn+lrd6v1jjLy/mn9YVZTMsxLtGCXpiCDC2wG/KTmi4862ysmP9do9dAQ==",
      "license": "MIT",
      "dependencies": {
        "@babel/runtime": "^7.7.2",
        "@jimp/utils": "^0.10.3",
        "core-js": "^3.4.1"
      },
      "peerDependencies": {
        "@jimp/custom": ">=0.3.5"
      }
    },
    "node_modules/@jimp/plugin-fisheye": {
      "version": "0.10.3",
      "resolved": "https://registry.npmjs.org/@jimp/plugin-fisheye/-/plugin-fisheye-0.10.3.tgz",
      "integrity": "sha512-RRZb1wqe+xdocGcFtj2xHU7sF7xmEZmIa6BmrfSchjyA2b32TGPWKnP3qyj7p6LWEsXn+19hRYbjfyzyebPElQ==",
      "license": "MIT",
      "dependencies": {
        "@babel/runtime": "^7.7.2",
        "@jimp/utils": "^0.10.3",
        "core-js": "^3.4.1"
      },
      "peerDependencies": {
        "@jimp/custom": ">=0.3.5"
      }
    },
    "node_modules/@jimp/plugin-flip": {
      "version": "0.10.3",
      "resolved": "https://registry.npmjs.org/@jimp/plugin-flip/-/plugin-flip-0.10.3.tgz",
      "integrity": "sha512-0epbi8XEzp0wmSjoW9IB0iMu0yNF17aZOxLdURCN3Zr+8nWPs5VNIMqSVa1Y62GSyiMDpVpKF/ITiXre+EqrPg==",
      "license": "MIT",
      "dependencies": {
        "@babel/runtime": "^7.7.2",
        "@jimp/utils": "^0.10.3",
        "core-js": "^3.4.1"
      },
      "peerDependencies": {
        "@jimp/custom": ">=0.3.5",
        "@jimp/plugin-rotate": ">=0.3.5"
      }
    },
    "node_modules/@jimp/plugin-gaussian": {
      "version": "0.10.3",
      "resolved": "https://registry.npmjs.org/@jimp/plugin-gaussian/-/plugin-gaussian-0.10.3.tgz",
      "integrity": "sha512-25eHlFbHUDnMMGpgRBBeQ2AMI4wsqCg46sue0KklI+c2BaZ+dGXmJA5uT8RTOrt64/K9Wz5E+2n7eBnny4dfpQ==",
      "license": "MIT",
      "dependencies": {
        "@babel/runtime": "^7.7.2",
        "@jimp/utils": "^0.10.3",
        "core-js": "^3.4.1"
      },
      "peerDependencies": {
        "@jimp/custom": ">=0.3.5"
      }
    },
    "node_modules/@jimp/plugin-invert": {
      "version": "0.10.3",
      "resolved": "https://registry.npmjs.org/@jimp/plugin-invert/-/plugin-invert-0.10.3.tgz",
      "integrity": "sha512-effYSApWY/FbtlzqsKXlTLkgloKUiHBKjkQnqh5RL4oQxh/33j6aX+HFdDyQKtsXb8CMd4xd7wyiD2YYabTa0g==",
      "license": "MIT",
      "dependencies": {
        "@babel/runtime": "^7.7.2",
        "@jimp/utils": "^0.10.3",
        "core-js": "^3.4.1"
      },
      "peerDependencies": {
        "@jimp/custom": ">=0.3.5"
      }
    },
    "node_modules/@jimp/plugin-mask": {
      "version": "0.10.3",
      "resolved": "https://registry.npmjs.org/@jimp/plugin-mask/-/plugin-mask-0.10.3.tgz",
      "integrity": "sha512-twrg8q8TIhM9Z6Jcu9/5f+OCAPaECb0eKrrbbIajJqJ3bCUlj5zbfgIhiQIzjPJ6KjpnFPSqHQfHkU1Vvk/nVw==",
      "license": "MIT",
      "dependencies": {
        "@babel/runtime": "^7.7.2",
        "@jimp/utils": "^0.10.3",
        "core-js": "^3.4.1"
      },
      "peerDependencies": {
        "@jimp/custom": ">=0.3.5"
      }
    },
    "node_modules/@jimp/plugin-normalize": {
      "version": "0.10.3",
      "resolved": "https://registry.npmjs.org/@jimp/plugin-normalize/-/plugin-normalize-0.10.3.tgz",
      "integrity": "sha512-xkb5eZI/mMlbwKkDN79+1/t/+DBo8bBXZUMsT4gkFgMRKNRZ6NQPxlv1d3QpRzlocsl6UMxrHnhgnXdLAcgrXw==",
      "license": "MIT",
      "dependencies": {
        "@babel/runtime": "^7.7.2",
        "@jimp/utils": "^0.10.3",
        "core-js": "^3.4.1"
      },
      "peerDependencies": {
        "@jimp/custom": ">=0.3.5"
      }
    },
    "node_modules/@jimp/plugin-print": {
      "version": "0.10.3",
      "resolved": "https://registry.npmjs.org/@jimp/plugin-print/-/plugin-print-0.10.3.tgz",
      "integrity": "sha512-wjRiI6yjXsAgMe6kVjizP+RgleUCLkH256dskjoNvJzmzbEfO7xQw9g6M02VET+emnbY0CO83IkrGm2q43VRyg==",
      "license": "MIT",
      "dependencies": {
        "@babel/runtime": "^7.7.2",
        "@jimp/utils": "^0.10.3",
        "core-js": "^3.4.1",
        "load-bmfont": "^1.4.0"
      },
      "peerDependencies": {
        "@jimp/custom": ">=0.3.5",
        "@jimp/plugin-blit": ">=0.3.5"
      }
    },
    "node_modules/@jimp/plugin-resize": {
      "version": "0.10.3",
      "resolved": "https://registry.npmjs.org/@jimp/plugin-resize/-/plugin-resize-0.10.3.tgz",
      "integrity": "sha512-rf8YmEB1d7Sg+g4LpqF0Mp+dfXfb6JFJkwlAIWPUOR7lGsPWALavEwTW91c0etEdnp0+JB9AFpy6zqq7Lwkq6w==",
      "license": "MIT",
      "dependencies": {
        "@babel/runtime": "^7.7.2",
        "@jimp/utils": "^0.10.3",
        "core-js": "^3.4.1"
      },
      "peerDependencies": {
        "@jimp/custom": ">=0.3.5"
      }
    },
    "node_modules/@jimp/plugin-rotate": {
      "version": "0.10.3",
      "resolved": "https://registry.npmjs.org/@jimp/plugin-rotate/-/plugin-rotate-0.10.3.tgz",
      "integrity": "sha512-YXLlRjm18fkW9MOHUaVAxWjvgZM851ofOipytz5FyKp4KZWDLk+dZK1JNmVmK7MyVmAzZ5jsgSLhIgj+GgN0Eg==",
      "license": "MIT",
      "dependencies": {
        "@babel/runtime": "^7.7.2",
        "@jimp/utils": "^0.10.3",
        "core-js": "^3.4.1"
      },
      "peerDependencies": {
        "@jimp/custom": ">=0.3.5",
        "@jimp/plugin-blit": ">=0.3.5",
        "@jimp/plugin-crop": ">=0.3.5",
        "@jimp/plugin-resize": ">=0.3.5"
      }
    },
    "node_modules/@jimp/plugin-scale": {
      "version": "0.10.3",
      "resolved": "https://registry.npmjs.org/@jimp/plugin-scale/-/plugin-scale-0.10.3.tgz",
      "integrity": "sha512-5DXD7x7WVcX1gUgnlFXQa8F+Q3ThRYwJm+aesgrYvDOY+xzRoRSdQvhmdd4JEEue3lyX44DvBSgCIHPtGcEPaw==",
      "license": "MIT",
      "dependencies": {
        "@babel/runtime": "^7.7.2",
        "@jimp/utils": "^0.10.3",
        "core-js": "^3.4.1"
      },
      "peerDependencies": {
        "@jimp/custom": ">=0.3.5",
        "@jimp/plugin-resize": ">=0.3.5"
      }
    },
    "node_modules/@jimp/plugin-shadow": {
      "version": "0.10.3",
      "resolved": "https://registry.npmjs.org/@jimp/plugin-shadow/-/plugin-shadow-0.10.3.tgz",
      "integrity": "sha512-/nkFXpt2zVcdP4ETdkAUL0fSzyrC5ZFxdcphbYBodqD7fXNqChS/Un1eD4xCXWEpW8cnG9dixZgQgStjywH0Mg==",
      "license": "MIT",
      "dependencies": {
        "@babel/runtime": "^7.7.2",
        "@jimp/utils": "^0.10.3",
        "core-js": "^3.4.1"
      },
      "peerDependencies": {
        "@jimp/custom": ">=0.3.5",
        "@jimp/plugin-blur": ">=0.3.5",
        "@jimp/plugin-resize": ">=0.3.5"
      }
    },
    "node_modules/@jimp/plugin-threshold": {
      "version": "0.10.3",
      "resolved": "https://registry.npmjs.org/@jimp/plugin-threshold/-/plugin-threshold-0.10.3.tgz",
      "integrity": "sha512-Dzh0Yq2wXP2SOnxcbbiyA4LJ2luwrdf1MghNIt9H+NX7B+IWw/N8qA2GuSm9n4BPGSLluuhdAWJqHcTiREriVA==",
      "license": "MIT",
      "dependencies": {
        "@babel/runtime": "^7.7.2",
        "@jimp/utils": "^0.10.3",
        "core-js": "^3.4.1"
      },
      "peerDependencies": {
        "@jimp/custom": ">=0.3.5",
        "@jimp/plugin-color": ">=0.8.0",
        "@jimp/plugin-resize": ">=0.8.0"
      }
    },
    "node_modules/@jimp/plugins": {
      "version": "0.10.3",
      "resolved": "https://registry.npmjs.org/@jimp/plugins/-/plugins-0.10.3.tgz",
      "integrity": "sha512-jTT3/7hOScf0EIKiAXmxwayHhryhc1wWuIe3FrchjDjr9wgIGNN2a7XwCgPl3fML17DXK1x8EzDneCdh261bkw==",
      "license": "MIT",
      "dependencies": {
        "@babel/runtime": "^7.7.2",
        "@jimp/plugin-blit": "^0.10.3",
        "@jimp/plugin-blur": "^0.10.3",
        "@jimp/plugin-circle": "^0.10.3",
        "@jimp/plugin-color": "^0.10.3",
        "@jimp/plugin-contain": "^0.10.3",
        "@jimp/plugin-cover": "^0.10.3",
        "@jimp/plugin-crop": "^0.10.3",
        "@jimp/plugin-displace": "^0.10.3",
        "@jimp/plugin-dither": "^0.10.3",
        "@jimp/plugin-fisheye": "^0.10.3",
        "@jimp/plugin-flip": "^0.10.3",
        "@jimp/plugin-gaussian": "^0.10.3",
        "@jimp/plugin-invert": "^0.10.3",
        "@jimp/plugin-mask": "^0.10.3",
        "@jimp/plugin-normalize": "^0.10.3",
        "@jimp/plugin-print": "^0.10.3",
        "@jimp/plugin-resize": "^0.10.3",
        "@jimp/plugin-rotate": "^0.10.3",
        "@jimp/plugin-scale": "^0.10.3",
        "@jimp/plugin-shadow": "^0.10.3",
        "@jimp/plugin-threshold": "^0.10.3",
        "core-js": "^3.4.1",
        "timm": "^1.6.1"
      },
      "peerDependencies": {
        "@jimp/custom": ">=0.3.5"
      }
    },
    "node_modules/@jimp/png": {
      "version": "0.10.3",
      "resolved": "https://registry.npmjs.org/@jimp/png/-/png-0.10.3.tgz",
      "integrity": "sha512-YKqk/dkl+nGZxSYIDQrqhmaP8tC3IK8H7dFPnnzFVvbhDnyYunqBZZO3SaZUKTichClRw8k/CjBhbc+hifSGWg==",
      "license": "MIT",
      "dependencies": {
        "@babel/runtime": "^7.7.2",
        "@jimp/utils": "^0.10.3",
        "core-js": "^3.4.1",
        "pngjs": "^3.3.3"
      },
      "peerDependencies": {
        "@jimp/custom": ">=0.3.5"
      }
    },
    "node_modules/@jimp/tiff": {
      "version": "0.10.3",
      "resolved": "https://registry.npmjs.org/@jimp/tiff/-/tiff-0.10.3.tgz",
      "integrity": "sha512-7EsJzZ5Y/EtinkBGuwX3Bi4S+zgbKouxjt9c82VJTRJOQgLWsE/RHqcyRCOQBhHAZ9QexYmDz34medfLKdoX0g==",
      "license": "MIT",
      "dependencies": {
        "@babel/runtime": "^7.7.2",
        "core-js": "^3.4.1",
        "utif": "^2.0.1"
      },
      "peerDependencies": {
        "@jimp/custom": ">=0.3.5"
      }
    },
    "node_modules/@jimp/types": {
      "version": "0.10.3",
      "resolved": "https://registry.npmjs.org/@jimp/types/-/types-0.10.3.tgz",
      "integrity": "sha512-XGmBakiHZqseSWr/puGN+CHzx0IKBSpsKlmEmsNV96HKDiP6eu8NSnwdGCEq2mmIHe0JNcg1hqg59hpwtQ7Tiw==",
      "license": "MIT",
      "dependencies": {
        "@babel/runtime": "^7.7.2",
        "@jimp/bmp": "^0.10.3",
        "@jimp/gif": "^0.10.3",
        "@jimp/jpeg": "^0.10.3",
        "@jimp/png": "^0.10.3",
        "@jimp/tiff": "^0.10.3",
        "core-js": "^3.4.1",
        "timm": "^1.6.1"
      },
      "peerDependencies": {
        "@jimp/custom": ">=0.3.5"
      }
    },
    "node_modules/@jimp/utils": {
      "version": "0.10.3",
      "resolved": "https://registry.npmjs.org/@jimp/utils/-/utils-0.10.3.tgz",
      "integrity": "sha512-VcSlQhkil4ReYmg1KkN+WqHyYfZ2XfZxDsKAHSfST1GEz/RQHxKZbX+KhFKtKflnL0F4e6DlNQj3vznMNXCR2w==",
      "license": "MIT",
      "dependencies": {
        "@babel/runtime": "^7.7.2",
        "core-js": "^3.4.1",
        "regenerator-runtime": "^0.13.3"
      }
    },
    "node_modules/@jimp/utils/node_modules/regenerator-runtime": {
      "version": "0.13.11",
      "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
      "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
      "license": "MIT"
    },
    "node_modules/@jridgewell/gen-mapping": {
      "version": "0.3.13",
      "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
      "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
      "license": "MIT",
      "dependencies": {
        "@jridgewell/sourcemap-codec": "^1.5.0",
        "@jridgewell/trace-mapping": "^0.3.24"
      }
    },
    "node_modules/@jridgewell/remapping": {
      "version": "2.3.5",
      "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
      "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
      "license": "MIT",
      "dependencies": {
        "@jridgewell/gen-mapping": "^0.3.5",
        "@jridgewell/trace-mapping": "^0.3.24"
      }
    },
    "node_modules/@jridgewell/resolve-uri": {
      "version": "3.1.2",
      "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
      "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
      "license": "MIT",
      "engines": {
        "node": ">=6.0.0"
      }
    },
    "node_modules/@jridgewell/source-map": {
      "version": "0.3.11",
      "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz",
      "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
      "devOptional": true,
      "license": "MIT",
      "dependencies": {
        "@jridgewell/gen-mapping": "^0.3.5",
        "@jridgewell/trace-mapping": "^0.3.25"
      }
    },
    "node_modules/@jridgewell/sourcemap-codec": {
      "version": "1.5.5",
      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
      "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
      "license": "MIT"
    },
    "node_modules/@jridgewell/trace-mapping": {
      "version": "0.3.31",
      "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
      "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
      "license": "MIT",
      "dependencies": {
        "@jridgewell/resolve-uri": "^3.1.0",
        "@jridgewell/sourcemap-codec": "^1.4.14"
      }
    },
    "node_modules/@nodelib/fs.scandir": {
      "version": "2.1.5",
      "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
      "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
      "license": "MIT",
      "dependencies": {
        "@nodelib/fs.stat": "2.0.5",
        "run-parallel": "^1.1.9"
      },
      "engines": {
        "node": ">= 8"
      }
    },
    "node_modules/@nodelib/fs.stat": {
      "version": "2.0.5",
      "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
      "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
      "license": "MIT",
      "engines": {
        "node": ">= 8"
      }
    },
    "node_modules/@nodelib/fs.walk": {
      "version": "1.2.8",
      "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
      "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
      "license": "MIT",
      "dependencies": {
        "@nodelib/fs.scandir": "2.1.5",
        "fastq": "^1.6.0"
      },
      "engines": {
        "node": ">= 8"
      }
    },
    "node_modules/@parcel/watcher": {
      "version": "2.5.6",
      "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz",
      "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==",
      "hasInstallScript": true,
      "license": "MIT",
      "optional": true,
      "dependencies": {
        "detect-libc": "^2.0.3",
        "is-glob": "^4.0.3",
        "node-addon-api": "^7.0.0",
        "picomatch": "^4.0.3"
      },
      "engines": {
        "node": ">= 10.0.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/parcel"
      },
      "optionalDependencies": {
        "@parcel/watcher-android-arm64": "2.5.6",
        "@parcel/watcher-darwin-arm64": "2.5.6",
        "@parcel/watcher-darwin-x64": "2.5.6",
        "@parcel/watcher-freebsd-x64": "2.5.6",
        "@parcel/watcher-linux-arm-glibc": "2.5.6",
        "@parcel/watcher-linux-arm-musl": "2.5.6",
        "@parcel/watcher-linux-arm64-glibc": "2.5.6",
        "@parcel/watcher-linux-arm64-musl": "2.5.6",
        "@parcel/watcher-linux-x64-glibc": "2.5.6",
        "@parcel/watcher-linux-x64-musl": "2.5.6",
        "@parcel/watcher-win32-arm64": "2.5.6",
        "@parcel/watcher-win32-ia32": "2.5.6",
        "@parcel/watcher-win32-x64": "2.5.6"
      }
    },
    "node_modules/@parcel/watcher-android-arm64": {
      "version": "2.5.6",
      "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz",
      "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==",
      "cpu": [
        "arm64"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "android"
      ],
      "engines": {
        "node": ">= 10.0.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/parcel"
      }
    },
    "node_modules/@parcel/watcher-darwin-arm64": {
      "version": "2.5.6",
      "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz",
      "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==",
      "cpu": [
        "arm64"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "darwin"
      ],
      "engines": {
        "node": ">= 10.0.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/parcel"
      }
    },
    "node_modules/@parcel/watcher-darwin-x64": {
      "version": "2.5.6",
      "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz",
      "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==",
      "cpu": [
        "x64"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "darwin"
      ],
      "engines": {
        "node": ">= 10.0.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/parcel"
      }
    },
    "node_modules/@parcel/watcher-freebsd-x64": {
      "version": "2.5.6",
      "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz",
      "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==",
      "cpu": [
        "x64"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "freebsd"
      ],
      "engines": {
        "node": ">= 10.0.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/parcel"
      }
    },
    "node_modules/@parcel/watcher-linux-arm-glibc": {
      "version": "2.5.6",
      "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz",
      "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==",
      "cpu": [
        "arm"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">= 10.0.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/parcel"
      }
    },
    "node_modules/@parcel/watcher-linux-arm-musl": {
      "version": "2.5.6",
      "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz",
      "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==",
      "cpu": [
        "arm"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">= 10.0.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/parcel"
      }
    },
    "node_modules/@parcel/watcher-linux-arm64-glibc": {
      "version": "2.5.6",
      "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz",
      "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==",
      "cpu": [
        "arm64"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">= 10.0.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/parcel"
      }
    },
    "node_modules/@parcel/watcher-linux-arm64-musl": {
      "version": "2.5.6",
      "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz",
      "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==",
      "cpu": [
        "arm64"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">= 10.0.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/parcel"
      }
    },
    "node_modules/@parcel/watcher-linux-x64-glibc": {
      "version": "2.5.6",
      "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz",
      "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==",
      "cpu": [
        "x64"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">= 10.0.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/parcel"
      }
    },
    "node_modules/@parcel/watcher-linux-x64-musl": {
      "version": "2.5.6",
      "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz",
      "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==",
      "cpu": [
        "x64"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">= 10.0.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/parcel"
      }
    },
    "node_modules/@parcel/watcher-win32-arm64": {
      "version": "2.5.6",
      "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz",
      "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==",
      "cpu": [
        "arm64"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "win32"
      ],
      "engines": {
        "node": ">= 10.0.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/parcel"
      }
    },
    "node_modules/@parcel/watcher-win32-ia32": {
      "version": "2.5.6",
      "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz",
      "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==",
      "cpu": [
        "ia32"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "win32"
      ],
      "engines": {
        "node": ">= 10.0.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/parcel"
      }
    },
    "node_modules/@parcel/watcher-win32-x64": {
      "version": "2.5.6",
      "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz",
      "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==",
      "cpu": [
        "x64"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "win32"
      ],
      "engines": {
        "node": ">= 10.0.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/parcel"
      }
    },
    "node_modules/@parcel/watcher/node_modules/picomatch": {
      "version": "4.0.4",
      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
      "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
      "license": "MIT",
      "optional": true,
      "engines": {
        "node": ">=12"
      },
      "funding": {
        "url": "https://github.com/sponsors/jonschlinkert"
      }
    },
    "node_modules/@rollup/pluginutils": {
      "version": "5.1.0",
      "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz",
      "integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==",
      "license": "MIT",
      "dependencies": {
        "@types/estree": "^1.0.0",
        "estree-walker": "^2.0.2",
        "picomatch": "^2.3.1"
      },
      "engines": {
        "node": ">=14.0.0"
      },
      "peerDependencies": {
        "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
      },
      "peerDependenciesMeta": {
        "rollup": {
          "optional": true
        }
      }
    },
    "node_modules/@rollup/rollup-android-arm-eabi": {
      "version": "4.62.0",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.0.tgz",
      "integrity": "sha512-IPIQ55ythEHkfEd9jMEi32OQ7SxURsGA43JI22lj01OLZNt2NUbJX8YUHxkVWyQ6daHPNn0truF5nSj3DQp6YQ==",
      "cpu": [
        "arm"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "android"
      ]
    },
    "node_modules/@rollup/rollup-android-arm64": {
      "version": "4.62.0",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.0.tgz",
      "integrity": "sha512-M6s9cr10MibETyo8JsOkq+Lo1+lU6hcvb1MApnUql5qte/5hMEgzlN8/ReIKNfRV8rrqX50W1BX9zoUhC192RA==",
      "cpu": [
        "arm64"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "android"
      ]
    },
    "node_modules/@rollup/rollup-darwin-arm64": {
      "version": "4.62.0",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.0.tgz",
      "integrity": "sha512-BqCoMoIbn0keKys+dEAdBa70EtOwV1bEsQCUgU9FdiZmmMge/Zk7LlkYGqbrdHR+Frnt0E1FOanly+rlwvvQzw==",
      "cpu": [
        "arm64"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "darwin"
      ]
    },
    "node_modules/@rollup/rollup-darwin-x64": {
      "version": "4.62.0",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.0.tgz",
      "integrity": "sha512-SIMzST3VFNXDAbeIWDWiFCNM5qncUBDWaEV7NfE7oZbDt2mgfW4MvbKdbYiGOLoM32gbTv608UMd0XktEYSD7w==",
      "cpu": [
        "x64"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "darwin"
      ]
    },
    "node_modules/@rollup/rollup-freebsd-arm64": {
      "version": "4.62.0",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.0.tgz",
      "integrity": "sha512-ezjfSQMP7ArdUsbBwbQIfwAlhE84I2iVnzQNCFSveqV42q+BmKlzVpf7mxv5EchLcoWU4y6/heFzVg1F+hodUQ==",
      "cpu": [
        "arm64"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "freebsd"
      ]
    },
    "node_modules/@rollup/rollup-freebsd-x64": {
      "version": "4.62.0",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.0.tgz",
      "integrity": "sha512-9+qTWGW9AZRhnUgwtTwzNwcPlL87ngkeN0LA+q1bADvmY9aNvWaF2TFW8BZgnQPYxpDI7+rMVLivcd4V737TAQ==",
      "cpu": [
        "x64"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "freebsd"
      ]
    },
    "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
      "version": "4.62.0",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.0.tgz",
      "integrity": "sha512-T1dMEQhXA/jkJ/jyMIw9IovK8bSUq7A8kLIlvZTb/6YIVsp2zLavr4F3oyllHWo7eIVJRyE5n3tUjQJEbE1IuQ==",
      "cpu": [
        "arm"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ]
    },
    "node_modules/@rollup/rollup-linux-arm-musleabihf": {
      "version": "4.62.0",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.0.tgz",
      "integrity": "sha512-2as0LgT7qQpyceQq6VUJYnumUMUrgGQCWIiDIN9DE0/tglsk6o66uCB4f3djRawAltvfCNLyZZrsqbPA6inCsA==",
      "cpu": [
        "arm"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ]
    },
    "node_modules/@rollup/rollup-linux-arm64-gnu": {
      "version": "4.62.0",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.0.tgz",
      "integrity": "sha512-bVURMg+6eNN9C/yc0aVjooZcwTTtYF4YW3xta5pP0//r3o1V8gXEHXWCndj47w/HhwsFroZrFhR+6uQP5T0n0g==",
      "cpu": [
        "arm64"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ]
    },
    "node_modules/@rollup/rollup-linux-arm64-musl": {
      "version": "4.62.0",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.0.tgz",
      "integrity": "sha512-Ful8pM/2yYI83PViWdFdpZhdI8HJ5qsXANe5atypbHDf+KIBBDsZsbyy8hbXnULVvW9NsTh5DHwbcBftyLTfiw==",
      "cpu": [
        "arm64"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ]
    },
    "node_modules/@rollup/rollup-linux-loong64-gnu": {
      "version": "4.62.0",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.0.tgz",
      "integrity": "sha512-9Gp/DgrkzfUBmNPVTyPTvay+4xEP7M/clXpj3efXBcm6uTIVIgDg4rqUpqKXvLEuFRVuEpSAOkhgNeecvaZ4Cg==",
      "cpu": [
        "loong64"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ]
    },
    "node_modules/@rollup/rollup-linux-loong64-musl": {
      "version": "4.62.0",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.0.tgz",
      "integrity": "sha512-m9tsJz54LUXkSYM8+8PG81B9IKK5r+2T0clMq4QrS16xFosufU7firBDAZEsDheDs7wTlP7h3++S7lMsU955HA==",
      "cpu": [
        "loong64"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ]
    },
    "node_modules/@rollup/rollup-linux-ppc64-gnu": {
      "version": "4.62.0",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.0.tgz",
      "integrity": "sha512-3UvJ5PNVU16aJf6M3tFI24pWzAl2/ynfbyRN3ICyQajK1lSkrnVYNnLz3v04J32qKa0FczJc22zeToc0lr2A3w==",
      "cpu": [
        "ppc64"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ]
    },
    "node_modules/@rollup/rollup-linux-ppc64-musl": {
      "version": "4.62.0",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.0.tgz",
      "integrity": "sha512-vRWUAbYLGHBZS6Q8Msb2sfnf1fvJf+47t8l/TwOerM2qArzy+IeNMTHrYLHXh95h8MoatPHI5hhSZNs+mGXKPg==",
      "cpu": [
        "ppc64"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ]
    },
    "node_modules/@rollup/rollup-linux-riscv64-gnu": {
      "version": "4.62.0",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.0.tgz",
      "integrity": "sha512-c00T5SYENHAt86cfW47URaP3Us5vLC/4QO7GYud1G5VNRffCwwCuBspwqYrriuJB+5m0WFzClCn9wed0FBjKvg==",
      "cpu": [
        "riscv64"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ]
    },
    "node_modules/@rollup/rollup-linux-riscv64-musl": {
      "version": "4.62.0",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.0.tgz",
      "integrity": "sha512-krrCDilhXOwFkSkO3Wm9I/f9H0L92XHHwy2fwxjukxIbh0dem8gZqOW5Y8BsHrpJv5qwlRBV+Wl4ZFyRWhUpwg==",
      "cpu": [
        "riscv64"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ]
    },
    "node_modules/@rollup/rollup-linux-s390x-gnu": {
      "version": "4.62.0",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.0.tgz",
      "integrity": "sha512-7pfYFSTc4/rUC/FtAI0Qp6QthDBCIi6/AuP1xYqFk5vanI6KnL5dWKP60OM/05LOsbwTmIcvr6eXC4CJuJ75IA==",
      "cpu": [
        "s390x"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ]
    },
    "node_modules/@rollup/rollup-linux-x64-gnu": {
      "version": "4.62.0",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.0.tgz",
      "integrity": "sha512-7SDIalKeIpG0Ifogbbdn58HmSotYMlf23K3dCJEmiVd9Fg36Vmni82iPQec27N3wY4Bvbxftkxz6vSx9OcouTg==",
      "cpu": [
        "x64"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ]
    },
    "node_modules/@rollup/rollup-linux-x64-musl": {
      "version": "4.62.0",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.0.tgz",
      "integrity": "sha512-eRZevouTH2i1HeAVLqJuLnt256krQkGY0TN6WsTmsIhuzbh457HuWDMakKwmi0Cjadux983CoSr8Lim2QhUIFw==",
      "cpu": [
        "x64"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ]
    },
    "node_modules/@rollup/rollup-openbsd-x64": {
      "version": "4.62.0",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.0.tgz",
      "integrity": "sha512-3oVS7FLGa4U1qcvao9ylGxrjXZyUQqR8UwxEcnUEyPX53O/C/   mKDZegNXTdHCP+h3e6ta/f1EN38Yif1mmZHYg==",
      "cpu": [
        "x64"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "openbsd"
      ]
    },
    "node_modules/@rollup/rollup-openharmony-arm64": {
      "version": "4.62.0",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.0.tgz",
      "integrity": "sha512-yTB9TgfWj5wHe5QgktAgXTLLot1gvEjl1NiPPAUiCs4oPrIWFl5V4nC3GrkNdj9LaAU4s94nVrGbGOCqUpyWsg==",
      "cpu": [
        "arm64"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "openharmony"
      ]
    },
    "node_modules/@rollup/rollup-win32-arm64-msvc": {
      "version": "4.62.0",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.0.tgz",
      "integrity": "sha512-5LOhoaesY3doG1c+ac/2JtgREpKoJr5bUHH8tKY0V8di7+uSV6BwLs2PlR0/yzefGOkR+wE7ZolZphHCsyG5Rw==",
      "cpu": [
        "arm64"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "win32"
      ]
    },
    "node_modules/@rollup/rollup-win32-ia32-msvc": {
      "version": "4.62.0",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.0.tgz",
      "integrity": "sha512-yYkWHhmbhRTWTnWos5HC4GcPQfjlzzCNbM9e/+GXrLuaBXYA3qSDR9f0Vgufd5S8yX81U8jPKp7ZnAjZFMtRnw==",
      "cpu": [
        "ia32"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "win32"
      ]
    },
    "node_modules/@rollup/rollup-win32-x64-gnu": {
      "version": "4.62.0",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.0.tgz",
      "integrity": "sha512-SoTb6lPg25xZlA2ibwQ++ahCCnH+FP0qmEuafMJ4gznZKOlXioKEAeJLgCrqjM98ACziXM9V1amFjICVL4IFoA==",
      "cpu": [
        "x64"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "win32"
      ]
    },
    "node_modules/@rollup/rollup-win32-x64-msvc": {
      "version": "4.62.0",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.0.tgz",
      "integrity": "sha512-5L+T1fMX4RIEBoZzT0+sQ0PhTS36NULFmMXtl1TZo44TMAROIMHbZufSOjVWt/Y622BtxgxtaNOokbTDvfsrZA==",
      "cpu": [
        "x64"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "win32"
      ]
    },
    "node_modules/@sinonjs/commons": {
      "version": "1.8.6",
      "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz",
      "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==",
      "dev": true,
      "license": "BSD-3-Clause",
      "peer": true,
      "dependencies": {
        "type-detect": "4.0.8"
      }
    },
    "node_modules/@sinonjs/fake-timers": {
      "version": "8.1.0",
      "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-8.1.0.tgz",
      "integrity": "sha512-OAPJUAtgeINhh/TAlUID4QTs53Njm7xzddaVlEs/SXwgtiD1tW22zAB/W1wdqfrpmikgaWQ9Fw6Ws+hsiRm5Vg==",
      "dev": true,
      "license": "BSD-3-Clause",
      "peer": true,
      "dependencies": {
        "@sinonjs/commons": "^1.7.0"
      }
    },
    "node_modules/@tootallnate/once": {
      "version": "1.1.2",
      "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz",
      "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "engines": {
        "node": ">= 6"
      }
    },
    "node_modules/@types/babel__core": {
      "version": "7.20.5",
      "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
      "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "@babel/parser": "^7.20.7",
        "@babel/types": "^7.20.7",
        "@types/babel__generator": "*",
        "@types/babel__template": "*",
        "@types/babel__traverse": "*"
      }
    },
    "node_modules/@types/babel__generator": {
      "version": "7.27.0",
      "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
      "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "@babel/types": "^7.0.0"
      }
    },
    "node_modules/@types/babel__template": {
      "version": "7.4.4",
      "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
      "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "@babel/parser": "^7.1.0",
        "@babel/types": "^7.0.0"
      }
    },
    "node_modules/@types/babel__traverse": {
      "version": "7.28.0",
      "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
      "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "@babel/types": "^7.28.2"
      }
    },
    "node_modules/@types/babel__traverse/node_modules/@babel/types": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
      "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "@babel/helper-string-parser": "^7.29.7",
        "@babel/helper-validator-identifier": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      }
    },
    "node_modules/@types/estree": {
      "version": "1.0.9",
      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
      "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
      "license": "MIT"
    },
    "node_modules/@types/graceful-fs": {
      "version": "4.1.9",
      "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz",
      "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "@types/node": "*"
      }
    },
    "node_modules/@types/istanbul-lib-coverage": {
      "version": "2.0.6",
      "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
      "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==",
      "dev": true,
      "license": "MIT",
      "peer": true
    },
    "node_modules/@types/istanbul-lib-report": {
      "version": "3.0.3",
      "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz",
      "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "@types/istanbul-lib-coverage": "*"
      }
    },
    "node_modules/@types/istanbul-reports": {
      "version": "3.0.4",
      "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz",
      "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "@types/istanbul-lib-report": "*"
      }
    },
    "node_modules/@types/node": {
      "version": "25.9.3",
      "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.3.tgz",
      "integrity": "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg==",
      "devOptional": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "undici-types": ">=7.24.0 <7.24.7"
      }
    },
    "node_modules/@types/prettier": {
      "version": "2.7.3",
      "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.3.tgz",
      "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==",
      "dev": true,
      "license": "MIT",
      "peer": true
    },
    "node_modules/@types/stack-utils": {
      "version": "2.0.3",
      "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz",
      "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==",
      "dev": true,
      "license": "MIT",
      "peer": true
    },
    "node_modules/@types/yargs": {
      "version": "16.0.11",
      "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.11.tgz",
      "integrity": "sha512-sbtvk8wDN+JvEdabmZExoW/HNr1cB7D/j4LT08rMiuikfA7m/JNJg7ATQcgzs34zHnoScDkY0ZRSl29Fkmk36g==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "@types/yargs-parser": "*"
      }
    },
    "node_modules/@types/yargs-parser": {
      "version": "21.0.3",
      "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz",
      "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==",
      "dev": true,
      "license": "MIT",
      "peer": true
    },
    "node_modules/@vitejs/plugin-legacy": {
      "version": "5.3.2",
      "resolved": "https://registry.npmjs.org/@vitejs/plugin-legacy/-/plugin-legacy-5.3.2.tgz",
      "integrity": "sha512-8moCOrIMaZ/Rjln0Q6GsH6s8fAt1JOI3k8nmfX4tXUxE5KAExVctSyOBk+A25GClsdSWqIk2yaUthH3KJ2X4tg==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/core": "^7.23.9",
        "@babel/preset-env": "^7.23.9",
        "browserslist": "^4.23.0",
        "browserslist-to-esbuild": "^2.1.1",
        "core-js": "^3.36.0",
        "magic-string": "^0.30.7",
        "regenerator-runtime": "^0.14.1",
        "systemjs": "^6.14.3"
      },
      "engines": {
        "node": "^18.0.0 || >=20.0.0"
      },
      "funding": {
        "url": "https://github.com/vitejs/vite?sponsor=1"
      },
      "peerDependencies": {
        "terser": "^5.4.0",
        "vite": "^5.0.0"
      }
    },
    "node_modules/@vitejs/plugin-vue": {
      "version": "5.2.4",
      "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz",
      "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==",
      "license": "MIT",
      "engines": {
        "node": "^18.0.0 || >=20.0.0"
      },
      "peerDependencies": {
        "vite": "^5.0.0 || ^6.0.0",
        "vue": "^3.2.25"
      }
    },
    "node_modules/@vitejs/plugin-vue-jsx": {
      "version": "3.1.0",
      "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue-jsx/-/plugin-vue-jsx-3.1.0.tgz",
      "integrity": "sha512-w9M6F3LSEU5kszVb9An2/MmXNxocAnUb3WhRr8bHlimhDrXNt6n6D2nJQR3UXpGlZHh/EsgouOHCsM8V3Ln+WA==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/core": "^7.23.3",
        "@babel/plugin-transform-typescript": "^7.23.3",
        "@vue/babel-plugin-jsx": "^1.1.5"
      },
      "engines": {
        "node": "^14.18.0 || >=16.0.0"
      },
      "peerDependencies": {
        "vite": "^4.0.0 || ^5.0.0",
        "vue": "^3.0.0"
      }
    },
    "node_modules/@vue/babel-helper-vue-transform-on": {
      "version": "1.5.0",
      "resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.5.0.tgz",
      "integrity": "sha512-0dAYkerNhhHutHZ34JtTl2czVQHUNWv6xEbkdF5W+Yrv5pCWsqjeORdOgbtW2I9gWlt+wBmVn+ttqN9ZxR5tzA==",
      "dev": true,
      "license": "MIT"
    },
    "node_modules/@vue/babel-plugin-jsx": {
      "version": "1.5.0",
      "resolved": "https://registry.npmjs.org/@vue/babel-plugin-jsx/-/babel-plugin-jsx-1.5.0.tgz",
      "integrity": "sha512-mneBhw1oOqCd2247O0Yw/mRwC9jIGACAJUlawkmMBiNmL4dGA2eMzuNZVNqOUfYTa6vqmND4CtOPzmEEEqLKFw==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-module-imports": "^7.27.1",
        "@babel/helper-plugin-utils": "^7.27.1",
        "@babel/plugin-syntax-jsx": "^7.27.1",
        "@babel/template": "^7.27.2",
        "@babel/traverse": "^7.28.0",
        "@babel/types": "^7.28.2",
        "@vue/babel-helper-vue-transform-on": "1.5.0",
        "@vue/babel-plugin-resolve-type": "1.5.0",
        "@vue/shared": "^3.5.18"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      },
      "peerDependenciesMeta": {
        "@babel/core": {
          "optional": true
        }
      }
    },
    "node_modules/@vue/babel-plugin-jsx/node_modules/@babel/types": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
      "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-string-parser": "^7.29.7",
        "@babel/helper-validator-identifier": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      }
    },
    "node_modules/@vue/babel-plugin-jsx/node_modules/@vue/shared": {
      "version": "3.5.38",
      "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.38.tgz",
      "integrity": "sha512-FTW0AFZNaK5/mOqvGBwVfUlNLU38TiQn4+DQgIFUnrBBJQ1crMJ82yeGQLV5jyKFsO8yRukpbuP7x+nRbH6aug==",
      "dev": true,
      "license": "MIT"
    },
    "node_modules/@vue/babel-plugin-resolve-type": {
      "version": "1.5.0",
      "resolved": "https://registry.npmjs.org/@vue/babel-plugin-resolve-type/-/babel-plugin-resolve-type-1.5.0.tgz",
      "integrity": "sha512-Wm/60o+53JwJODm4Knz47dxJnLDJ9FnKnGZJbUUf8nQRAtt6P+undLUAVU3Ha33LxOJe6IPoifRQ6F/0RrU31w==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/code-frame": "^7.27.1",
        "@babel/helper-module-imports": "^7.27.1",
        "@babel/helper-plugin-utils": "^7.27.1",
        "@babel/parser": "^7.28.0",
        "@vue/compiler-sfc": "^3.5.18"
      },
      "funding": {
        "url": "https://github.com/sponsors/sxzz"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@vue/babel-plugin-resolve-type/node_modules/@babel/code-frame": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
      "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-validator-identifier": "^7.29.7",
        "js-tokens": "^4.0.0",
        "picocolors": "^1.1.1"
      },
      "engines": {
        "node": ">=6.9.0"
      }
    },
    "node_modules/@vue/babel-plugin-resolve-type/node_modules/@babel/parser": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
      "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/types": "^7.29.7"
      },
      "bin": {
        "parser": "bin/babel-parser.js"
      },
      "engines": {
        "node": ">=6.0.0"
      }
    },
    "node_modules/@vue/babel-plugin-resolve-type/node_modules/@babel/types": {
      "version": "7.29.7",
      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
      "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-string-parser": "^7.29.7",
        "@babel/helper-validator-identifier": "^7.29.7"
      },
      "engines": {
        "node": ">=6.9.0"
      }
    },
    "node_modules/@vue/babel-plugin-resolve-type/node_modules/@vue/compiler-core": {
      "version": "3.5.38",
      "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.38.tgz",
      "integrity": "sha512-s99aGxWYig9ErHbct27KXEGhrBYlRI6c4MwAgXErOAbX9xiW37/uMa+XUDO69zLz83dng8UUZ70CTOJrLrYrEQ==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/parser": "^7.29.7",
        "@vue/shared": "3.5.38",
        "entities": "^7.0.1",
        "estree-walker": "^2.0.2",
        "source-map-js": "^1.2.1"
      }
    },
    "node_modules/@vue/babel-plugin-resolve-type/node_modules/@vue/compiler-dom": {
      "version": "3.5.38",
      "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.38.tgz",
      "integrity": "sha512-JTqp25l8aFfJYF7/KmsXZjAxJz7T+SjmTJLoXVjHtc2BrSgSiW2n9Aem/cWq1OPe68A8JL06B3eVdhlP0H4TVw==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@vue/compiler-core": "3.5.38",
        "@vue/shared": "3.5.38"
      }
    },
    "node_modules/@vue/babel-plugin-resolve-type/node_modules/@vue/compiler-sfc": {
      "version": "3.5.38",
      "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.38.tgz",
      "integrity": "sha512-DuA2GiZawSEW442iw/9+Fkol8hTgb4Ke5KkhmSry65QA7YuyMbIdy8p0XZRMvNwJdgRz307W8g1CSzdvS4nuNg==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/parser": "^7.29.7",
        "@vue/compiler-core": "3.5.38",
        "@vue/compiler-dom": "3.5.38",
        "@vue/compiler-ssr": "3.5.38",
        "@vue/shared": "3.5.38",
        "estree-walker": "^2.0.2",
        "magic-string": "^0.30.21",
        "postcss": "^8.5.15",
        "source-map-js": "^1.2.1"
      }
    },
    "node_modules/@vue/babel-plugin-resolve-type/node_modules/@vue/compiler-ssr": {
      "version": "3.5.38",
      "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.38.tgz",
      "integrity": "sha512-7s+W5Gc42FGxZMcuwl8H5B29T8BJPMdBT7KHFE+BbAuZ/iTEdTtv7z2XiMjiaUUw4w3ZcCEdHs36RuYJ2VA7bA==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@vue/compiler-dom": "3.5.38",
        "@vue/shared": "3.5.38"
      }
    },
    "node_modules/@vue/babel-plugin-resolve-type/node_modules/@vue/shared": {
      "version": "3.5.38",
      "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.38.tgz",
      "integrity": "sha512-FTW0AFZNaK5/mOqvGBwVfUlNLU38TiQn4+DQgIFUnrBBJQ1crMJ82yeGQLV5jyKFsO8yRukpbuP7x+nRbH6aug==",
      "dev": true,
      "license": "MIT"
    },
    "node_modules/@vue/babel-plugin-resolve-type/node_modules/magic-string": {
      "version": "0.30.21",
      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
      "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@jridgewell/sourcemap-codec": "^1.5.5"
      }
    },
    "node_modules/@vue/babel-plugin-resolve-type/node_modules/picocolors": {
      "version": "1.1.1",
      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
      "dev": true,
      "license": "ISC"
    },
    "node_modules/@vue/compiler-core": {
      "version": "3.4.21",
      "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.21.tgz",
      "integrity": "sha512-MjXawxZf2SbZszLPYxaFCjxfibYrzr3eYbKxwpLR9EQN+oaziSu3qKVbwBERj1IFIB8OLUewxB5m/BFzi613og==",
      "license": "MIT",
      "dependencies": {
        "@babel/parser": "^7.23.9",
        "@vue/shared": "3.4.21",
        "entities": "^4.5.0",
        "estree-walker": "^2.0.2",
        "source-map-js": "^1.0.2"
      }
    },
    "node_modules/@vue/compiler-core/node_modules/entities": {
      "version": "4.5.0",
      "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
      "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
      "license": "BSD-2-Clause",
      "engines": {
        "node": ">=0.12"
      },
      "funding": {
        "url": "https://github.com/fb55/entities?sponsor=1"
      }
    },
    "node_modules/@vue/compiler-dom": {
      "version": "3.4.21",
      "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.21.tgz",
      "integrity": "sha512-IZC6FKowtT1sl0CR5DpXSiEB5ayw75oT2bma1BEhV7RRR1+cfwLrxc2Z8Zq/RGFzJ8w5r9QtCOvTjQgdn0IKmA==",
      "license": "MIT",
      "dependencies": {
        "@vue/compiler-core": "3.4.21",
        "@vue/shared": "3.4.21"
      }
    },
    "node_modules/@vue/compiler-sfc": {
      "version": "3.4.21",
      "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.21.tgz",
      "integrity": "sha512-me7epoTxYlY+2CUM7hy9PCDdpMPfIwrOvAXud2Upk10g4YLv9UBW7kL798TvMeDhPthkZ0CONNrK2GoeI1ODiQ==",
      "license": "MIT",
      "dependencies": {
        "@babel/parser": "^7.23.9",
        "@vue/compiler-core": "3.4.21",
        "@vue/compiler-dom": "3.4.21",
        "@vue/compiler-ssr": "3.4.21",
        "@vue/shared": "3.4.21",
        "estree-walker": "^2.0.2",
        "magic-string": "^0.30.7",
        "postcss": "^8.4.35",
        "source-map-js": "^1.0.2"
      }
    },
    "node_modules/@vue/compiler-ssr": {
      "version": "3.4.21",
      "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.21.tgz",
      "integrity": "sha512-M5+9nI2lPpAsgXOGQobnIueVqc9sisBFexh5yMIMRAPYLa7+5wEJs8iqOZc1WAa9WQbx9GR2twgznU8LTIiZ4Q==",
      "license": "MIT",
      "dependencies": {
        "@vue/compiler-dom": "3.4.21",
        "@vue/shared": "3.4.21"
      }
    },
    "node_modules/@vue/consolidate": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/@vue/consolidate/-/consolidate-1.0.0.tgz",
      "integrity": "sha512-oTyUE+QHIzLw2PpV14GD/c7EohDyP64xCniWTcqcEmTd699eFqTIwOmtDYjcO1j3QgdXoJEoWv1/cCdLrRoOfg==",
      "license": "MIT",
      "engines": {
        "node": ">= 0.12.0"
      }
    },
    "node_modules/@vue/devtools-api": {
      "version": "6.6.4",
      "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
      "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
      "license": "MIT"
    },
    "node_modules/@vue/reactivity": {
      "version": "3.5.38",
      "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.38.tgz",
      "integrity": "sha512-pG6LV/NDNRbKizcUjFFLAfjaL8mcv4DmR9avNcUw2gDHBzZneuS2TWCmp633ynzxz9YYKNeEPK2I8Wraqy2HUQ==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@vue/shared": "3.5.38"
      }
    },
    "node_modules/@vue/reactivity/node_modules/@vue/shared": {
      "version": "3.5.38",
      "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.38.tgz",
      "integrity": "sha512-FTW0AFZNaK5/mOqvGBwVfUlNLU38TiQn4+DQgIFUnrBBJQ1crMJ82yeGQLV5jyKFsO8yRukpbuP7x+nRbH6aug==",
      "dev": true,
      "license": "MIT"
    },
    "node_modules/@vue/runtime-core": {
      "version": "3.5.38",
      "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.38.tgz",
      "integrity": "sha512-iyW8WVfF1CpCXxncZY5Ei6rSd6oZr5DgEom//fUjRBRl56AXPD+s9ATvukRt77ZFTuYlnVA1bxY+dJB94tWVYw==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@vue/reactivity": "3.5.38",
        "@vue/shared": "3.5.38"
      }
    },
    "node_modules/@vue/runtime-core/node_modules/@vue/shared": {
      "version": "3.5.38",
      "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.38.tgz",
      "integrity": "sha512-FTW0AFZNaK5/mOqvGBwVfUlNLU38TiQn4+DQgIFUnrBBJQ1crMJ82yeGQLV5jyKFsO8yRukpbuP7x+nRbH6aug==",
      "dev": true,
      "license": "MIT"
    },
    "node_modules/@vue/runtime-dom": {
      "version": "3.4.21",
      "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.21.tgz",
      "integrity": "sha512-gvf+C9cFpevsQxbkRBS1NpU8CqxKw0ebqMvLwcGQrNpx6gqRDodqKqA+A2VZZpQ9RpK2f9yfg8VbW/EpdFUOJw==",
      "license": "MIT",
      "dependencies": {
        "@vue/runtime-core": "3.4.21",
        "@vue/shared": "3.4.21",
        "csstype": "^3.1.3"
      }
    },
    "node_modules/@vue/runtime-dom/node_modules/@vue/reactivity": {
      "version": "3.4.21",
      "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.21.tgz",
      "integrity": "sha512-UhenImdc0L0/4ahGCyEzc/pZNwVgcglGy9HVzJ1Bq2Mm9qXOpP8RyNTjookw/gOCUlXSEtuZ2fUg5nrHcoqJcw==",
      "license": "MIT",
      "dependencies": {
        "@vue/shared": "3.4.21"
      }
    },
    "node_modules/@vue/runtime-dom/node_modules/@vue/runtime-core": {
      "version": "3.4.21",
      "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.21.tgz",
      "integrity": "sha512-pQthsuYzE1XcGZznTKn73G0s14eCJcjaLvp3/DKeYWoFacD9glJoqlNBxt3W2c5S40t6CCcpPf+jG01N3ULyrA==",
      "license": "MIT",
      "dependencies": {
        "@vue/reactivity": "3.4.21",
        "@vue/shared": "3.4.21"
      }
    },
    "node_modules/@vue/server-renderer": {
      "version": "3.4.21",
      "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.21.tgz",
      "integrity": "sha512-aV1gXyKSN6Rz+6kZ6kr5+Ll14YzmIbeuWe7ryJl5muJ4uwSwY/aStXTixx76TwkZFJLm1aAlA/HSWEJ4EyiMkg==",
      "license": "MIT",
      "dependencies": {
        "@vue/compiler-ssr": "3.4.21",
        "@vue/shared": "3.4.21"
      },
      "peerDependencies": {
        "vue": "3.4.21"
      }
    },
    "node_modules/@vue/shared": {
      "version": "3.4.21",
      "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.21.tgz",
      "integrity": "sha512-PuJe7vDIi6VYSinuEbUIQgMIRZGgM8e4R+G+/dQTk0X1NEdvgvvgv7m+rfmDH1gZzyA1OjjoWskvHlfRNfQf3g==",
      "license": "MIT"
    },
    "node_modules/abab": {
      "version": "2.0.6",
      "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
      "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==",
      "deprecated": "Use your platform's native atob() and btoa() methods instead",
      "dev": true,
      "license": "BSD-3-Clause",
      "peer": true
    },
    "node_modules/accepts": {
      "version": "1.3.8",
      "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
      "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "mime-types": "~2.1.34",
        "negotiator": "0.6.3"
      },
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/acorn": {
      "version": "8.17.0",
      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz",
      "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==",
      "license": "MIT",
      "bin": {
        "acorn": "bin/acorn"
      },
      "engines": {
        "node": ">=0.4.0"
      }
    },
    "node_modules/acorn-globals": {
      "version": "6.0.0",
      "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-6.0.0.tgz",
      "integrity": "sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "acorn": "^7.1.1",
        "acorn-walk": "^7.1.1"
      }
    },
    "node_modules/acorn-globals/node_modules/acorn": {
      "version": "7.4.1",
      "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
      "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "bin": {
        "acorn": "bin/acorn"
      },
      "engines": {
        "node": ">=0.4.0"
      }
    },
    "node_modules/acorn-walk": {
      "version": "7.2.0",
      "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz",
      "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "engines": {
        "node": ">=0.4.0"
      }
    },
    "node_modules/address": {
      "version": "1.2.2",
      "resolved": "https://registry.npmjs.org/address/-/address-1.2.2.tgz",
      "integrity": "sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">= 10.0.0"
      }
    },
    "node_modules/adm-zip": {
      "version": "0.5.16",
      "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz",
      "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==",
      "license": "MIT",
      "engines": {
        "node": ">=12.0"
      }
    },
    "node_modules/agent-base": {
      "version": "6.0.2",
      "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
      "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "debug": "4"
      },
      "engines": {
        "node": ">= 6.0.0"
      }
    },
    "node_modules/ansi-escapes": {
      "version": "4.3.2",
      "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
      "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "type-fest": "^0.21.3"
      },
      "engines": {
        "node": ">=8"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/ansi-regex": {
      "version": "5.0.1",
      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/ansi-styles": {
      "version": "3.2.1",
      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
      "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
      "license": "MIT",
      "dependencies": {
        "color-convert": "^1.9.0"
      },
      "engines": {
        "node": ">=4"
      }
    },
    "node_modules/any-base": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/any-base/-/any-base-1.1.0.tgz",
      "integrity": "sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg==",
      "license": "MIT"
    },
    "node_modules/anymatch": {
      "version": "3.1.3",
      "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
      "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
      "license": "ISC",
      "dependencies": {
        "normalize-path": "^3.0.0",
        "picomatch": "^2.0.4"
      },
      "engines": {
        "node": ">= 8"
      }
    },
    "node_modules/argparse": {
      "version": "1.0.10",
      "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
      "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "sprintf-js": "~1.0.2"
      }
    },
    "node_modules/array-flatten": {
      "version": "1.1.1",
      "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
      "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
      "dev": true,
      "license": "MIT"
    },
    "node_modules/asynckit": {
      "version": "0.4.0",
      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
      "dev": true,
      "license": "MIT",
      "peer": true
    },
    "node_modules/autoprefixer": {
      "version": "10.4.20",
      "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz",
      "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==",
      "funding": [
        {
          "type": "opencollective",
          "url": "https://opencollective.com/postcss/"
        },
        {
          "type": "tidelift",
          "url": "https://tidelift.com/funding/github/npm/autoprefixer"
        },
        {
          "type": "github",
          "url": "https://github.com/sponsors/ai"
        }
      ],
      "license": "MIT",
      "dependencies": {
        "browserslist": "^4.23.3",
        "caniuse-lite": "^1.0.30001646",
        "fraction.js": "^4.3.7",
        "normalize-range": "^0.1.2",
        "picocolors": "^1.0.1",
        "postcss-value-parser": "^4.2.0"
      },
      "bin": {
        "autoprefixer": "bin/autoprefixer"
      },
      "engines": {
        "node": "^10 || ^12 || >=14"
      },
      "peerDependencies": {
        "postcss": "^8.1.0"
      }
    },
    "node_modules/babel-jest": {
      "version": "27.5.1",
      "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-27.5.1.tgz",
      "integrity": "sha512-cdQ5dXjGRd0IBRATiQ4mZGlGlRE8kJpjPOixdNRdT+m3UcNqmYWN6rK6nvtXYfY3D76cb8s/O1Ss8ea24PIwcg==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "@jest/transform": "^27.5.1",
        "@jest/types": "^27.5.1",
        "@types/babel__core": "^7.1.14",
        "babel-plugin-istanbul": "^6.1.1",
        "babel-preset-jest": "^27.5.1",
        "chalk": "^4.0.0",
        "graceful-fs": "^4.2.9",
        "slash": "^3.0.0"
      },
      "engines": {
        "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.8.0"
      }
    },
    "node_modules/babel-jest/node_modules/ansi-styles": {
      "version": "4.3.0",
      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "color-convert": "^2.0.1"
      },
      "engines": {
        "node": ">=8"
      },
      "funding": {
        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
      }
    },
    "node_modules/babel-jest/node_modules/chalk": {
      "version": "4.1.2",
      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "ansi-styles": "^4.1.0",
        "supports-color": "^7.1.0"
      },
      "engines": {
        "node": ">=10"
      },
      "funding": {
        "url": "https://github.com/chalk/chalk?sponsor=1"
      }
    },
    "node_modules/babel-jest/node_modules/color-convert": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "color-name": "~1.1.4"
      },
      "engines": {
        "node": ">=7.0.0"
      }
    },
    "node_modules/babel-jest/node_modules/color-name": {
      "version": "1.1.4",
      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
      "dev": true,
      "license": "MIT",
      "peer": true
    },
    "node_modules/babel-jest/node_modules/has-flag": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/babel-jest/node_modules/supports-color": {
      "version": "7.2.0",
      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "has-flag": "^4.0.0"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/babel-plugin-istanbul": {
      "version": "6.1.1",
      "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz",
      "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==",
      "dev": true,
      "license": "BSD-3-Clause",
      "peer": true,
      "dependencies": {
        "@babel/helper-plugin-utils": "^7.0.0",
        "@istanbuljs/load-nyc-config": "^1.0.0",
        "@istanbuljs/schema": "^0.1.2",
        "istanbul-lib-instrument": "^5.0.4",
        "test-exclude": "^6.0.0"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/babel-plugin-jest-hoist": {
      "version": "27.5.1",
      "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-27.5.1.tgz",
      "integrity": "sha512-50wCwD5EMNW4aRpOwtqzyZHIewTYNxLA4nhB+09d8BIssfNfzBRhkBIHiaPv1Si226TQSvp8gxAJm2iY2qs2hQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "@babel/template": "^7.3.3",
        "@babel/types": "^7.3.3",
        "@types/babel__core": "^7.0.0",
        "@types/babel__traverse": "^7.0.6"
      },
      "engines": {
        "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
      }
    },
    "node_modules/babel-plugin-polyfill-corejs2": {
      "version": "0.4.17",
      "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.17.tgz",
      "integrity": "sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/compat-data": "^7.28.6",
        "@babel/helper-define-polyfill-provider": "^0.6.8",
        "semver": "^6.3.1"
      },
      "peerDependencies": {
        "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
      }
    },
    "node_modules/babel-plugin-polyfill-corejs3": {
      "version": "0.14.2",
      "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.2.tgz",
      "integrity": "sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-define-polyfill-provider": "^0.6.8",
        "core-js-compat": "^3.48.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
      }
    },
    "node_modules/babel-plugin-polyfill-regenerator": {
      "version": "0.6.8",
      "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.8.tgz",
      "integrity": "sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/helper-define-polyfill-provider": "^0.6.8"
      },
      "peerDependencies": {
        "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
      }
    },
    "node_modules/babel-preset-current-node-syntax": {
      "version": "1.2.0",
      "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz",
      "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "@babel/plugin-syntax-async-generators": "^7.8.4",
        "@babel/plugin-syntax-bigint": "^7.8.3",
        "@babel/plugin-syntax-class-properties": "^7.12.13",
        "@babel/plugin-syntax-class-static-block": "^7.14.5",
        "@babel/plugin-syntax-import-attributes": "^7.24.7",
        "@babel/plugin-syntax-import-meta": "^7.10.4",
        "@babel/plugin-syntax-json-strings": "^7.8.3",
        "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4",
        "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3",
        "@babel/plugin-syntax-numeric-separator": "^7.10.4",
        "@babel/plugin-syntax-object-rest-spread": "^7.8.3",
        "@babel/plugin-syntax-optional-catch-binding": "^7.8.3",
        "@babel/plugin-syntax-optional-chaining": "^7.8.3",
        "@babel/plugin-syntax-private-property-in-object": "^7.14.5",
        "@babel/plugin-syntax-top-level-await": "^7.14.5"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0 || ^8.0.0-0"
      }
    },
    "node_modules/babel-preset-jest": {
      "version": "27.5.1",
      "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-27.5.1.tgz",
      "integrity": "sha512-Nptf2FzlPCWYuJg41HBqXVT8ym6bXOevuCTbhxlUpjwtysGaIWFvDEjp4y+G7fl13FgOdjs7P/DmErqH7da0Ag==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "babel-plugin-jest-hoist": "^27.5.1",
        "babel-preset-current-node-syntax": "^1.0.0"
      },
      "engines": {
        "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0"
      }
    },
    "node_modules/balanced-match": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
      "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
      "dev": true,
      "license": "MIT",
      "peer": true
    },
    "node_modules/base64-js": {
      "version": "1.5.1",
      "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
      "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
      "funding": [
        {
          "type": "github",
          "url": "https://github.com/sponsors/feross"
        },
        {
          "type": "patreon",
          "url": "https://www.patreon.com/feross"
        },
        {
          "type": "consulting",
          "url": "https://feross.org/support"
        }
      ],
      "license": "MIT"
    },
    "node_modules/base64url": {
      "version": "3.0.1",
      "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz",
      "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==",
      "license": "MIT",
      "engines": {
        "node": ">=6.0.0"
      }
    },
    "node_modules/baseline-browser-mapping": {
      "version": "2.10.37",
      "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.37.tgz",
      "integrity": "sha512-girxaJ7WZssDOFhzCGZTDKoTa1gk6A1TbflaYTpykLJ4UU9Fz9kx1aREM8JCuoVHbL8X8T/mJg7w2oYSq72Oig==",
      "license": "Apache-2.0",
      "bin": {
        "baseline-browser-mapping": "dist/cli.cjs"
      },
      "engines": {
        "node": ">=6.0.0"
      }
    },
    "node_modules/binary-extensions": {
      "version": "2.3.0",
      "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
      "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
      "license": "MIT",
      "engines": {
        "node": ">=8"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/bmp-js": {
      "version": "0.1.0",
      "resolved": "https://registry.npmjs.org/bmp-js/-/bmp-js-0.1.0.tgz",
      "integrity": "sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw==",
      "license": "MIT"
    },
    "node_modules/body-parser": {
      "version": "1.20.3",
      "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
      "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "bytes": "3.1.2",
        "content-type": "~1.0.5",
        "debug": "2.6.9",
        "depd": "2.0.0",
        "destroy": "1.2.0",
        "http-errors": "2.0.0",
        "iconv-lite": "0.4.24",
        "on-finished": "2.4.1",
        "qs": "6.13.0",
        "raw-body": "2.5.2",
        "type-is": "~1.6.18",
        "unpipe": "1.0.0"
      },
      "engines": {
        "node": ">= 0.8",
        "npm": "1.2.8000 || >= 1.4.16"
      }
    },
    "node_modules/body-parser/node_modules/debug": {
      "version": "2.6.9",
      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
      "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "ms": "2.0.0"
      }
    },
    "node_modules/body-parser/node_modules/ms": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
      "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
      "dev": true,
      "license": "MIT"
    },
    "node_modules/body-parser/node_modules/qs": {
      "version": "6.13.0",
      "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
      "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
      "dev": true,
      "license": "BSD-3-Clause",
      "dependencies": {
        "side-channel": "^1.0.6"
      },
      "engines": {
        "node": ">=0.6"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/brace-expansion": {
      "version": "1.1.15",
      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz",
      "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "balanced-match": "^1.0.0",
        "concat-map": "0.0.1"
      }
    },
    "node_modules/braces": {
      "version": "3.0.3",
      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
      "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
      "license": "MIT",
      "dependencies": {
        "fill-range": "^7.1.1"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/browser-process-hrtime": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz",
      "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==",
      "dev": true,
      "license": "BSD-2-Clause",
      "peer": true
    },
    "node_modules/browserslist": {
      "version": "4.28.2",
      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
      "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
      "funding": [
        {
          "type": "opencollective",
          "url": "https://opencollective.com/browserslist"
        },
        {
          "type": "tidelift",
          "url": "https://tidelift.com/funding/github/npm/browserslist"
        },
        {
          "type": "github",
          "url": "https://github.com/sponsors/ai"
        }
      ],
      "license": "MIT",
      "dependencies": {
        "baseline-browser-mapping": "^2.10.12",
        "caniuse-lite": "^1.0.30001782",
        "electron-to-chromium": "^1.5.328",
        "node-releases": "^2.0.36",
        "update-browserslist-db": "^1.2.3"
      },
      "bin": {
        "browserslist": "cli.js"
      },
      "engines": {
        "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
      }
    },
    "node_modules/browserslist-to-esbuild": {
      "version": "2.1.1",
      "resolved": "https://registry.npmjs.org/browserslist-to-esbuild/-/browserslist-to-esbuild-2.1.1.tgz",
      "integrity": "sha512-KN+mty6C3e9AN8Z5dI1xeN15ExcRNeISoC3g7V0Kax/MMF9MSoYA2G7lkTTcVUFntiEjkpI0HNgqJC1NjdyNUw==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "meow": "^13.0.0"
      },
      "bin": {
        "browserslist-to-esbuild": "cli/index.js"
      },
      "engines": {
        "node": ">=18"
      },
      "peerDependencies": {
        "browserslist": "*"
      }
    },
    "node_modules/bser": {
      "version": "2.1.1",
      "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz",
      "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==",
      "dev": true,
      "license": "Apache-2.0",
      "peer": true,
      "dependencies": {
        "node-int64": "^0.4.0"
      }
    },
    "node_modules/buffer": {
      "version": "5.7.1",
      "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
      "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
      "funding": [
        {
          "type": "github",
          "url": "https://github.com/sponsors/feross"
        },
        {
          "type": "patreon",
          "url": "https://www.patreon.com/feross"
        },
        {
          "type": "consulting",
          "url": "https://feross.org/support"
        }
      ],
      "license": "MIT",
      "dependencies": {
        "base64-js": "^1.3.1",
        "ieee754": "^1.1.13"
      }
    },
    "node_modules/buffer-equal": {
      "version": "0.0.1",
      "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-0.0.1.tgz",
      "integrity": "sha512-RgSV6InVQ9ODPdLWJ5UAqBqJBOg370Nz6ZQtRzpt6nUjc8v0St97uJ4PYC6NztqIScrAXafKM3mZPMygSe1ggA==",
      "license": "MIT",
      "engines": {
        "node": ">=0.4.0"
      }
    },
    "node_modules/buffer-from": {
      "version": "1.1.2",
      "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
      "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
      "devOptional": true,
      "license": "MIT"
    },
    "node_modules/bytes": {
      "version": "3.1.2",
      "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
      "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/cac": {
      "version": "6.7.9",
      "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.9.tgz",
      "integrity": "sha512-XN5qEpfNQCJ8jRaZgitSkkukjMRCGio+X3Ks5KUbGGlPbV+pSem1l9VuzooCBXOiMFshUZgyYqg6rgN8rjkb/w==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/call-bind-apply-helpers": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
      "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "es-errors": "^1.3.0",
        "function-bind": "^1.1.2"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/call-bound": {
      "version": "1.0.4",
      "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
      "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "call-bind-apply-helpers": "^1.0.2",
        "get-intrinsic": "^1.3.0"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/callsites": {
      "version": "3.1.0",
      "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
      "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "engines": {
        "node": ">=6"
      }
    },
    "node_modules/camelcase": {
      "version": "5.3.1",
      "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
      "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "engines": {
        "node": ">=6"
      }
    },
    "node_modules/caniuse-lite": {
      "version": "1.0.30001799",
      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz",
      "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==",
      "funding": [
        {
          "type": "opencollective",
          "url": "https://opencollective.com/browserslist"
        },
        {
          "type": "tidelift",
          "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
        },
        {
          "type": "github",
          "url": "https://github.com/sponsors/ai"
        }
      ],
      "license": "CC-BY-4.0"
    },
    "node_modules/centra": {
      "version": "2.7.0",
      "resolved": "https://registry.npmjs.org/centra/-/centra-2.7.0.tgz",
      "integrity": "sha512-PbFMgMSrmgx6uxCdm57RUos9Tc3fclMvhLSATYN39XsDV29B89zZ3KA89jmY0vwSGazyU+uerqwa6t+KaodPcg==",
      "license": "MIT",
      "dependencies": {
        "follow-redirects": "^1.15.6"
      }
    },
    "node_modules/chalk": {
      "version": "2.4.2",
      "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
      "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
      "license": "MIT",
      "dependencies": {
        "ansi-styles": "^3.2.1",
        "escape-string-regexp": "^1.0.5",
        "supports-color": "^5.3.0"
      },
      "engines": {
        "node": ">=4"
      }
    },
    "node_modules/char-regex": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz",
      "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "engines": {
        "node": ">=10"
      }
    },
    "node_modules/chokidar": {
      "version": "3.6.0",
      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
      "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
      "license": "MIT",
      "dependencies": {
        "anymatch": "~3.1.2",
        "braces": "~3.0.2",
        "glob-parent": "~5.1.2",
        "is-binary-path": "~2.1.0",
        "is-glob": "~4.0.1",
        "normalize-path": "~3.0.0",
        "readdirp": "~3.6.0"
      },
      "engines": {
        "node": ">= 8.10.0"
      },
      "funding": {
        "url": "https://paulmillr.com/funding/"
      },
      "optionalDependencies": {
        "fsevents": "~2.3.2"
      }
    },
    "node_modules/ci-info": {
      "version": "3.9.0",
      "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
      "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==",
      "dev": true,
      "funding": [
        {
          "type": "github",
          "url": "https://github.com/sponsors/sibiraj-s"
        }
      ],
      "license": "MIT",
      "peer": true,
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/cjs-module-lexer": {
      "version": "1.4.3",
      "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz",
      "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==",
      "dev": true,
      "license": "MIT",
      "peer": true
    },
    "node_modules/cliui": {
      "version": "7.0.4",
      "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
      "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
      "dev": true,
      "license": "ISC",
      "peer": true,
      "dependencies": {
        "string-width": "^4.2.0",
        "strip-ansi": "^6.0.0",
        "wrap-ansi": "^7.0.0"
      }
    },
    "node_modules/co": {
      "version": "4.6.0",
      "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
      "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "engines": {
        "iojs": ">= 1.0.0",
        "node": ">= 0.12.0"
      }
    },
    "node_modules/collect-v8-coverage": {
      "version": "1.0.3",
      "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz",
      "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==",
      "dev": true,
      "license": "MIT",
      "peer": true
    },
    "node_modules/color-convert": {
      "version": "1.9.3",
      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
      "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
      "license": "MIT",
      "dependencies": {
        "color-name": "1.1.3"
      }
    },
    "node_modules/color-name": {
      "version": "1.1.3",
      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
      "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
      "license": "MIT"
    },
    "node_modules/combined-stream": {
      "version": "1.0.8",
      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "delayed-stream": "~1.0.0"
      },
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/commander": {
      "version": "2.20.3",
      "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
      "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
      "devOptional": true,
      "license": "MIT"
    },
    "node_modules/compare-versions": {
      "version": "3.6.0",
      "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.6.0.tgz",
      "integrity": "sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA==",
      "license": "MIT"
    },
    "node_modules/concat-map": {
      "version": "0.0.1",
      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
      "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
      "dev": true,
      "license": "MIT",
      "peer": true
    },
    "node_modules/confbox": {
      "version": "0.1.8",
      "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz",
      "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==",
      "license": "MIT"
    },
    "node_modules/content-disposition": {
      "version": "0.5.4",
      "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
      "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "safe-buffer": "5.2.1"
      },
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/content-type": {
      "version": "1.0.5",
      "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
      "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/convert-source-map": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
      "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
      "license": "MIT"
    },
    "node_modules/cookie": {
      "version": "0.6.0",
      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
      "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/cookie-signature": {
      "version": "1.0.6",
      "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
      "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
      "dev": true,
      "license": "MIT"
    },
    "node_modules/core-js": {
      "version": "3.49.0",
      "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz",
      "integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==",
      "hasInstallScript": true,
      "license": "MIT",
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/core-js"
      }
    },
    "node_modules/core-js-compat": {
      "version": "3.49.0",
      "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.49.0.tgz",
      "integrity": "sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "browserslist": "^4.28.1"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/core-js"
      }
    },
    "node_modules/core-js-pure": {
      "version": "3.49.0",
      "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.49.0.tgz",
      "integrity": "sha512-XM4RFka59xATyJv/cS3O3Kml72hQXUeGRuuTmMYFxwzc9/7C8OYTaIR/Ji+Yt8DXzsFLNhat15cE/JP15HrCgw==",
      "hasInstallScript": true,
      "license": "MIT",
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/core-js"
      }
    },
    "node_modules/cross-env": {
      "version": "7.0.3",
      "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
      "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "cross-spawn": "^7.0.1"
      },
      "bin": {
        "cross-env": "src/bin/cross-env.js",
        "cross-env-shell": "src/bin/cross-env-shell.js"
      },
      "engines": {
        "node": ">=10.14",
        "npm": ">=6",
        "yarn": ">=1"
      }
    },
    "node_modules/cross-spawn": {
      "version": "7.0.6",
         "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
      "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "path-key": "^3.1.0",
        "shebang-command": "^2.0.0",
        "which": "^2.0.1"
      },
      "engines": {
        "node": ">= 8"
      }
    },
    "node_modules/css-font-size-keywords": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/css-font-size-keywords/-/css-font-size-keywords-1.0.0.tgz",
      "integrity": "sha512-Q+svMDbMlelgCfH/RVDKtTDaf5021O486ZThQPIpahnIjUkMUslC+WuOQSWTgGSrNCH08Y7tYNEmmy0hkfMI8Q==",
      "license": "MIT"
    },
    "node_modules/css-font-stretch-keywords": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/css-font-stretch-keywords/-/css-font-stretch-keywords-1.0.1.tgz",
      "integrity": "sha512-KmugPO2BNqoyp9zmBIUGwt58UQSfyk1X5DbOlkb2pckDXFSAfjsD5wenb88fNrD6fvS+vu90a/tsPpb9vb0SLg==",
      "license": "MIT"
    },
    "node_modules/css-font-style-keywords": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/css-font-style-keywords/-/css-font-style-keywords-1.0.1.tgz",
      "integrity": "sha512-0Fn0aTpcDktnR1RzaBYorIxQily85M2KXRpzmxQPgh8pxUN9Fcn00I8u9I3grNr1QXVgCl9T5Imx0ZwKU973Vg==",
      "license": "MIT"
    },
    "node_modules/css-font-weight-keywords": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/css-font-weight-keywords/-/css-font-weight-keywords-1.0.0.tgz",
      "integrity": "sha512-5So8/NH+oDD+EzsnF4iaG4ZFHQ3vaViePkL1ZbZ5iC/KrsCY+WHq/lvOgrtmuOQ9pBBZ1ADGpaf+A4lj1Z9eYA==",
      "license": "MIT"
    },
    "node_modules/css-list-helpers": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/css-list-helpers/-/css-list-helpers-2.0.0.tgz",
      "integrity": "sha512-9Bj8tZ0jWbAM3u/U6m/boAzAwLPwtjzFvwivr2piSvyVa3K3rChJzQy4RIHkNkKiZCHrEMWDJWtTR8UyVhdDnQ==",
      "license": "MIT"
    },
    "node_modules/css-system-font-keywords": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/css-system-font-keywords/-/css-system-font-keywords-1.0.0.tgz",
      "integrity": "sha512-1umTtVd/fXS25ftfjB71eASCrYhilmEsvDEI6wG/QplnmlfmVM5HkZ/ZX46DT5K3eblFPgLUHt5BRCb0YXkSFA==",
      "license": "MIT"
    },
    "node_modules/cssesc": {
      "version": "3.0.0",
      "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
      "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
      "license": "MIT",
      "bin": {
        "cssesc": "bin/cssesc"
      },
      "engines": {
        "node": ">=4"
      }
    },
    "node_modules/cssom": {
      "version": "0.4.4",
      "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz",
      "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==",
      "dev": true,
      "license": "MIT",
      "peer": true
    },
    "node_modules/cssstyle": {
      "version": "2.3.0",
      "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz",
      "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "cssom": "~0.3.6"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/cssstyle/node_modules/cssom": {
      "version": "0.3.8",
      "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz",
      "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==",
      "dev": true,
      "license": "MIT",
      "peer": true
    },
    "node_modules/csstype": {
      "version": "3.2.3",
      "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
      "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
      "license": "MIT"
    },
    "node_modules/data-urls": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz",
      "integrity": "sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "abab": "^2.0.3",
        "whatwg-mimetype": "^2.3.0",
        "whatwg-url": "^8.0.0"
      },
      "engines": {
        "node": ">=10"
      }
    },
    "node_modules/debug": {
      "version": "4.3.7",
      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
      "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
      "license": "MIT",
      "dependencies": {
        "ms": "^2.1.3"
      },
      "engines": {
        "node": ">=6.0"
      },
      "peerDependenciesMeta": {
        "supports-color": {
          "optional": true
        }
      }
    },
    "node_modules/decimal.js": {
      "version": "10.6.0",
      "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
      "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
      "dev": true,
      "license": "MIT",
      "peer": true
    },
    "node_modules/dedent": {
      "version": "0.7.0",
      "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz",
      "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==",
      "dev": true,
      "license": "MIT",
      "peer": true
    },
    "node_modules/deepmerge": {
      "version": "4.3.1",
      "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
      "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/default-gateway": {
      "version": "6.0.3",
      "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz",
      "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==",
      "dev": true,
      "license": "BSD-2-Clause",
      "dependencies": {
        "execa": "^5.0.0"
      },
      "engines": {
        "node": ">= 10"
      }
    },
    "node_modules/delayed-stream": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "engines": {
        "node": ">=0.4.0"
      }
    },
    "node_modules/depd": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
      "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/destroy": {
      "version": "1.2.0",
      "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
      "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">= 0.8",
        "npm": "1.2.8000 || >= 1.4.16"
      }
    },
    "node_modules/detect-libc": {
      "version": "2.1.2",
      "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
      "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
      "license": "Apache-2.0",
      "optional": true,
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/detect-newline": {
      "version": "3.1.0",
      "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz",
      "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/diff-sequences": {
      "version": "27.5.1",
      "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz",
      "integrity": "sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "engines": {
        "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
      }
    },
    "node_modules/dom-walk": {
      "version": "0.1.2",
      "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz",
      "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w=="
    },
    "node_modules/domexception": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz",
      "integrity": "sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==",
      "deprecated": "Use your platform's native DOMException instead",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "webidl-conversions": "^5.0.0"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/domexception/node_modules/webidl-conversions": {
      "version": "5.0.0",
      "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz",
      "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==",
      "dev": true,
      "license": "BSD-2-Clause",
      "peer": true,
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/dunder-proto": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
      "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "call-bind-apply-helpers": "^1.0.1",
        "es-errors": "^1.3.0",
        "gopd": "^1.2.0"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/ee-first": {
      "version": "1.1.1",
      "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
      "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
      "dev": true,
      "license": "MIT"
    },
    "node_modules/electron-to-chromium": {
      "version": "1.5.372",
      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.372.tgz",
      "integrity": "sha512-M3yhbAlilnwqC8D21t28UCDGHyitShTmmLRU/H+b74P6Ski16Nb9HONYEaVpMj/pwC7BEo5B95FpjODLCWbtfA==",
      "license": "ISC"
    },
    "node_modules/emittery": {
      "version": "0.8.1",
      "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.8.1.tgz",
      "integrity": "sha512-uDfvUjVrfGJJhymx/kz6prltenw1u7WrCg1oa94zYY8xxVpLLUu045LAT0dhDZdXG58/EpPL/5kA180fQ/qudg==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "engines": {
        "node": ">=10"
      },
      "funding": {
        "url": "https://github.com/sindresorhus/emittery?sponsor=1"
      }
    },
    "node_modules/emoji-regex": {
      "version": "8.0.0",
      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
      "dev": true,
      "license": "MIT",
      "peer": true
    },
    "node_modules/encodeurl": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
      "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/entities": {
      "version": "7.0.1",
      "resolved": "https://registry.npmjs.org/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/error-ex": {
      "version": "1.3.4",
      "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
      "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "is-arrayish": "^0.2.1"
      }
    },
    "node_modules/es-define-property": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
      "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/es-errors": {
      "version": "1.3.0",
      "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/es-module-lexer": {
      "version": "1.5.4",
      "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz",
      "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==",
      "license": "MIT"
    },
    "node_modules/es-object-atoms": {
      "version": "1.1.2",
      "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
      "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "es-errors": "^1.3.0"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/es-set-tostringtag": {
      "version": "2.1.0",
      "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
      "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "es-errors": "^1.3.0",
        "get-intrinsic": "^1.2.6",
        "has-tostringtag": "^1.0.2",
        "hasown": "^2.0.2"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/esbuild": {
      "version": "0.20.2",
      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz",
      "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==",
      "hasInstallScript": true,
      "license": "MIT",
      "bin": {
        "esbuild": "bin/esbuild"
      },
      "engines": {
        "node": ">=12"
      },
      "optionalDependencies": {
        "@esbuild/aix-ppc64": "0.20.2",
        "@esbuild/android-arm": "0.20.2",
        "@esbuild/android-arm64": "0.20.2",
        "@esbuild/android-x64": "0.20.2",
        "@esbuild/darwin-arm64": "0.20.2",
        "@esbuild/darwin-x64": "0.20.2",
        "@esbuild/freebsd-arm64": "0.20.2",
        "@esbuild/freebsd-x64": "0.20.2",
        "@esbuild/linux-arm": "0.20.2",
        "@esbuild/linux-arm64": "0.20.2",
        "@esbuild/linux-ia32": "0.20.2",
        "@esbuild/linux-loong64": "0.20.2",
        "@esbuild/linux-mips64el": "0.20.2",
        "@esbuild/linux-ppc64": "0.20.2",
        "@esbuild/linux-riscv64": "0.20.2",
        "@esbuild/linux-s390x": "0.20.2",
        "@esbuild/linux-x64": "0.20.2",
        "@esbuild/netbsd-x64": "0.20.2",
        "@esbuild/openbsd-x64": "0.20.2",
        "@esbuild/sunos-x64": "0.20.2",
        "@esbuild/win32-arm64": "0.20.2",
        "@esbuild/win32-ia32": "0.20.2",
        "@esbuild/win32-x64": "0.20.2"
      }
    },
    "node_modules/escalade": {
      "version": "3.2.0",
      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
      "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
      "license": "MIT",
      "engines": {
        "node": ">=6"
      }
    },
    "node_modules/escape-html": {
      "version": "1.0.3",
      "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
      "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
      "dev": true,
      "license": "MIT"
    },
    "node_modules/escape-string-regexp": {
      "version": "1.0.5",
      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
      "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
      "license": "MIT",
      "engines": {
        "node": ">=0.8.0"
      }
    },
    "node_modules/escodegen": {
      "version": "2.1.0",
      "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz",
      "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==",
      "dev": true,
      "license": "BSD-2-Clause",
      "peer": true,
      "dependencies": {
        "esprima": "^4.0.1",
        "estraverse": "^5.2.0",
        "esutils": "^2.0.2"
      },
      "bin": {
        "escodegen": "bin/escodegen.js",
        "esgenerate": "bin/esgenerate.js"
      },
      "engines": {
        "node": ">=6.0"
      },
      "optionalDependencies": {
        "source-map": "~0.6.1"
      }
    },
    "node_modules/esprima": {
      "version": "4.0.1",
      "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
      "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
      "dev": true,
      "license": "BSD-2-Clause",
      "peer": true,
      "bin": {
        "esparse": "bin/esparse.js",
        "esvalidate": "bin/esvalidate.js"
      },
      "engines": {
        "node": ">=4"
      }
    },
    "node_modules/estraverse": {
      "version": "5.3.0",
      "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
      "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
      "dev": true,
      "license": "BSD-2-Clause",
      "peer": true,
      "engines": {
        "node": ">=4.0"
      }
    },
    "node_modules/estree-walker": {
      "version": "2.0.2",
      "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
      "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
      "license": "MIT"
    },
    "node_modules/esutils": {
      "version": "2.0.3",
      "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
      "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
      "dev": true,
      "license": "BSD-2-Clause",
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/etag": {
      "version": "1.8.1",
      "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
      "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/execa": {
      "version": "5.1.1",
      "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
      "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "cross-spawn": "^7.0.3",
        "get-stream": "^6.0.0",
        "human-signals": "^2.1.0",
        "is-stream": "^2.0.0",
        "merge-stream": "^2.0.0",
        "npm-run-path": "^4.0.1",
        "onetime": "^5.1.2",
        "signal-exit": "^3.0.3",
        "strip-final-newline": "^2.0.0"
      },
      "engines": {
        "node": ">=10"
      },
      "funding": {
        "url": "https://github.com/sindresorhus/execa?sponsor=1"
      }
    },
    "node_modules/exif-parser": {
      "version": "0.1.12",
      "resolved": "https://registry.npmjs.org/exif-parser/-/exif-parser-0.1.12.tgz",
      "integrity": "sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw=="
    },
    "node_modules/exit": {
      "version": "0.1.2",
      "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz",
      "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==",
      "dev": true,
      "peer": true,
      "engines": {
        "node": ">= 0.8.0"
      }
    },
    "node_modules/expect": {
      "version": "27.5.1",
      "resolved": "https://registry.npmjs.org/expect/-/expect-27.5.1.tgz",
      "integrity": "sha512-E1q5hSUG2AmYQwQJ041nvgpkODHQvB+RKlB4IYdru6uJsyFTRyZAP463M+1lINorwbqAmUggi6+WwkD8lCS/Dw==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "@jest/types": "^27.5.1",
        "jest-get-type": "^27.5.1",
        "jest-matcher-utils": "^27.5.1",
        "jest-message-util": "^27.5.1"
      },
      "engines": {
        "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
      }
    },
    "node_modules/express": {
      "version": "4.20.0",
      "resolved": "https://registry.npmjs.org/express/-/express-4.20.0.tgz",
      "integrity": "sha512-pLdae7I6QqShF5PnNTCVn4hI91Dx0Grkn2+IAsMTgMIKuQVte2dN9PeGSSAME2FR8anOhVA62QDIUaWVfEXVLw==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "accepts": "~1.3.8",
        "array-flatten": "1.1.1",
        "body-parser": "1.20.3",
        "content-disposition": "0.5.4",
        "content-type": "~1.0.4",
        "cookie": "0.6.0",
        "cookie-signature": "1.0.6",
        "debug": "2.6.9",
        "depd": "2.0.0",
        "encodeurl": "~2.0.0",
        "escape-html": "~1.0.3",
        "etag": "~1.8.1",
        "finalhandler": "1.2.0",
        "fresh": "0.5.2",
        "http-errors": "2.0.0",
        "merge-descriptors": "1.0.3",
        "methods": "~1.1.2",
        "on-finished": "2.4.1",
        "parseurl": "~1.3.3",
        "path-to-regexp": "0.1.10",
        "proxy-addr": "~2.0.7",
        "qs": "6.11.0",
        "range-parser": "~1.2.1",
        "safe-buffer": "5.2.1",
        "send": "0.19.0",
        "serve-static": "1.16.0",
        "setprototypeof": "1.2.0",
        "statuses": "2.0.1",
        "type-is": "~1.6.18",
        "utils-merge": "1.0.1",
        "vary": "~1.1.2"
      },
      "engines": {
        "node": ">= 0.10.0"
      }
    },
    "node_modules/express/node_modules/debug": {
      "version": "2.6.9",
      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
      "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "ms": "2.0.0"
      }
    },
    "node_modules/express/node_modules/ms": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
      "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
      "dev": true,
      "license": "MIT"
    },
    "node_modules/exsolve": {
      "version": "1.0.8",
      "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz",
      "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==",
      "license": "MIT"
    },
    "node_modules/fast-glob": {
      "version": "3.3.3",
      "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
      "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
      "license": "MIT",
      "dependencies": {
        "@nodelib/fs.stat": "^2.0.2",
        "@nodelib/fs.walk": "^1.2.3",
        "glob-parent": "^5.1.2",
        "merge2": "^1.3.0",
        "micromatch": "^4.0.8"
      },
      "engines": {
        "node": ">=8.6.0"
      }
    },
    "node_modules/fast-json-stable-stringify": {
      "version": "2.1.0",
      "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
      "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
      "dev": true,
      "license": "MIT",
      "peer": true
    },
    "node_modules/fastq": {
      "version": "1.20.1",
      "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
      "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
      "license": "ISC",
      "dependencies": {
        "reusify": "^1.0.4"
      }
    },
    "node_modules/fb-watchman": {
      "version": "2.0.2",
      "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz",
      "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==",
      "dev": true,
      "license": "Apache-2.0",
      "peer": true,
      "dependencies": {
        "bser": "2.1.1"
      }
    },
    "node_modules/file-type": {
      "version": "9.0.0",
      "resolved": "https://registry.npmjs.org/file-type/-/file-type-9.0.0.tgz",
      "integrity": "sha512-Qe/5NJrgIOlwijpq3B7BEpzPFcgzggOTagZmkXQY4LA6bsXKTUstK7Wp12lEJ/mLKTpvIZxmIuRcLYWT6ov9lw==",
      "license": "MIT",
      "engines": {
        "node": ">=6"
      }
    },
    "node_modules/fill-range": {
      "version": "7.1.1",
      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
      "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
      "license": "MIT",
      "dependencies": {
        "to-regex-range": "^5.0.1"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/finalhandler": {
      "version": "1.2.0",
      "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
      "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "debug": "2.6.9",
        "encodeurl": "~1.0.2",
        "escape-html": "~1.0.3",
        "on-finished": "2.4.1",
        "parseurl": "~1.3.3",
        "statuses": "2.0.1",
        "unpipe": "~1.0.0"
      },
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/finalhandler/node_modules/debug": {
      "version": "2.6.9",
      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
      "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "ms": "2.0.0"
      }
    },
    "node_modules/finalhandler/node_modules/encodeurl": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
      "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/finalhandler/node_modules/ms": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
      "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
      "dev": true,
      "license": "MIT"
    },
    "node_modules/find-up": {
      "version": "4.1.0",
      "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
      "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "locate-path": "^5.0.0",
        "path-exists": "^4.0.0"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/follow-redirects": {
      "version": "1.16.0",
      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
      "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
      "funding": [
        {
          "type": "individual",
          "url": "https://github.com/sponsors/RubenVerborgh"
        }
      ],
      "license": "MIT",
      "engines": {
        "node": ">=4.0"
      },
      "peerDependenciesMeta": {
        "debug": {
          "optional": true
        }
      }
    },
    "node_modules/form-data": {
      "version": "3.0.5",
      "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.5.tgz",
      "integrity": "sha512-j23EibVLnp4zNXGW7LjryXYa2X6U/M96yoOX+ybZxwkYajdxRNEqYY3zhh7y0i6kfISKS2jr+EJq1YTUDEv5+w==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "asynckit": "^0.4.0",
        "combined-stream": "^1.0.8",
        "es-set-tostringtag": "^2.1.0",
        "hasown": "^2.0.4",
        "mime-types": "^2.1.35"
      },
      "engines": {
        "node": ">= 6"
      }
    },
    "node_modules/forwarded": {
      "version": "0.2.0",
      "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
      "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/fraction.js": {
      "version": "4.3.7",
      "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
      "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==",
      "license": "MIT",
      "engines": {
        "node": "*"
      },
      "funding": {
        "type": "patreon",
        "url": "https://github.com/sponsors/rawify"
      }
    },
    "node_modules/fresh": {
      "version": "0.5.2",
      "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
      "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/fs-extra": {
      "version": "10.1.0",
      "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
      "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
      "license": "MIT",
      "dependencies": {
        "graceful-fs": "^4.2.0",
        "jsonfile": "^6.0.1",
        "universalify": "^2.0.0"
      },
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/fs.realpath": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
      "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
      "dev": true,
      "license": "ISC",
      "peer": true
    },
    "node_modules/fsevents": {
      "version": "2.3.3",
      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
      "hasInstallScript": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "darwin"
      ],
      "engines": {
        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
      }
    },
    "node_modules/function-bind": {
      "version": "1.1.2",
      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
      "license": "MIT",
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/generic-names": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/generic-names/-/generic-names-4.0.0.tgz",
      "integrity": "sha512-ySFolZQfw9FoDb3ed9d80Cm9f0+r7qj+HJkWjeD9RBfpxEVTlVhol+gvaQB/78WbwYfbnNh8nWHHBSlg072y6A==",
      "license": "MIT",
      "dependencies": {
        "loader-utils": "^3.2.0"
      }
    },
    "node_modules/gensync": {
      "version": "1.0.0-beta.2",
      "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
      "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
      "license": "MIT",
      "engines": {
        "node": ">=6.9.0"
      }
    },
    "node_modules/get-caller-file": {
      "version": "2.0.5",
      "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
      "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
      "dev": true,
      "license": "ISC",
      "peer": true,
      "engines": {
        "node": "6.* || 8.* || >= 10.*"
      }
    },
    "node_modules/get-intrinsic": {
      "version": "1.3.0",
      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
      "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "call-bind-apply-helpers": "^1.0.2",
        "es-define-property": "^1.0.1",
        "es-errors": "^1.3.0",
        "es-object-atoms": "^1.1.1",
        "function-bind": "^1.1.2",
        "get-proto": "^1.0.1",
        "gopd": "^1.2.0",
        "has-symbols": "^1.1.0",
        "hasown": "^2.0.2",
        "math-intrinsics": "^1.1.0"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/get-package-type": {
      "version": "0.1.0",
      "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz",
      "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "engines": {
        "node": ">=8.0.0"
      }
    },
    "node_modules/get-proto": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
      "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "dunder-proto": "^1.0.1",
        "es-object-atoms": "^1.0.0"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/get-stream": {
      "version": "6.0.1",
      "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
      "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=10"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/glob": {
      "version": "7.2.3",
      "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
      "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
      "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
      "dev": true,
      "license": "ISC",
      "peer": true,
      "dependencies": {
        "fs.realpath": "^1.0.0",
        "inflight": "^1.0.4",
        "inherits": "2",
        "minimatch": "^3.1.1",
        "once": "^1.3.0",
        "path-is-absolute": "^1.0.0"
      },
      "engines": {
        "node": "*"
      },
      "funding": {
        "url": "https://github.com/sponsors/isaacs"
      }
    },
    "node_modules/glob-parent": {
      "version": "5.1.2",
      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
      "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
      "license": "ISC",
      "dependencies": {
        "is-glob": "^4.0.1"
      },
      "engines": {
        "node": ">= 6"
      }
    },
    "node_modules/global": {
      "version": "4.4.0",
      "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz",
      "integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==",
      "license": "MIT",
      "dependencies": {
        "min-document": "^2.19.0",
        "process": "^0.11.10"
      }
    },
    "node_modules/gopd": {
      "version": "1.2.0",
      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
      "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/graceful-fs": {
      "version": "4.2.11",
      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
      "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
      "license": "ISC"
    },
    "node_modules/has-flag": {
      "version": "3.0.0",
      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
      "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
      "license": "MIT",
      "engines": {
        "node": ">=4"
      }
    },
    "node_modules/has-symbols": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
      "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/has-tostringtag": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
      "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "has-symbols": "^1.0.3"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/hash-sum": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-2.0.0.tgz",
      "integrity": "sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==",
      "license": "MIT"
    },
    "node_modules/hasown": {
      "version": "2.0.4",
      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
      "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
      "license": "MIT",
      "dependencies": {
        "function-bind": "^1.1.2"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/html-encoding-sniffer": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz",
      "integrity": "sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "whatwg-encoding": "^1.0.5"
      },
      "engines": {
        "node": ">=10"
      }
    },
    "node_modules/html-escaper": {
      "version": "2.0.2",
      "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
      "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
      "dev": true,
      "license": "MIT",
      "peer": true
    },
    "node_modules/http-errors": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
      "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "depd": "2.0.0",
        "inherits": "2.0.4",
        "setprototypeof": "1.2.0",
        "statuses": "2.0.1",
        "toidentifier": "1.0.1"
      },
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/http-proxy-agent": {
      "version": "4.0.1",
      "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz",
      "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "@tootallnate/once": "1",
        "agent-base": "6",
        "debug": "4"
      },
      "engines": {
        "node": ">= 6"
      }
    },
    "node_modules/https-proxy-agent": {
      "version": "5.0.1",
      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
      "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "agent-base": "6",
        "debug": "4"
      },
      "engines": {
        "node": ">= 6"
      }
    },
    "node_modules/human-signals": {
      "version": "2.1.0",
      "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
      "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
      "dev": true,
      "license": "Apache-2.0",
      "engines": {
        "node": ">=10.17.0"
      }
    },
    "node_modules/iconv-lite": {
      "version": "0.4.24",
      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
      "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "safer-buffer": ">= 2.1.2 < 3"
      },
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/icss-replace-symbols": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz",
      "integrity": "sha512-chIaY3Vh2mh2Q3RGXttaDIzeiPvaVXJ+C4DAh/w3c37SKZ/U6PGMmuicR2EQQp9bKG8zLMCl7I+PtIoOOPp8Gg==",
      "license": "ISC"
    },
    "node_modules/icss-utils": {
      "version": "5.1.0",
      "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz",
      "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==",
      "license": "ISC",
      "engines": {
        "node": "^10 || ^12 || >= 14"
      },
      "peerDependencies": {
        "postcss": "^8.1.0"
      }
    },
    "node_modules/ieee754": {
      "version": "1.2.1",
      "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
      "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
      "funding": [
        {
          "type": "github",
          "url": "https://github.com/sponsors/feross"
        },
        {
          "type": "patreon",
          "url": "https://www.patreon.com/feross"
        },
        {
          "type": "consulting",
          "url": "https://feross.org/support"
        }
      ],
      "license": "BSD-3-Clause"
    },
    "node_modules/immutable": {
      "version": "5.1.6",
      "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.6.tgz",
      "integrity": "sha512-q1swsS8K7L8usSHuOqF2TAoCCkonYz0SG38wLAggaa4Wml70zixIvt2ql4coQ2C2B3hTjltJry4r6bULwgAXLQ==",
      "devOptional": true,
      "license": "MIT"
    },
    "node_modules/import-local": {
      "version": "3.2.0",
      "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz",
      "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "pkg-dir": "^4.2.0",
        "resolve-cwd": "^3.0.0"
      },
      "bin": {
        "import-local-fixture": "fixtures/cli.js"
      },
      "engines": {
        "node": ">=8"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/imurmurhash": {
      "version": "0.1.4",
      "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
      "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "engines": {
        "node": ">=0.8.19"
      }
    },
    "node_modules/inflight": {
      "version": "1.0.6",
      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
      "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
      "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
      "dev": true,
      "license": "ISC",
      "peer": true,
      "dependencies": {
        "once": "^1.3.0",
        "wrappy": "1"
      }
    },
    "node_modules/inherits": {
      "version": "2.0.4",
      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
      "dev": true,
      "license": "ISC"
    },
    "node_modules/invert-kv": {
      "version": "3.0.1",
      "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-3.0.1.tgz",
      "integrity": "sha512-CYdFeFexxhv/Bcny+Q0BfOV+ltRlJcd4BBZBYFX/O0u4npJrgZtIcjokegtiSMAvlMTJ+Koq0GBCc//3bueQxw==",
      "license": "MIT",
      "engines": {
        "node": ">=8"
      },
      "funding": {
        "url": "https://github.com/sindresorhus/invert-kv?sponsor=1"
      }
    },
    "node_modules/ipaddr.js": {
      "version": "1.9.1",
      "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
      "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">= 0.10"
      }
    },
    "node_modules/is-arrayish": {
      "version": "0.2.1",
      "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
      "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
      "dev": true,
      "license": "MIT",
      "peer": true
    },
    "node_modules/is-binary-path": {
      "version": "2.1.0",
      "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
      "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
      "license": "MIT",
      "dependencies": {
        "binary-extensions": "^2.0.0"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/is-core-module": {
      "version": "2.16.2",
      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz",
      "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==",
      "license": "MIT",
      "dependencies": {
        "hasown": "^2.0.3"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/is-extglob": {
      "version": "2.1.1",
      "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
      "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
      "license": "MIT",
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/is-fullwidth-code-point": {
      "version": "3.0.0",
      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
      "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/is-function": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.2.tgz",
      "integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==",
      "license": "MIT"
    },
    "node_modules/is-generator-fn": {
      "version": "2.1.0",
      "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz",
      "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "engines": {
        "node": ">=6"
      }
    },
    "node_modules/is-glob": {
      "version": "4.0.3",
      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
      "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
      "license": "MIT",
      "dependencies": {
        "is-extglob": "^2.1.1"
      },
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/is-number": {
      "version": "7.0.0",
      "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
      "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
      "license": "MIT",
      "engines": {
        "node": ">=0.12.0"
      }
    },
    "node_modules/is-potential-custom-element-name": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
      "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
      "dev": true,
      "license": "MIT",
      "peer": true
    },
    "node_modules/is-stream": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
      "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=8"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/is-typedarray": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
      "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==",
      "dev": true,
      "license": "MIT",
      "peer": true
    },
    "node_modules/isbinaryfile": {
      "version": "5.0.2",
      "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.2.tgz",
      "integrity": "sha512-GvcjojwonMjWbTkfMpnVHVqXW/wKMYDfEpY94/8zy8HFMOqb/VL6oeONq9v87q4ttVlaTLnGXnJD4B5B1OTGIg==",
      "license": "MIT",
      "engines": {
        "node": ">= 18.0.0"
      },
      "funding": {
        "url": "https://github.com/sponsors/gjtorikian/"
      }
    },
    "node_modules/isexe": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
      "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
      "dev": true,
      "license": "ISC"
    },
    "node_modules/istanbul-lib-coverage": {
      "version": "3.2.2",
      "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
      "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==",
      "dev": true,
      "license": "BSD-3-Clause",
      "peer": true,
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/istanbul-lib-instrument": {
      "version": "5.2.1",
      "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz",
      "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==",
      "dev": true,
      "license": "BSD-3-Clause",
      "peer": true,
      "dependencies": {
        "@babel/core": "^7.12.3",
        "@babel/parser": "^7.14.7",
        "@istanbuljs/schema": "^0.1.2",
        "istanbul-lib-coverage": "^3.2.0",
        "semver": "^6.3.0"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/istanbul-lib-report": {
      "version": "3.0.1",
      "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
      "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
      "dev": true,
      "license": "BSD-3-Clause",
      "peer": true,
      "dependencies": {
        "istanbul-lib-coverage": "^3.0.0",
        "make-dir": "^4.0.0",
        "supports-color": "^7.1.0"
      },
      "engines": {
        "node": ">=10"
      }
    },
    "node_modules/istanbul-lib-report/node_modules/has-flag": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/istanbul-lib-report/node_modules/supports-color": {
      "version": "7.2.0",
      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "has-flag": "^4.0.0"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/istanbul-lib-source-maps": {
      "version": "4.0.1",
      "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz",
      "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==",
      "dev": true,
      "license": "BSD-3-Clause",
      "peer": true,
      "dependencies": {
        "debug": "^4.1.1",
        "istanbul-lib-coverage": "^3.0.0",
        "source-map": "^0.6.1"
      },
      "engines": {
        "node": ">=10"
      }
    },
    "node_modules/istanbul-reports": {
      "version": "3.2.0",
      "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz",
      "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==",
      "dev": true,
      "license": "BSD-3-Clause",
      "peer": true,
      "dependencies": {
        "html-escaper": "^2.0.0",
        "istanbul-lib-report": "^3.0.0"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/jest": {
      "version": "27.0.4",
      "resolved": "https://registry.npmjs.org/jest/-/jest-27.0.4.tgz",
      "integrity": "sha512-Px1iKFooXgGSkk1H8dJxxBIrM3tsc5SIuI4kfKYK2J+4rvCvPGr/cXktxh0e9zIPQ5g09kOMNfHQEmusBUf/ZA==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "@jest/core": "^27.0.4",
        "import-local": "^3.0.2",
        "jest-cli": "^27.0.4"
      },
      "bin": {
        "jest": "bin/jest.js"
      },
      "engines": {
        "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
      },
      "peerDependencies": {
        "node-notifier": "^8.0.1 || ^9.0.0"
      },
      "peerDependenciesMeta": {
        "node-notifier": {
          "optional": true
        }
      }
    },
    "node_modules/jest-changed-files": {
      "version": "27.5.1",
      "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-27.5.1.tgz",
      "integrity": "sha512-buBLMiByfWGCoMsLLzGUUSpAmIAGnbR2KJoMN10ziLhOLvP4e0SlypHnAel8iqQXTrcbmfEY9sSqae5sgUsTvw==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "@jest/types": "^27.5.1",
        "execa": "^5.0.0",
        "throat": "^6.0.1"
      },
      "engines": {
        "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
      }
    },
    "node_modules/jest-circus": {
      "version": "27.5.1",
      "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-27.5.1.tgz",
      "integrity": "sha512-D95R7x5UtlMA5iBYsOHFFbMD/GVA4R/Kdq15f7xYWUfWHBto9NYRsOvnSauTgdF+ogCpJ4tyKOXhUifxS65gdw==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "@jest/environment": "^27.5.1",
        "@jest/test-result": "^27.5.1",
        "@jest/types": "^27.5.1",
        "@types/node": "*",
        "chalk": "^4.0.0",
        "co": "^4.6.0",
        "dedent": "^0.7.0",
        "expect": "^27.5.1",
        "is-generator-fn": "^2.0.0",
        "jest-each": "^27.5.1",
        "jest-matcher-utils": "^27.5.1",
        "jest-message-util": "^27.5.1",
        "jest-runtime": "^27.5.1",
        "jest-snapshot": "^27.5.1",
        "jest-util": "^27.5.1",
        "pretty-format": "^27.5.1",
        "slash": "^3.0.0",
        "stack-utils": "^2.0.3",
        "throat": "^6.0.1"
      },
      "engines": {
        "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
      }
    },
    "node_modules/jest-circus/node_modules/ansi-styles": {
      "version": "4.3.0",
      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "color-convert": "^2.0.1"
      },
      "engines": {
        "node": ">=8"
      },
      "funding": {
        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
      }
    },
    "node_modules/jest-circus/node_modules/chalk": {
      "version": "4.1.2",
      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "ansi-styles": "^4.1.0",
        "supports-color": "^7.1.0"
      },
      "engines": {
        "node": ">=10"
      },
      "funding": {
        "url": "https://github.com/chalk/chalk?sponsor=1"
      }
    },
    "node_modules/jest-circus/node_modules/color-convert": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "color-name": "~1.1.4"
      },
      "engines": {
        "node": ">=7.0.0"
      }
    },
    "node_modules/jest-circus/node_modules/color-name": {
      "version": "1.1.4",
      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
      "dev": true,
      "license": "MIT",
      "peer": true
    },
    "node_modules/jest-circus/node_modules/has-flag": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/jest-circus/node_modules/supports-color": {
      "version": "7.2.0",
      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "has-flag": "^4.0.0"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/jest-cli": {
      "version": "27.5.1",
      "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-27.5.1.tgz",
      "integrity": "sha512-Hc6HOOwYq4/74/c62dEE3r5elx8wjYqxY0r0G/nFrLDPMFRu6RA/u8qINOIkvhxG7mMQ5EJsOGfRpI8L6eFUVw==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "@jest/core": "^27.5.1",
        "@jest/test-result": "^27.5.1",
        "@jest/types": "^27.5.1",
        "chalk": "^4.0.0",
        "exit": "^0.1.2",
        "graceful-fs": "^4.2.9",
        "import-local": "^3.0.2",
        "jest-config": "^27.5.1",
        "jest-util": "^27.5.1",
        "jest-validate": "^27.5.1",
        "prompts": "^2.0.1",
        "yargs": "^16.2.0"
      },
      "bin": {
        "jest": "bin/jest.js"
      },
      "engines": {
        "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
      },
      "peerDependencies": {
        "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0"
      },
      "peerDependenciesMeta": {
        "node-notifier": {
          "optional": true
        }
      }
    },
    "node_modules/jest-cli/node_modules/ansi-styles": {
      "version": "4.3.0",
      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "color-convert": "^2.0.1"
      },
      "engines": {
        "node": ">=8"
      },
      "funding": {
        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
      }
    },
    "node_modules/jest-cli/node_modules/chalk": {
      "version": "4.1.2",
      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "ansi-styles": "^4.1.0",
        "supports-color": "^7.1.0"
      },
      "engines": {
        "node": ">=10"
      },
      "funding": {
        "url": "https://github.com/chalk/chalk?sponsor=1"
      }
    },
    "node_modules/jest-cli/node_modules/color-convert": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "color-name": "~1.1.4"
      },
      "engines": {
        "node": ">=7.0.0"
      }
    },
    "node_modules/jest-cli/node_modules/color-name": {
      "version": "1.1.4",
      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
      "dev": true,
      "license": "MIT",   
      "peer": true
    },
    "node_modules/jest-cli/node_modules/has-flag": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/jest-cli/node_modules/supports-color": {
      "version": "7.2.0",
      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "has-flag": "^4.0.0"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/jest-config": {
      "version": "27.5.1",
      "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-27.5.1.tgz",
      "integrity": "sha512-5sAsjm6tGdsVbW9ahcChPAFCk4IlkQUknH5AvKjuLTSlcO/wCZKyFdn7Rg0EkC+OGgWODEy2hDpWB1PgzH0JNA==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "@babel/core": "^7.8.0",
        "@jest/test-sequencer": "^27.5.1",
        "@jest/types": "^27.5.1",
        "babel-jest": "^27.5.1",
        "chalk": "^4.0.0",
        "ci-info": "^3.2.0",
        "deepmerge": "^4.2.2",
        "glob": "^7.1.1",
        "graceful-fs": "^4.2.9",
        "jest-circus": "^27.5.1",
        "jest-environment-jsdom": "^27.5.1",
        "jest-environment-node": "^27.5.1",
        "jest-get-type": "^27.5.1",
        "jest-jasmine2": "^27.5.1",
        "jest-regex-util": "^27.5.1",
        "jest-resolve": "^27.5.1",
        "jest-runner": "^27.5.1",
        "jest-util": "^27.5.1",
        "jest-validate": "^27.5.1",
        "micromatch": "^4.0.4",
        "parse-json": "^5.2.0",
        "pretty-format": "^27.5.1",
        "slash": "^3.0.0",
        "strip-json-comments": "^3.1.1"
      },
      "engines": {
        "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
      },
      "peerDependencies": {
        "ts-node": ">=9.0.0"
      },
      "peerDependenciesMeta": {
        "ts-node": {
          "optional": true
        }
      }
    },
    "node_modules/jest-config/node_modules/ansi-styles": {
      "version": "4.3.0",
      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "color-convert": "^2.0.1"
      },
      "engines": {
        "node": ">=8"
      },
      "funding": {
        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
      }
    },
    "node_modules/jest-config/node_modules/chalk": {
      "version": "4.1.2",
      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "ansi-styles": "^4.1.0",
        "supports-color": "^7.1.0"
      },
      "engines": {
        "node": ">=10"
      },
      "funding": {
        "url": "https://github.com/chalk/chalk?sponsor=1"
      }
    },
    "node_modules/jest-config/node_modules/color-convert": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "color-name": "~1.1.4"
      },
      "engines": {
        "node": ">=7.0.0"
      }
    },
    "node_modules/jest-config/node_modules/color-name": {
      "version": "1.1.4",
      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
      "dev": true,
      "license": "MIT",
      "peer": true
    },
    "node_modules/jest-config/node_modules/has-flag": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/jest-config/node_modules/supports-color": {
      "version": "7.2.0",
      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "has-flag": "^4.0.0"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/jest-diff": {
      "version": "27.5.1",
      "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.5.1.tgz",
      "integrity": "sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "chalk": "^4.0.0",
        "diff-sequences": "^27.5.1",
        "jest-get-type": "^27.5.1",
        "pretty-format": "^27.5.1"
      },
      "engines": {
        "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
      }
    },
    "node_modules/jest-diff/node_modules/ansi-styles": {
      "version": "4.3.0",
      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "color-convert": "^2.0.1"
      },
      "engines": {
        "node": ">=8"
      },
      "funding": {
        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
      }
    },
    "node_modules/jest-diff/node_modules/chalk": {
      "version": "4.1.2",
      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "ansi-styles": "^4.1.0",
        "supports-color": "^7.1.0"
      },
      "engines": {
        "node": ">=10"
      },
      "funding": {
        "url": "https://github.com/chalk/chalk?sponsor=1"
      }
    },
    "node_modules/jest-diff/node_modules/color-convert": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "color-name": "~1.1.4"
      },
      "engines": {
        "node": ">=7.0.0"
      }
    },
    "node_modules/jest-diff/node_modules/color-name": {
      "version": "1.1.4",
      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
      "dev": true,
      "license": "MIT",
      "peer": true
    },
    "node_modules/jest-diff/node_modules/has-flag": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/jest-diff/node_modules/supports-color": {
      "version": "7.2.0",
      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "has-flag": "^4.0.0"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/jest-docblock": {
      "version": "27.5.1",
      "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-27.5.1.tgz",
      "integrity": "sha512-rl7hlABeTsRYxKiUfpHrQrG4e2obOiTQWfMEH3PxPjOtdsfLQO4ReWSZaQ7DETm4xu07rl4q/h4zcKXyU0/OzQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "detect-newline": "^3.0.0"
      },
      "engines": {
        "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
      }
    },
    "node_modules/jest-each": {
      "version": "27.5.1",
      "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-27.5.1.tgz",
      "integrity": "sha512-1Ff6p+FbhT/bXQnEouYy00bkNSY7OUpfIcmdl8vZ31A1UUaurOLPA8a8BbJOF2RDUElwJhmeaV7LnagI+5UwNQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "@jest/types": "^27.5.1",
        "chalk": "^4.0.0",
        "jest-get-type": "^27.5.1",
        "jest-util": "^27.5.1",
        "pretty-format": "^27.5.1"
      },
      "engines": {
        "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
      }
    },
    "node_modules/jest-each/node_modules/ansi-styles": {
      "version": "4.3.0",
      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "color-convert": "^2.0.1"
      },
      "engines": {
        "node": ">=8"
      },
      "funding": {
        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
      }
    },
    "node_modules/jest-each/node_modules/chalk": {
      "version": "4.1.2",
      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "ansi-styles": "^4.1.0",
        "supports-color": "^7.1.0"
      },
      "engines": {
        "node": ">=10"
      },
      "funding": {
        "url": "https://github.com/chalk/chalk?sponsor=1"
      }
    },
    "node_modules/jest-each/node_modules/color-convert": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "color-name": "~1.1.4"
      },
      "engines": {
        "node": ">=7.0.0"
      }
    },
    "node_modules/jest-each/node_modules/color-name": {
      "version": "1.1.4",
      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
      "dev": true,
      "license": "MIT",
      "peer": true
    },
    "node_modules/jest-each/node_modules/has-flag": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/jest-each/node_modules/supports-color": {
      "version": "7.2.0",
      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "has-flag": "^4.0.0"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/jest-environment-jsdom": {
      "version": "27.5.1",
      "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-27.5.1.tgz",
      "integrity": "sha512-TFBvkTC1Hnnnrka/fUb56atfDtJ9VMZ94JkjTbggl1PEpwrYtUBKMezB3inLmWqQsXYLcMwNoDQwoBTAvFfsfw==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "@jest/environment": "^27.5.1",
        "@jest/fake-timers": "^27.5.1",
        "@jest/types": "^27.5.1",
        "@types/node": "*",
        "jest-mock": "^27.5.1",
        "jest-util": "^27.5.1",
        "jsdom": "^16.6.0"
      },
      "engines": {
        "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
      }
    },
    "node_modules/jest-environment-node": {
      "version": "27.5.1",
      "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-27.5.1.tgz",
      "integrity": "sha512-Jt4ZUnxdOsTGwSRAfKEnE6BcwsSPNOijjwifq5sDFSA2kesnXTvNqKHYgM0hDq3549Uf/KzdXNYn4wMZJPlFLw==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "@jest/environment": "^27.5.1",
        "@jest/fake-timers": "^27.5.1",
        "@jest/types": "^27.5.1",
        "@types/node": "*",
        "jest-mock": "^27.5.1",
        "jest-util": "^27.5.1"
      },
      "engines": {
        "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
      }
    },
    "node_modules/jest-get-type": {
      "version": "27.5.1",
      "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz",
      "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "engines": {
        "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
      }
    },
    "node_modules/jest-haste-map": {
      "version": "27.5.1",
      "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-27.5.1.tgz",
      "integrity": "sha512-7GgkZ4Fw4NFbMSDSpZwXeBiIbx+t/46nJ2QitkOjvwPYyZmqttu2TDSimMHP1EkPOi4xUZAN1doE5Vd25H4Jng==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "@jest/types": "^27.5.1",
        "@types/graceful-fs": "^4.1.2",
        "@types/node": "*",
        "anymatch": "^3.0.3",
        "fb-watchman": "^2.0.0",
        "graceful-fs": "^4.2.9",
        "jest-regex-util": "^27.5.1",
        "jest-serializer": "^27.5.1",
        "jest-util": "^27.5.1",
        "jest-worker": "^27.5.1",
        "micromatch": "^4.0.4",
        "walker": "^1.0.7"
      },
      "engines": {
        "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
      },
      "optionalDependencies": {
        "fsevents": "^2.3.2"
      }
    },
    "node_modules/jest-jasmine2": {
      "version": "27.5.1",
      "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-27.5.1.tgz",
      "integrity": "sha512-jtq7VVyG8SqAorDpApwiJJImd0V2wv1xzdheGHRGyuT7gZm6gG47QEskOlzsN1PG/6WNaCo5pmwMHDf3AkG2pQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "@jest/environment": "^27.5.1",
        "@jest/source-map": "^27.5.1",
        "@jest/test-result": "^27.5.1",
        "@jest/types": "^27.5.1",
        "@types/node": "*",
        "chalk": "^4.0.0",
        "co": "^4.6.0",
        "expect": "^27.5.1",
        "is-generator-fn": "^2.0.0",
        "jest-each": "^27.5.1",
        "jest-matcher-utils": "^27.5.1",
        "jest-message-util": "^27.5.1",
        "jest-runtime": "^27.5.1",
        "jest-snapshot": "^27.5.1",
        "jest-util": "^27.5.1",
        "pretty-format": "^27.5.1",
        "throat": "^6.0.1"
      },
      "engines": {
        "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
      }
    },
    "node_modules/jest-jasmine2/node_modules/ansi-styles": {
      "version": "4.3.0",
      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "color-convert": "^2.0.1"
      },
      "engines": {
        "node": ">=8"
      },
      "funding": {
        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
      }
    },
    "node_modules/jest-jasmine2/node_modules/chalk": {
      "version": "4.1.2",
      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "ansi-styles": "^4.1.0",
        "supports-color": "^7.1.0"
      },
      "engines": {
        "node": ">=10"
      },
      "funding": {
        "url": "https://github.com/chalk/chalk?sponsor=1"
      }
    },
    "node_modules/jest-jasmine2/node_modules/color-convert": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "color-name": "~1.1.4"
      },
      "engines": {
        "node": ">=7.0.0"
      }
    },
    "node_modules/jest-jasmine2/node_modules/color-name": {
      "version": "1.1.4",
      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
      "dev": true,
      "license": "MIT",
      "peer": true
    },
    "node_modules/jest-jasmine2/node_modules/has-flag": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/jest-jasmine2/node_modules/supports-color": {
      "version": "7.2.0",
      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "has-flag": "^4.0.0"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/jest-leak-detector": {
      "version": "27.5.1",
      "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-27.5.1.tgz",
      "integrity": "sha512-POXfWAMvfU6WMUXftV4HolnJfnPOGEu10fscNCA76KBpRRhcMN2c8d3iT2pxQS3HLbA+5X4sOUPzYO2NUyIlHQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "jest-get-type": "^27.5.1",
        "pretty-format": "^27.5.1"
      },
      "engines": {
        "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
      }
    },
    "node_modules/jest-matcher-utils": {
      "version": "27.5.1",
      "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz",
      "integrity": "sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "chalk": "^4.0.0",
        "jest-diff": "^27.5.1",
        "jest-get-type": "^27.5.1",
        "pretty-format": "^27.5.1"
      },
      "engines": {
        "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
      }
    },
    "node_modules/jest-matcher-utils/node_modules/ansi-styles": {
      "version": "4.3.0",
      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "color-convert": "^2.0.1"
      },
      "engines": {
        "node": ">=8"
      },
      "funding": {
        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
      }
    },
    "node_modules/jest-matcher-utils/node_modules/chalk": {
      "version": "4.1.2",
      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "ansi-styles": "^4.1.0",
        "supports-color": "^7.1.0"
      },
      "engines": {
        "node": ">=10"
      },
      "funding": {
        "url": "https://github.com/chalk/chalk?sponsor=1"
      }
    },
    "node_modules/jest-matcher-utils/node_modules/color-convert": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "color-name": "~1.1.4"
      },
      "engines": {
        "node": ">=7.0.0"
      }
    },
    "node_modules/jest-matcher-utils/node_modules/color-name": {
      "version": "1.1.4",
      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
      "dev": true,
      "license": "MIT",
      "peer": true
    },
    "node_modules/jest-matcher-utils/node_modules/has-flag": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/jest-matcher-utils/node_modules/supports-color": {
      "version": "7.2.0",
      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "has-flag": "^4.0.0"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/jest-message-util": {
      "version": "27.5.1",
      "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-27.5.1.tgz",
      "integrity": "sha512-rMyFe1+jnyAAf+NHwTclDz0eAaLkVDdKVHHBFWsBWHnnh5YeJMNWWsv7AbFYXfK3oTqvL7VTWkhNLu1jX24D+g==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "@babel/code-frame": "^7.12.13",
        "@jest/types": "^27.5.1",
        "@types/stack-utils": "^2.0.0",
        "chalk": "^4.0.0",
        "graceful-fs": "^4.2.9",
        "micromatch": "^4.0.4",
        "pretty-format": "^27.5.1",
        "slash": "^3.0.0",
        "stack-utils": "^2.0.3"
      },
      "engines": {
        "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
      }
    },
    "node_modules/jest-message-util/node_modules/ansi-styles": {
      "version": "4.3.0",
      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "color-convert": "^2.0.1"
      },
      "engines": {
        "node": ">=8"
      },
      "funding": {
        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
      }
    },
    "node_modules/jest-message-util/node_modules/chalk": {
      "version": "4.1.2",
      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "ansi-styles": "^4.1.0",
        "supports-color": "^7.1.0"
      },
      "engines": {
        "node": ">=10"
      },
      "funding": {
        "url": "https://github.com/chalk/chalk?sponsor=1"
      }
    },
    "node_modules/jest-message-util/node_modules/color-convert": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "color-name": "~1.1.4"
      },
      "engines": {
        "node": ">=7.0.0"
      }
    },
    "node_modules/jest-message-util/node_modules/color-name": {
      "version": "1.1.4",
      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
      "dev": true,
      "license": "MIT",
      "peer": true
    },
    "node_modules/jest-message-util/node_modules/has-flag": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/jest-message-util/node_modules/supports-color": {
      "version": "7.2.0",
      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "has-flag": "^4.0.0"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/jest-mock": {
      "version": "27.5.1",
      "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-27.5.1.tgz",
      "integrity": "sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "@jest/types": "^27.5.1",
        "@types/node": "*"
      },
      "engines": {
        "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
      }
    },
    "node_modules/jest-pnp-resolver": {
      "version": "1.2.3",
      "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz",
      "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "engines": {
        "node": ">=6"
      },
      "peerDependencies": {
        "jest-resolve": "*"
      },
      "peerDependenciesMeta": {
        "jest-resolve": {
          "optional": true
        }
      }
    },
    "node_modules/jest-regex-util": {
      "version": "27.5.1",
      "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-27.5.1.tgz",
      "integrity": "sha512-4bfKq2zie+x16okqDXjXn9ql2B0dScQu+vcwe4TvFVhkVyuWLqpZrZtXxLLWoXYgn0E87I6r6GRYHF7wFZBUvg==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "engines": {
        "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
      }
    },
    "node_modules/jest-resolve": {
      "version": "27.5.1",
      "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-27.5.1.tgz",
      "integrity": "sha512-FFDy8/9E6CV83IMbDpcjOhumAQPDyETnU2KZ1O98DwTnz8AOBsW/Xv3GySr1mOZdItLR+zDZ7I/UdTFbgSOVCw==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "@jest/types": "^27.5.1",
        "chalk": "^4.0.0",
        "graceful-fs": "^4.2.9",
        "jest-haste-map": "^27.5.1",
        "jest-pnp-resolver": "^1.2.2",
        "jest-util": "^27.5.1",
        "jest-validate": "^27.5.1",
        "resolve": "^1.20.0",
        "resolve.exports": "^1.1.0",
        "slash": "^3.0.0"
      },
      "engines": {
        "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
      }
    },
    "node_modules/jest-resolve-dependencies": {
      "version": "27.5.1",
      "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-27.5.1.tgz",
      "integrity": "sha512-QQOOdY4PE39iawDn5rzbIePNigfe5B9Z91GDD1ae/xNDlu9kaat8QQ5EKnNmVWPV54hUdxCVwwj6YMgR2O7IOg==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "@jest/types": "^27.5.1",
        "jest-regex-util": "^27.5.1",
        "jest-snapshot": "^27.5.1"
      },
      "engines": {
        "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
      }
    },
    "node_modules/jest-resolve/node_modules/ansi-styles": {
      "version": "4.3.0",
      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "color-convert": "^2.0.1"
      },
      "engines": {
        "node": ">=8"
      },
      "funding": {
        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
      }
    },
    "node_modules/jest-resolve/node_modules/chalk": {
      "version": "4.1.2",
      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "ansi-styles": "^4.1.0",
        "supports-color": "^7.1.0"
      },
      "engines": {
        "node": ">=10"
      },
      "funding": {
        "url": "https://github.com/chalk/chalk?sponsor=1"
      }
    },
    "node_modules/jest-resolve/node_modules/color-convert": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "color-name": "~1.1.4"
      },
      "engines": {
        "node": ">=7.0.0"
      }
    },
    "node_modules/jest-resolve/node_modules/color-name": {
      "version": "1.1.4",
      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
      "dev": true,
      "license": "MIT",
      "peer": true
    },
    "node_modules/jest-resolve/node_modules/has-flag": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/jest-resolve/node_modules/supports-color": {
      "version": "7.2.0",
      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "has-flag": "^4.0.0"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/jest-runner": {
      "version": "27.5.1",
      "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-27.5.1.tgz",
      "integrity": "sha512-g4NPsM4mFCOwFKXO4p/H/kWGdJp9V8kURY2lX8Me2drgXqG7rrZAx5kv+5H7wtt/cdFIjhqYx1HrlqWHaOvDaQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "@jest/console": "^27.5.1",
        "@jest/environment": "^27.5.1",
        "@jest/test-result": "^27.5.1",
        "@jest/transform": "^27.5.1",
        "@jest/types": "^27.5.1",
        "@types/node": "*",
        "chalk": "^4.0.0",
        "emittery": "^0.8.1",
        "graceful-fs": "^4.2.9",
        "jest-docblock": "^27.5.1",
        "jest-environment-jsdom": "^27.5.1",
        "jest-environment-node": "^27.5.1",
        "jest-haste-map": "^27.5.1",
        "jest-leak-detector": "^27.5.1",
        "jest-message-util": "^27.5.1",
        "jest-resolve": "^27.5.1",
        "jest-runtime": "^27.5.1",
        "jest-util": "^27.5.1",
        "jest-worker": "^27.5.1",
        "source-map-support": "^0.5.6",
        "throat": "^6.0.1"
      },
      "engines": {
        "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
      }
    },
    "node_modules/jest-runner/node_modules/ansi-styles": {
      "version": "4.3.0",
      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "color-convert": "^2.0.1"
      },
      "engines": {
        "node": ">=8"
      },
      "funding": {
        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
      }
    },
    "node_modules/jest-runner/node_modules/chalk": {
      "version": "4.1.2",
      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "ansi-styles": "^4.1.0",
        "supports-color": "^7.1.0"
      },
      "engines": {
        "node": ">=10"
      },
      "funding": {
        "url": "https://github.com/chalk/chalk?sponsor=1"
      }
    },
    "node_modules/jest-runner/node_modules/color-convert": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "color-name": "~1.1.4"
      },
      "engines": {
        "node": ">=7.0.0"
      }
    },
    "node_modules/jest-runner/node_modules/color-name": {
      "version": "1.1.4",
      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
      "dev": true,
      "license": "MIT",
      "peer": true
    },
    "node_modules/jest-runner/node_modules/has-flag": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/jest-runner/node_modules/supports-color": {
      "version": "7.2.0",
      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "has-flag": "^4.0.0"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/jest-runtime": {
      "version": "27.5.1",
      "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-27.5.1.tgz",
      "integrity": "sha512-o7gxw3Gf+H2IGt8fv0RiyE1+r83FJBRruoA+FXrlHw6xEyBsU8ugA6IPfTdVyA0w8HClpbK+DGJxH59UrNMx8A==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "@jest/environment": "^27.5.1",
        "@jest/fake-timers": "^27.5.1",
        "@jest/globals": "^27.5.1",
        "@jest/source-map": "^27.5.1",
        "@jest/test-result": "^27.5.1",
        "@jest/transform": "^27.5.1",
        "@jest/types": "^27.5.1",
        "chalk": "^4.0.0",
        "cjs-module-lexer": "^1.0.0",
        "collect-v8-coverage": "^1.0.0",
        "execa": "^5.0.0",
        "glob": "^7.1.3",
        "graceful-fs": "^4.2.9",
        "jest-haste-map": "^27.5.1",
        "jest-message-util": "^27.5.1",
        "jest-mock": "^27.5.1",
        "jest-regex-util": "^27.5.1",
        "jest-resolve": "^27.5.1",
        "jest-snapshot": "^27.5.1",
        "jest-util": "^27.5.1",
        "slash": "^3.0.0",
        "strip-bom": "^4.0.0"
      },
      "engines": {
        "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
      }
    },
    "node_modules/jest-runtime/node_modules/ansi-styles": {
      "version": "4.3.0",
      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "color-convert": "^2.0.1"
      },
      "engines": {
        "node": ">=8"
      },
      "funding": {
        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
      }
    },
    "node_modules/jest-runtime/node_modules/chalk": {
      "version": "4.1.2",
      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "ansi-styles": "^4.1.0",
        "supports-color": "^7.1.0"
      },
      "engines": {
        "node": ">=10"
      },
      "funding": {
        "url": "https://github.com/chalk/chalk?sponsor=1"
      }
    },
    "node_modules/jest-runtime/node_modules/color-convert": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "color-name": "~1.1.4"
      },
      "engines": {
        "node": ">=7.0.0"
      }
    },
    "node_modules/jest-runtime/node_modules/color-name": {
      "version": "1.1.4",
      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
      "dev": true,
      "license": "MIT",
      "peer": true
    },
    "node_modules/jest-runtime/node_modules/has-flag": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/jest-runtime/node_modules/supports-color": {
      "version": "7.2.0",
      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "has-flag": "^4.0.0"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/jest-serializer": {
      "version": "27.5.1",
      "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-27.5.1.tgz",
      "integrity": "sha512-jZCyo6iIxO1aqUxpuBlwTDMkzOAJS4a3eYz3YzgxxVQFwLeSA7Jfq5cbqCY+JLvTDrWirgusI/0KwxKMgrdf7w==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "@types/node": "*",
        "graceful-fs": "^4.2.9"
      },
      "engines": {
        "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
      }
    },
    "node_modules/jest-snapshot": {
      "version": "27.5.1",
      "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-27.5.1.tgz",
      "integrity": "sha512-yYykXI5a0I31xX67mgeLw1DZ0bJB+gpq5IpSuCAoyDi0+BhgU/RIrL+RTzDmkNTchvDFWKP8lp+w/42Z3us5sA==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "@babel/core": "^7.7.2",
        "@babel/generator": "^7.7.2",
        "@babel/plugin-syntax-typescript": "^7.7.2",
        "@babel/traverse": "^7.7.2",
        "@babel/types": "^7.0.0",
        "@jest/transform": "^27.5.1",
        "@jest/types": "^27.5.1",
        "@types/babel__traverse": "^7.0.4",
        "@types/prettier": "^2.1.5",
        "babel-preset-current-node-syntax": "^1.0.0",
        "chalk": "^4.0.0",
        "expect": "^27.5.1",
        "graceful-fs": "^4.2.9",
        "jest-diff": "^27.5.1",
        "jest-get-type": "^27.5.1",
        "jest-haste-map": "^27.5.1",
        "jest-matcher-utils": "^27.5.1",
        "jest-message-util": "^27.5.1",
        "jest-util": "^27.5.1",
        "natural-compare": "^1.4.0",
        "pretty-format": "^27.5.1",
        "semver": "^7.3.2"
      },
      "engines": {
        "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
      }
    },
    "node_modules/jest-snapshot/node_modules/ansi-styles": {
      "version": "4.3.0",
      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "color-convert": "^2.0.1"
      },
      "engines": {
        "node": ">=8"
      },
      "funding": {
        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
      }
    },
    "node_modules/jest-snapshot/node_modules/chalk": {
      "version": "4.1.2",
      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "ansi-styles": "^4.1.0",
        "supports-color": "^7.1.0"
      },
      "engines": {
        "node": ">=10"
      },
      "funding": {
        "url": "https://github.com/chalk/chalk?sponsor=1"
      }
    },
    "node_modules/jest-snapshot/node_modules/color-convert": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "color-name": "~1.1.4"
      },
      "engines": {
        "node": ">=7.0.0"
      }
    },
    "node_modules/jest-snapshot/node_modules/color-name": {
      "version": "1.1.4",
      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
      "dev": true,
      "license": "MIT",
      "peer": true
    },
    "node_modules/jest-snapshot/node_modules/has-flag": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/jest-snapshot/node_modules/semver": {
      "version": "7.8.4",
      "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz",
      "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==",
      "dev": true,
      "license": "ISC",
      "peer": true,
      "bin": {
        "semver": "bin/semver.js"
      },
      "engines": {
        "node": ">=10"
      }
    },
    "node_modules/jest-snapshot/node_modules/supports-color": {
      "version": "7.2.0",
      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "has-flag": "^4.0.0"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/jest-util": {
      "version": "27.5.1",
      "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz",
      "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "@jest/types": "^27.5.1",
        "@types/node": "*",
        "chalk": "^4.0.0",
        "ci-info": "^3.2.0",
        "graceful-fs": "^4.2.9",
        "picomatch": "^2.2.3"
      },
      "engines": {
        "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
      }
    },
    "node_modules/jest-util/node_modules/ansi-styles": {
      "version": "4.3.0",
      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "color-convert": "^2.0.1"
      },
      "engines": {
        "node": ">=8"
      },
      "funding": {
        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
      }
    },
    "node_modules/jest-util/node_modules/chalk": {
      "version": "4.1.2",
      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "ansi-styles": "^4.1.0",
        "supports-color": "^7.1.0"
      },
      "engines": {
        "node": ">=10"
      },
      "funding": {
        "url": "https://github.com/chalk/chalk?sponsor=1"
      }
    },
    "node_modules/jest-util/node_modules/color-convert": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "color-name": "~1.1.4"
      },
      "engines": {
        "node": ">=7.0.0"
      }
    },
    "node_modules/jest-util/node_modules/color-name": {
      "version": "1.1.4",
      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
      "dev": true,
      "license": "MIT",
      "peer": true
    },
    "node_modules/jest-util/node_modules/has-flag": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/jest-util/node_modules/supports-color": {
      "version": "7.2.0",
      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "has-flag": "^4.0.0"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/jest-validate": {
      "version": "27.5.1",
      "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-27.5.1.tgz",
      "integrity": "sha512-thkNli0LYTmOI1tDB3FI1S1RTp/Bqyd9pTarJwL87OIBFuqEb5Apv5EaApEudYg4g86e3CT6kM0RowkhtEnCBQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "@jest/types": "^27.5.1",
        "camelcase": "^6.2.0",
        "chalk": "^4.0.0",
        "jest-get-type": "^27.5.1",
        "leven": "^3.1.0",
        "pretty-format": "^27.5.1"
      },
      "engines": {
        "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
      }
    },
    "node_modules/jest-validate/node_modules/ansi-styles": {
      "version": "4.3.0",
      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "color-convert": "^2.0.1"
      },
      "engines": {
        "node": ">=8"
      },
      "funding": {
        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
      }
    },
    "node_modules/jest-validate/node_modules/camelcase": {
      "version": "6.3.0",
      "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
      "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "engines": {
        "node": ">=10"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/jest-validate/node_modules/chalk": {
      "version": "4.1.2",
      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "ansi-styles": "^4.1.0",
        "supports-color": "^7.1.0"
      },
      "engines": {
        "node": ">=10"
      },
      "funding": {
        "url": "https://github.com/chalk/chalk?sponsor=1"
      }
    },
    "node_modules/jest-validate/node_modules/color-convert": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "color-name": "~1.1.4"
      },
      "engines": {
        "node": ">=7.0.0"
      }
    },
    "node_modules/jest-validate/node_modules/color-name": {
      "version": "1.1.4",
      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
      "dev": true,
      "license": "MIT",
      "peer": true
    },
    "node_modules/jest-validate/node_modules/has-flag": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/jest-validate/node_modules/supports-color": {
      "version": "7.2.0",
      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "has-flag": "^4.0.0"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/jest-watcher": {
      "version": "27.5.1",
      "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-27.5.1.tgz",
      "integrity": "sha512-z676SuD6Z8o8qbmEGhoEUFOM1+jfEiL3DXHK/xgEiG2EyNYfFG60jluWcupY6dATjfEsKQuibReS1djInQnoVw==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "@jest/test-result": "^27.5.1",
        "@jest/types": "^27.5.1",
        "@types/node": "*",
        "ansi-escapes": "^4.2.1",
        "chalk": "^4.0.0",
        "jest-util": "^27.5.1",
        "string-length": "^4.0.1"
      },
      "engines": {
        "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
      }
    },
    "node_modules/jest-watcher/node_modules/ansi-styles": {
      "version": "4.3.0",
      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "color-convert": "^2.0.1"
      },
      "engines": {
        "node": ">=8"
      },
      "funding": {
        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
      }
    },
    "node_modules/jest-watcher/node_modules/chalk": {
      "version": "4.1.2",
      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "ansi-styles": "^4.1.0",
        "supports-color": "^7.1.0"
      },
      "engines": {
        "node": ">=10"
      },
      "funding": {
        "url": "https://github.com/chalk/chalk?sponsor=1"
      }
    },
    "node_modules/jest-watcher/node_modules/color-convert": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "color-name": "~1.1.4"
      },
      "engines": {
        "node": ">=7.0.0"
      }
    },
    "node_modules/jest-watcher/node_modules/color-name": {
      "version": "1.1.4",
      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
      "dev": true,
      "license": "MIT",
      "peer": true
    },
    "node_modules/jest-watcher/node_modules/has-flag": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/jest-watcher/node_modules/supports-color": {
      "version": "7.2.0",
      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "has-flag": "^4.0.0"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/jest-worker": {
      "version": "27.5.1",
      "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz",
      "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "@types/node": "*",
        "merge-stream": "^2.0.0",
        "supports-color": "^8.0.0"
      },
      "engines": {
        "node": ">= 10.13.0"
      }
    },
    "node_modules/jest-worker/node_modules/has-flag": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/jest-worker/node_modules/supports-color": {
      "version": "8.1.1",
      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
      "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "has-flag": "^4.0.0"
      },
      "engines": {
        "node": ">=10"
      },
      "funding": {
        "url": "https://github.com/chalk/supports-color?sponsor=1"
      }
    },
    "node_modules/jimp": {
      "version": "0.10.3",
      "resolved": "https://registry.npmjs.org/jimp/-/jimp-0.10.3.tgz",
      "integrity": "sha512-meVWmDMtyUG5uYjFkmzu0zBgnCvvxwWNi27c4cg55vWNVC9ES4Lcwb+ogx+uBBQE3Q+dLKjXaLl0JVW+nUNwbQ==",
      "license": "MIT",
      "dependencies": {
        "@babel/runtime": "^7.7.2",
        "@jimp/custom": "^0.10.3",
        "@jimp/plugins": "^0.10.3",
        "@jimp/types": "^0.10.3",
        "core-js": "^3.4.1",
        "regenerator-runtime": "^0.13.3"
      }
    },
    "node_modules/jimp/node_modules/regenerator-runtime": {
      "version": "0.13.11",
      "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
      "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
      "license": "MIT"
    },
    "node_modules/jpeg-js": {
      "version": "0.3.7",
      "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.3.7.tgz",
      "integrity": "sha512-9IXdWudL61npZjvLuVe/ktHiA41iE8qFyLB+4VDTblEsWBzeg8WQTlktdUK4CdncUqtUgUg0bbOmTE2bKBKaBQ==",
      "license": "BSD-3-Clause"
    },
    "node_modules/js-tokens": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
      "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
      "license": "MIT"
    },
    "node_modules/js-yaml": {
      "version": "3.14.2",
      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
      "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "argparse": "^1.0.7",
        "esprima": "^4.0.0"
      },
      "bin": {
        "js-yaml": "bin/js-yaml.js"
      }
    },
    "node_modules/jsdom": {
      "version": "16.7.0",
      "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.7.0.tgz",
      "integrity": "sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "abab": "^2.0.5",
        "acorn": "^8.2.4",
        "acorn-globals": "^6.0.0",
        "cssom": "^0.4.4",
        "cssstyle": "^2.3.0",
        "data-urls": "^2.0.0",
        "decimal.js": "^10.2.1",
        "domexception": "^2.0.1",
        "escodegen": "^2.0.0",
        "form-data": "^3.0.0",
        "html-encoding-sniffer": "^2.0.1",
        "http-proxy-agent": "^4.0.1",
        "https-proxy-agent": "^5.0.0",
        "is-potential-custom-element-name": "^1.0.1",
        "nwsapi": "^2.2.0",
        "parse5": "6.0.1",
        "saxes": "^5.0.1",
        "symbol-tree": "^3.2.4",
        "tough-cookie": "^4.0.0",
        "w3c-hr-time": "^1.0.2",
        "w3c-xmlserializer": "^2.0.0",
        "webidl-conversions": "^6.1.0",
        "whatwg-encoding": "^1.0.5",
        "whatwg-mimetype": "^2.3.0",
        "whatwg-url": "^8.5.0",
        "ws": "^7.4.6",
        "xml-name-validator": "^3.0.0"
      },
      "engines": {
        "node": ">=10"
      },
      "peerDependencies": {
        "canvas": "^2.5.0"
      },
      "peerDependenciesMeta": {
        "canvas": {
          "optional": true
        }
      }
    },
    "node_modules/jsdom/node_modules/ws": {
      "version": "7.5.11",
      "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.11.tgz",
      "integrity": "sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "engines": {
        "node": ">=8.3.0"
      },
      "peerDependencies": {
        "bufferutil": "^4.0.1",
        "utf-8-validate": "^5.0.2"
      },
      "peerDependenciesMeta": {
        "bufferutil": {
          "optional": true
        },
        "utf-8-validate": {
          "optional": true
        }
      }
    },
    "node_modules/jsesc": {
      "version": "3.1.0",
      "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
      "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
      "license": "MIT",
      "bin": {
        "jsesc": "bin/jsesc"
      },
      "engines": {
        "node": ">=6"
      }
    },
    "node_modules/json-parse-even-better-errors": {
      "version": "2.3.1",
      "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
      "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
      "dev": true,
      "license": "MIT",
      "peer": true
    },
    "node_modules/json5": {
      "version": "2.2.3",
      "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
      "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
      "license": "MIT",
      "bin": {
        "json5": "lib/cli.js"
      },
      "engines": {
        "node": ">=6"
      }
    },
    "node_modules/jsonc-parser": {
      "version": "3.3.1",
      "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz",
      "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==",
      "license": "MIT"
    },
    "node_modules/jsonfile": {
      "version": "6.2.1",
      "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz",
      "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==",
      "license": "MIT",
      "dependencies": {
        "universalify": "^2.0.0"
      },
      "optionalDependencies": {
        "graceful-fs": "^4.1.6"
      }
    },
    "node_modules/kleur": {
      "version": "3.0.3",
      "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
      "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "engines": {
        "node": ">=6"
      }
    },
    "node_modules/lcid": {
      "version": "3.1.1",
      "resolved": "https://registry.npmjs.org/lcid/-/lcid-3.1.1.tgz",
      "integrity": "sha512-M6T051+5QCGLBQb8id3hdvIW8+zeFV2FyBGFS9IEK5H9Wt4MueD4bW1eWikpHgZp+5xR3l5c8pZUkQsIA0BFZg==",
      "license": "MIT",
      "dependencies": {
        "invert-kv": "^3.0.0"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/leven": {
      "version": "3.1.0",
      "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
      "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "engines": {
        "node": ">=6"
      }
    },
    "node_modules/licia": {
      "version": "1.41.1",
      "resolved": "h   ttps://registry.npmjs.org/licia/-/licia-1.41.1.tgz",
      "integrity": "sha512-XqObV8u1KEMdYWaNK0leRrTwhzKnLQEkhbnuUu7qGNH3zJoN7l9sfvF6PfHstSCuUOmpEP+0SBjRrk0I9uZs8g==",
      "license": "MIT"
    },
    "node_modules/lilconfig": {
      "version": "2.1.0",
      "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
      "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==",
      "license": "MIT",
      "engines": {
        "node": ">=10"
      }
    },
    "node_modules/lines-and-columns": {
      "version": "2.0.4",
      "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-2.0.4.tgz",
      "integrity": "sha512-wM1+Z03eypVAVUCE7QdSqpVIvelbOakn1M0bPDoA4SGWPx3sNDVUiMo3L6To6WWGClB7VyXnhQ4Sn7gxiJbE6A==",
      "license": "MIT",
      "engines": {
        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
      }
    },
    "node_modules/load-bmfont": {
      "version": "1.4.2",
      "resolved": "https://registry.npmjs.org/load-bmfont/-/load-bmfont-1.4.2.tgz",
      "integrity": "sha512-qElWkmjW9Oq1F9EI5Gt7aD9zcdHb9spJCW1L/dmPf7KzCCEJxq8nhHz5eCgI9aMf7vrG/wyaCqdsI+Iy9ZTlog==",
      "license": "MIT",
      "dependencies": {
        "buffer-equal": "0.0.1",
        "mime": "^1.3.4",
        "parse-bmfont-ascii": "^1.0.3",
        "parse-bmfont-binary": "^1.0.5",
        "parse-bmfont-xml": "^1.1.4",
        "phin": "^3.7.1",
        "xhr": "^2.0.1",
        "xtend": "^4.0.0"
      }
    },
    "node_modules/load-bmfont/node_modules/mime": {
      "version": "1.6.0",
      "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
      "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
      "license": "MIT",
      "bin": {
        "mime": "cli.js"
      },
      "engines": {
        "node": ">=4"
      }
    },
    "node_modules/load-bmfont/node_modules/phin": {
      "version": "3.7.1",
      "resolved": "https://registry.npmjs.org/phin/-/phin-3.7.1.tgz",
      "integrity": "sha512-GEazpTWwTZaEQ9RhL7Nyz0WwqilbqgLahDM3D0hxWwmVDI52nXEybHqiN6/elwpkJBhcuj+WbBu+QfT0uhPGfQ==",
      "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
      "license": "MIT",
      "dependencies": {
        "centra": "^2.7.0"
      },
      "engines": {
        "node": ">= 8"
      }
    },
    "node_modules/loader-utils": {
      "version": "3.3.1",
      "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.3.1.tgz",
      "integrity": "sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==",
      "license": "MIT",
      "engines": {
        "node": ">= 12.13.0"
      }
    },
    "node_modules/local-pkg": {
      "version": "1.2.1",
      "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.2.1.tgz",
      "integrity": "sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==",
      "license": "MIT",
      "dependencies": {
        "mlly": "^1.7.4",
        "pkg-types": "^2.3.0",
        "quansync": "^0.2.11"
      },
      "engines": {
        "node": ">=14"
      },
      "funding": {
        "url": "https://github.com/sponsors/antfu"
      }
    },
    "node_modules/local-pkg/node_modules/confbox": {
      "version": "0.2.4",
      "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz",
      "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==",
      "license": "MIT"
    },
    "node_modules/local-pkg/node_modules/pkg-types": {
      "version": "2.3.1",
      "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.1.tgz",
      "integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==",
      "license": "MIT",
      "dependencies": {
        "confbox": "^0.2.4",
        "exsolve": "^1.0.8",
        "pathe": "^2.0.3"
      }
    },
    "node_modules/localstorage-polyfill": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/localstorage-polyfill/-/localstorage-polyfill-1.0.1.tgz",
      "integrity": "sha512-m4iHVZxFH5734oQcPKU08025gIz2+4bjWR9lulP8ZYxEJR0BpA0w32oJmkzh8y3UI9ci7xCBehQDc3oA1X+VHw==",
      "license": "MIT",
      "engines": {
        "node": ">=6"
      }
    },
    "node_modules/locate-path": {
      "version": "5.0.0",
      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
      "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "p-locate": "^4.1.0"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/lodash": {
      "version": "4.18.1",
      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
      "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
      "dev": true,
      "license": "MIT",
      "peer": true
    },
    "node_modules/lodash.camelcase": {
      "version": "4.3.0",
      "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
      "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
      "license": "MIT"
    },
    "node_modules/lodash.debounce": {
      "version": "4.0.8",
      "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
      "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
      "dev": true,
      "license": "MIT"
    },
    "node_modules/lru-cache": {
      "version": "5.1.1",
      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
      "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
      "license": "ISC",
      "dependencies": {
        "yallist": "^3.0.2"
      }
    },
    "node_modules/magic-string": {
      "version": "0.30.11",
      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz",
      "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==",
      "license": "MIT",
      "dependencies": {
        "@jridgewell/sourcemap-codec": "^1.5.0"
      }
    },
    "node_modules/make-dir": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
      "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "semver": "^7.5.3"
      },
      "engines": {
        "node": ">=10"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/make-dir/node_modules/semver": {
      "version": "7.8.4",
      "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz",
      "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==",
      "dev": true,
      "license": "ISC",
      "peer": true,
      "bin": {
        "semver": "bin/semver.js"
      },
      "engines": {
        "node": ">=10"
      }
    },
    "node_modules/makeerror": {
      "version": "1.0.12",
      "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz",
      "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==",
      "dev": true,
      "license": "BSD-3-Clause",
      "peer": true,
      "dependencies": {
        "tmpl": "1.0.5"
      }
    },
    "node_modules/math-intrinsics": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
      "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/media-typer": {
      "version": "0.3.0",
      "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
      "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/meow": {
      "version": "13.2.0",
      "resolved": "https://registry.npmjs.org/meow/-/meow-13.2.0.tgz",
      "integrity": "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=18"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/merge": {
      "version": "2.1.1",
      "resolved": "https://registry.npmjs.org/merge/-/merge-2.1.1.tgz",
      "integrity": "sha512-jz+Cfrg9GWOZbQAnDQ4hlVnQky+341Yk5ru8bZSe6sIDTCIg8n9i/u7hSQGSVOF3C7lH6mGtqjkiT9G4wFLL0w==",
      "license": "MIT"
    },
    "node_modules/merge-descriptors": {
      "version": "1.0.3",
      "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
      "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
      "dev": true,
      "license": "MIT",
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/merge-stream": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
      "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
      "dev": true,
      "license": "MIT"
    },
    "node_modules/merge2": {
      "version": "1.4.1",
      "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
      "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
      "license": "MIT",
      "engines": {
        "node": ">= 8"
      }
    },
    "node_modules/methods": {
      "version": "1.1.2",
      "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
      "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/micromatch": {
      "version": "4.0.8",
      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
      "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
      "license": "MIT",
      "dependencies": {
        "braces": "^3.0.3",
        "picomatch": "^2.3.1"
      },
      "engines": {
        "node": ">=8.6"
      }
    },
    "node_modules/mime": {
      "version": "3.0.0",
      "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz",
      "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==",
      "license": "MIT",
      "bin": {
        "mime": "cli.js"
      },
      "engines": {
        "node": ">=10.0.0"
      }
    },
    "node_modules/mime-db": {
      "version": "1.52.0",
      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/mime-types": {
      "version": "2.1.35",
      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "mime-db": "1.52.0"
      },
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/mimic-fn": {
      "version": "2.1.0",
      "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
      "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=6"
      }
    },
    "node_modules/min-document": {
      "version": "2.19.2",
      "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.2.tgz",
      "integrity": "sha512-8S5I8db/uZN8r9HSLFVWPdJCvYOejMcEC82VIzNUc6Zkklf/d1gg2psfE79/vyhWOj4+J8MtwmoOz3TmvaGu5A==",
      "license": "MIT",
      "dependencies": {
        "dom-walk": "^0.1.0"
      }
    },
    "node_modules/minimatch": {
      "version": "3.1.5",
      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
      "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
      "dev": true,
      "license": "ISC",
      "peer": true,
      "dependencies": {
        "brace-expansion": "^1.1.7"
      },
      "engines": {
        "node": "*"
      }
    },
    "node_modules/minimist": {
      "version": "1.2.8",
      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
      "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
      "license": "MIT",
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/mkdirp": {
      "version": "0.5.6",
      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
      "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
      "license": "MIT",
      "dependencies": {
        "minimist": "^1.2.6"
      },
      "bin": {
        "mkdirp": "bin/cmd.js"
      }
    },
    "node_modules/mlly": {
      "version": "1.8.2",
      "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz",
      "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==",
      "license": "MIT",
      "dependencies": {
        "acorn": "^8.16.0",
        "pathe": "^2.0.3",
        "pkg-types": "^1.3.1",
        "ufo": "^1.6.3"
      }
    },
    "node_modules/module-alias": {
      "version": "2.2.3",
      "resolved": "https://registry.npmjs.org/module-alias/-/module-alias-2.2.3.tgz",
      "integrity": "sha512-23g5BFj4zdQL/b6tor7Ji+QY4pEfNH784BMslY9Qb0UnJWRAt+lQGLYmRaM0KDBwIG23ffEBELhZDP2rhi9f/Q==",
      "license": "MIT"
    },
    "node_modules/ms": {
      "version": "2.1.3",
      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
      "license": "MIT"
    },
    "node_modules/nanoid": {
      "version": "3.3.12",
      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
      "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
      "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/natural-compare": {
      "version": "1.4.0",
      "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
      "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
      "dev": true,
      "license": "MIT",
      "peer": true
    },
    "node_modules/negotiator": {
      "version": "0.6.3",
      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
      "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/node-addon-api": {
      "version": "7.1.1",
      "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
      "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
      "license": "MIT",
      "optional": true
    },
    "node_modules/node-int64": {
      "version": "0.4.0",
      "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
      "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==",
      "dev": true,
      "license": "MIT",
      "peer": true
    },
    "node_modules/node-releases": {
      "version": "2.0.47",
      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz",
      "integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==",
      "license": "MIT",
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/normalize-path": {
      "version": "3.0.0",
      "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
      "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
      "license": "MIT",
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/normalize-range": {
      "version": "0.1.2",
      "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
      "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==",
      "license": "MIT",
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/npm-run-path": {
      "version": "4.0.1",
      "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
      "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "path-key": "^3.0.0"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/nwsapi": {
      "version": "2.2.24",
      "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.24.tgz",
      "integrity": "sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==",
      "dev": true,
      "license": "MIT",
      "peer": true
    },
    "node_modules/object-inspect": {
      "version": "1.13.4",
      "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
      "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/omggif": {
      "version": "1.0.10",
      "resolved": "https://registry.npmjs.org/omggif/-/omggif-1.0.10.tgz",
      "integrity": "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==",
      "license": "MIT"
    },
    "node_modules/on-finished": {
      "version": "2.4.1",
      "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
      "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "ee-first": "1.1.1"
      },
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/once": {
      "version": "1.4.0",
      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
      "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
      "dev": true,
      "license": "ISC",
      "peer": true,
      "dependencies": {
        "wrappy": "1"
      }
    },
    "node_modules/onetime": {
      "version": "5.1.2",
      "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
      "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "mimic-fn": "^2.1.0"
      },
      "engines": {
        "node": ">=6"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/os-locale-s-fix": {
      "version": "1.0.8-fix-1",
      "resolved": "https://registry.npmjs.org/os-locale-s-fix/-/os-locale-s-fix-1.0.8-fix-1.tgz",
      "integrity": "sha512-Sv0OvhPiMutICiwORAUefv02DCPb62IelBmo8ZsSrRHyI3FStqIWZvjqDkvtjU+lcujo7UNir+dCwKSqlEQ/5w==",
      "license": "MIT",
      "dependencies": {
        "lcid": "^3.0.0"
      },
      "engines": {
        "node": ">=10",
        "yarn": "^1.22.4"
      }
    },
    "node_modules/p-limit": {
      "version": "2.3.0",
      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
      "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "p-try": "^2.0.0"
      },
      "engines": {
        "node": ">=6"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/p-locate": {
      "version": "4.1.0",
      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
      "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "p-limit": "^2.2.0"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/p-try": {
      "version": "2.2.0",
      "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
      "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "engines": {
        "node": ">=6"
      }
    },
    "node_modules/pako": {
      "version": "1.0.11",
      "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
      "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
      "license": "(MIT AND Zlib)"
    },
    "node_modules/parse-bmfont-ascii": {
      "version": "1.0.6",
      "resolved": "https://registry.npmjs.org/parse-bmfont-ascii/-/parse-bmfont-ascii-1.0.6.tgz",
      "integrity": "sha512-U4RrVsUFCleIOBsIGYOMKjn9PavsGOXxbvYGtMOEfnId0SVNsgehXh1DxUdVPLoxd5mvcEtvmKs2Mmf0Mpa1ZA==",
      "license": "MIT"
    },
    "node_modules/parse-bmfont-binary": {
      "version": "1.0.6",
      "resolved": "https://registry.npmjs.org/parse-bmfont-binary/-/parse-bmfont-binary-1.0.6.tgz",
      "integrity": "sha512-GxmsRea0wdGdYthjuUeWTMWPqm2+FAd4GI8vCvhgJsFnoGhTrLhXDDupwTo7rXVAgaLIGoVHDZS9p/5XbSqeWA==",
      "license": "MIT"
    },
    "node_modules/parse-bmfont-xml": {
      "version": "1.1.6",
      "resolved": "https://registry.npmjs.org/parse-bmfont-xml/-/parse-bmfont-xml-1.1.6.tgz",
      "integrity": "sha512-0cEliVMZEhrFDwMh4SxIyVJpqYoOWDJ9P895tFuS+XuNzI5UBmBk5U5O4KuJdTnZpSBI4LFA2+ZiJaiwfSwlMA==",
      "license": "MIT",
      "dependencies": {
        "xml-parse-from-string": "^1.0.0",
        "xml2js": "^0.5.0"
      }
    },
    "node_modules/parse-css-font": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/parse-css-font/-/parse-css-font-4.0.0.tgz",
      "integrity": "sha512-lnY7dTUfjRXsSo5G5C639L8RaBBaVSgL+5hacIFKsNHzeCJQ5SFSZv1DZmc7+wZv/22PFGOq2YbaEHLdaCS/mQ==",
      "license": "MIT",
      "dependencies": {
        "css-font-size-keywords": "^1.0.0",
        "css-font-stretch-keywords": "^1.0.1",
        "css-font-style-keywords": "^1.0.1",
        "css-font-weight-keywords": "^1.0.0",
        "css-list-helpers": "^2.0.0",
        "css-system-font-keywords": "^1.0.0",
        "unquote": "^1.1.1"
      }
    },
    "node_modules/parse-headers": {
      "version": "2.0.6",
      "resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.6.tgz",
      "integrity": "sha512-Tz11t3uKztEW5FEVZnj1ox8GKblWn+PvHY9TmJV5Mll2uHEwRdR/5Li1OlXoECjLYkApdhWy44ocONwXLiKO5A==",
      "license": "MIT"
    },
    "node_modules/parse-json": {
      "version": "5.2.0",
      "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
      "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "@babel/code-frame": "^7.0.0",
        "error-ex": "^1.3.1",
        "json-parse-even-better-errors": "^2.3.0",
        "lines-and-columns": "^1.1.6"
      },
      "engines": {
        "node": ">=8"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/parse-json/node_modules/lines-and-columns": {
      "version": "1.2.4",
      "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
      "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
      "dev": true,
      "license": "MIT",
      "peer": true
    },
    "node_modules/parse5": {
      "version": "6.0.1",
      "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz",
      "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==",
      "dev": true,
      "license": "MIT",
      "peer": true
    },
    "node_modules/parseurl": {
      "version": "1.3.3",
      "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
      "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/path-exists": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
      "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/path-is-absolute": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
      "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/path-key": {
      "version": "3.1.1",
      "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
      "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/path-parse": {
      "version": "1.0.7",
      "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
      "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
      "license": "MIT"
    },
    "node_modules/path-to-regexp": {
      "version": "0.1.10",
      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
      "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==",
      "dev": true,
      "license": "MIT"
    },
    "node_modules/pathe": {
      "version": "2.0.3",
      "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
      "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
      "license": "MIT"
    },
    "node_modules/phin": {
      "version": "2.9.3",
      "resolved": "https://registry.npmjs.org/phin/-/phin-2.9.3.tgz",
      "integrity": "sha512-CzFr90qM24ju5f88quFC/6qohjC144rehe5n6DH900lgXmUe86+xCKc10ev56gRKC4/BkHUoG4uSiQgBiIXwDA==",
      "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
      "license": "MIT"
    },
    "node_modules/picocolors": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
      "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==",
      "license": "ISC"
    },
    "node_modules/picomatch": {
      "version": "2.3.2",
      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
      "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
      "license": "MIT",
      "engines": {
        "node": ">=8.6"
      },
      "funding": {
        "url": "https://github.com/sponsors/jonschlinkert"
      }
    },
    "node_modules/pify": {
      "version": "2.3.0",
      "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
      "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
      "license": "MIT",
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/pirates": {
      "version": "4.0.7",
      "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
      "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "engines": {
        "node": ">= 6"
      }
    },
    "node_modules/pixelmatch": {
      "version": "4.0.2",
      "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-4.0.2.tgz",
      "integrity": "sha512-J8B6xqiO37sU/gkcMglv6h5Jbd9xNER7aHzpfRdNmV4IbQBzBpe4l9XmbG+xPF/znacgu2jfEw+wHffaq/YkXA==",
      "license": "ISC",
      "dependencies": {
        "pngjs": "^3.0.0"
      },
      "bin": {
        "pixelmatch": "bin/pixelmatch"
      }
    },
    "node_modules/pkg-dir": {
      "version": "4.2.0",
      "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
      "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "find-up": "^4.0.0"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/pkg-types": {
      "version": "1.3.1",
      "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz",
      "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==",
      "license": "MIT",
      "dependencies": {
        "confbox": "^0.1.8",
        "mlly": "^1.7.4",
        "pathe": "^2.0.1"
      }
    },
    "node_modules/pngjs": {
      "version": "3.4.0",
      "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz",
      "integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==",
      "license": "MIT",
      "engines": {
        "node": ">=4.0.0"
      }
    },
    "node_modules/postcss": {
      "version": "8.5.15",
      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
      "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
      "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.12",
        "picocolors": "^1.1.1",
        "source-map-js": "^1.2.1"
      },
      "engines": {
        "node": "^10 || ^12 || >=14"
      }
    },
    "node_modules/postcss-import": {
      "version": "14.1.0",
      "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-14.1.0.tgz",
      "integrity": "sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==",
      "license": "MIT",
      "dependencies": {
        "postcss-value-parser": "^4.0.0",
        "read-cache": "^1.0.0",
        "resolve": "^1.1.7"
      },
      "engines": {
        "node": ">=10.0.0"
      },
      "peerDependencies": {
        "postcss": "^8.0.0"
      }
    },
    "node_modules/postcss-load-config": {
      "version": "3.1.4",
      "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz",
      "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==",
      "license": "MIT",
      "dependencies": {
        "lilconfig": "^2.0.5",
        "yaml": "^1.10.2"
      },
      "engines": {
        "node": ">= 10"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/postcss/"
      },
      "peerDependencies": {
        "postcss": ">=8.0.9",
        "ts-node": ">=9.0.0"
      },
      "peerDependenciesMeta": {
        "postcss": {
          "optional": true
        },
        "ts-node": {
          "optional": true
        }
      }
    },
    "node_modules/postcss-modules": {
      "version": "4.3.1",
      "resolved": "https://registry.npmjs.org/postcss-modules/-/postcss-modules-4.3.1.tgz",
      "integrity": "sha512-ItUhSUxBBdNamkT3KzIZwYNNRFKmkJrofvC2nWab3CPKhYBQ1f27XXh1PAPE27Psx58jeelPsxWB/+og+KEH0Q==",
      "license": "MIT",
      "dependencies": {
        "generic-names": "^4.0.0",
        "icss-replace-symbols": "^1.1.0",
        "lodash.camelcase": "^4.3.0",
        "postcss-modules-extract-imports": "^3.0.0",
        "postcss-modules-local-by-default": "^4.0.0",
        "postcss-modules-scope": "^3.0.0",
        "postcss-modules-values": "^4.0.0",
        "string-hash": "^1.1.1"
      },
      "peerDependencies": {
        "postcss": "^8.0.0"
      }
    },
    "node_modules/postcss-modules-extract-imports": {
      "version": "3.1.0",
      "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz",
      "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==",
      "license": "ISC",
      "engines": {
        "node": "^10 || ^12 || >= 14"
      },
      "peerDependencies": {
        "postcss": "^8.1.0"
      }
    },
    "node_modules/postcss-modules-local-by-default": {
      "version": "4.2.0",
      "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz",
      "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==",
      "license": "MIT",
      "dependencies": {
        "icss-utils": "^5.0.0",
        "postcss-selector-parser": "^7.0.0",
        "postcss-value-parser": "^4.1.0"
      },
      "engines": {
        "node": "^10 || ^12 || >= 14"
      },
      "peerDependencies": {
        "postcss": "^8.1.0"
      }
    },
    "node_modules/postcss-modules-local-by-default/node_modules/postcss-selector-parser": {
      "version": "7.1.4",
      "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.4.tgz",
      "integrity": "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==",
      "license": "MIT",
      "dependencies": {
        "cssesc": "^3.0.0",
        "util-deprecate": "^1.0.2"
      },
      "engines": {
        "node": ">=4"
      }
    },
    "node_modules/postcss-modules-scope": {
      "version": "3.2.1",
      "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz",
      "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==",
      "license": "ISC",
      "dependencies": {
        "postcss-selector-parser": "^7.0.0"
      },
      "engines": {
        "node": "^10 || ^12 || >= 14"
      },
      "peerDependencies": {
        "postcss": "^8.1.0"
      }
    },
    "node_modules/postcss-modules-scope/node_modules/postcss-selector-parser": {
      "version": "7.1.4",
      "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.4.tgz",
      "integrity": "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==",
      "license": "MIT",
      "dependencies": {
        "cssesc": "^3.0.0",
        "util-deprecate": "^1.0.2"
      },
      "engines": {
        "node": ">=4"
      }
    },
    "node_modules/postcss-modules-values": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz",
      "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==",
      "license": "ISC",
      "dependencies": {
        "icss-utils": "^5.0.0"
      },
      "engines": {
        "node": "^10 || ^12 || >= 14"
      },
      "peerDependencies": {
        "postcss": "^8.1.0"
      }
    },
    "node_modules/postcss-selector-parser": {
      "version": "6.1.2",
      "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
      "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
      "license": "MIT",
      "dependencies": {
        "cssesc": "^3.0.0",
        "util-deprecate": "^1.0.2"
      },
      "engines": {
        "node": ">=4"
      }
    },
    "node_modules/postcss-value-parser": {
      "version": "4.2.0",
      "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
      "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
      "license": "MIT"
    },
    "node_modules/postcss/node_modules/picocolors": {
      "version": "1.1.1",
      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
      "license": "ISC"
    },
    "node_modules/pretty-format": {
      "version": "27.5.1",
      "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
      "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "ansi-regex": "^5.0.1",
        "ansi-styles": "^5.0.0",
        "react-is": "^17.0.1"
      },
      "engines": {
        "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
      }
    },
    "node_modules/pretty-format/node_modules/ansi-styles": {
      "version": "5.2.0",
      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
      "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "engines": {
        "node": ">=10"
      },
      "funding": {
        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
      }
    },
    "node_modules/process": {
      "version": "0.11.10",
      "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
      "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
      "license": "MIT",
      "engines": {
        "node": ">= 0.6.0"
      }
    },
    "node_modules/prompts": {
      "version": "2.4.2",
      "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
      "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "kleur": "^3.0.3",
        "sisteransi": "^1.0.5"
      },
      "engines": {
        "node": ">= 6"
      }
    },
    "node_modules/proxy-addr": {
      "version": "2.0.7",
      "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
      "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "forwarded": "0.2.0",
        "ipaddr.js": "1.9.1"
      },
      "engines": {
        "node": ">= 0.10"
      }
    },
    "node_modules/psl": {
      "version": "1.15.0",
      "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz",
      "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "punycode": "^2.3.1"
      },
      "funding": {
        "url": "https://github.com/sponsors/lupomontero"
      }
    },
    "node_modules/punycode": {
      "version": "2.3.1",
      "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
      "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "engines": {
        "node": ">=6"
      }
    },
    "node_modules/qrcode-reader": {
      "version": "1.0.4",
      "resolved": "https://registry.npmjs.org/qrcode-reader/-/qrcode-reader-1.0.4.tgz",
      "integrity": "sha512-rRjALGNh9zVqvweg1j5OKIQKNsw3bLC+7qwlnead5K/9cb1cEIAGkwikt/09U0K+2IDWGD9CC6SP7tHAjUeqvQ==",
      "license": "Apache-2.0"
    },
    "node_modules/qrcode-terminal": {
      "version": "0.12.0",
      "resolved": "https://registry.npmjs.org/qrcode-terminal/-/qrcode-terminal-0.12.0.tgz",
      "integrity": "sha512-EXtzRZmC+YGmGlDFbXKxQiMZNwCLEO6BANKXG4iCtSIM0yqc/pappSx3RIKr4r0uh5JsBckOXeKrB3Iz7mdQpQ==",
      "bin": {
        "qrcode-terminal": "bin/qrcode-terminal.js"
      }
    },
    "node_modules/qs": {
      "version": "6.11.0",
      "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
      "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
      "dev": true,
      "license": "BSD-3-Clause",
      "dependencies": {
        "side-channel": "^1.0.4"
      },
      "engines": {
        "node": ">=0.6"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/quansync": {
      "version": "0.2.11",
      "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz",
      "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==",
      "funding": [
        {
          "type": "individual",
          "url": "https://github.com/sponsors/antfu"
        },
        {
          "type": "individual",
          "url": "https://github.com/sponsors/sxzz"
        }
      ],
      "license": "MIT"
    },
    "node_modules/querystringify": {
      "version": "2.2.0",
      "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
      "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
      "dev": true,
      "license": "MIT",
      "peer": true
    },
    "node_modules/queue-microtask": {
      "version": "1.2.3",
      "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
      "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
      "funding": [
        {
          "type": "github",
          "url": "https://github.com/sponsors/feross"
        },
        {
          "type": "patreon",
          "url": "https://www.patreon.com/feross"
        },
        {
          "type": "consulting",
          "url": "https://feross.org/support"
        }
      ],
      "license": "MIT"
    },
    "node_modules/range-parser": {
      "version": "1.2.1",
      "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
      "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/raw-body": {
      "version": "2.5.2",
      "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
      "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "bytes": "3.1.2",
        "http-errors": "2.0.0",
        "iconv-lite": "0.4.24",
        "unpipe": "1.0.0"
      },
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/react-is": {
      "version": "17.0.2",
      "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
      "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
      "dev": true,
      "license": "MIT",
      "peer": true
    },
    "node_modules/read-cache": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
      "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
      "license": "MIT",
      "dependencies": {
        "pify": "^2.3.0"
      }
    },
    "node_modules/readdirp": {
      "version": "3.6.0",
      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
      "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
      "license": "MIT",
      "dependencies": {
        "picomatch": "^2.2.1"
      },
      "engines": {
        "node": ">=8.10.0"
      }
    },
    "node_modules/regenerate": {
      "version": "1.4.2",
      "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
      "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==",
      "dev": true,
      "license": "MIT"
    },
    "node_modules/regenerate-unicode-properties": {
      "version": "10.2.2",
      "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz",
      "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "regenerate": "^1.4.2"
      },
      "engines": {
        "node": ">=4"
      }
    },
    "node_modules/regenerator-runtime": {
      "version": "0.14.1",
      "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
      "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
      "dev": true,
      "license": "MIT"
    },
    "node_modules/regexpu-core": {
      "version": "6.4.0",
      "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz",
      "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "regenerate": "^1.4.2",
        "regenerate-unicode-properties": "^10.2.2",
        "regjsgen": "^0.8.0",
        "regjsparser": "^0.13.0",
        "unicode-match-property-ecmascript": "^2.0.0",
        "unicode-match-property-value-ecmascript": "^2.2.1"
      },
      "engines": {
        "node": ">=4"
      }
    },
    "node_modules/regjsgen": {
      "version": "0.8.0",
      "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz",
      "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==",
      "dev": true,
      "license": "MIT"
    },
    "node_modules/regjsparser": {
      "version": "0.13.2",
      "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.2.tgz",
      "integrity": "sha512-NgRBy2Nx/bE+9F27nVHnqcN5HjyLmecqsqx2PJHu3/IEtADD4WuxuXIVExD5PoSDFVrl78dOonfcOe5O+5nbzQ==",
      "dev": true,
      "license": "BSD-2-Clause",
      "dependencies": {
        "jsesc": "~3.1.0"
      },
      "bin": {
        "regjsparser": "bin/parser"
      }
    },
    "node_modules/require-directory": {
      "version": "2.1.1",
      "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
      "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/requires-port": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
      "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
      "dev": true,
      "license": "MIT",
      "peer": true
    },
    "node_modules/resolve": {
      "version": "1.22.8",
      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
      "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
      "license": "MIT",
      "dependencies": {
        "is-core-module": "^2.13.0",
        "path-parse": "^1.0.7",
        "supports-preserve-symlinks-flag": "^1.0.0"
      },
      "bin": {
        "resolve": "bin/resolve"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/resolve-cwd": {
      "version": "3.0.0",
      "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz",
      "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "resolve-from": "^5.0.0"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/resolve-from": {
      "version": "5.0.0",
      "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
      "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/resolve.exports": {
      "version": "1.1.1",
      "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-1.1.1.tgz",
      "integrity": "sha512-/NtpHNDN7jWhAaQ9BvBUYZ6YTXsRBgfqWFWP7BZBaoMJO/I3G5OFzvTuWNlZC3aPjins1F+TNrLKsGbH4rfsRQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "engines": {
        "node": ">=10"
      }
    },
    "node_modules/reusify": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
      "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
      "license": "MIT",
      "engines": {
        "iojs": ">=1.0.0",
        "node": ">=0.10.0"
      }
    },
    "node_modules/rimraf": {
      "version": "3.0.2",
      "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
      "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
      "deprecated": "Rimraf versions prior to v4 are no longer supported",
      "dev": true,
      "license": "ISC",
      "peer": true,
      "dependencies": {
        "glob": "^7.1.3"
      },
      "bin": {
        "rimraf": "bin.js"
      },
      "funding": {
        "url": "https://github.com/sponsors/isaacs"
      }
    },
    "node_modules/rollup": {
      "version": "4.62.0",
      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.0.tgz",
      "integrity": "sha512-nc72Wgq62I7rtDV4izT5/aaS0zxy3kttkinf9586ApknY3jZO9NYsmtc24fUckA0X7Q2v+ML4a15pdUlV5V/jA==",
      "license": "MIT",
      "dependencies": {
        "@types/estree": "1.0.9"
      },
      "bin": {
        "rollup": "dist/bin/rollup"
      },
      "engines": {
        "node": ">=18.0.0",
        "npm": ">=8.0.0"
      },
      "optionalDependencies": {
        "@rollup/rollup-android-arm-eabi": "4.62.0",
        "@rollup/rollup-android-arm64": "4.62.0",
        "@rollup/rollup-darwin-arm64": "4.62.0",
        "@rollup/rollup-darwin-x64": "4.62.0",
        "@rollup/rollup-freebsd-arm64": "4.62.0",
        "@rollup/rollup-freebsd-x64": "4.62.0",
        "@rollup/rollup-linux-arm-gnueabihf": "4.62.0",
        "@rollup/rollup-linux-arm-musleabihf": "4.62.0",
        "@rollup/rollup-linux-arm64-gnu": "4.62.0",
        "@rollup/rollup-linux-arm64-musl": "4.62.0",
        "@rollup/rollup-linux-loong64-gnu": "4.62.0",
        "@rollup/rollup-linux-loong64-musl": "4.62.0",
        "@rollup/rollup-linux-ppc64-gnu": "4.62.0",
        "@rollup/rollup-linux-ppc64-musl": "4.62.0",
        "@rollup/rollup-linux-riscv64-gnu": "4.62.0",
        "@rollup/rollup-linux-riscv64-musl": "4.62.0",
        "@rollup/rollup-linux-s390x-gnu": "4.62.0",
        "@rollup/rollup-linux-x64-gnu": "4.62.0",
        "@rollup/rollup-linux-x64-musl": "4.62.0",
        "@rollup/rollup-openbsd-x64": "4.62.0",
        "@rollup/rollup-openharmony-arm64": "4.62.0",
        "@rollup/rollup-win32-arm64-msvc": "4.62.0",
        "@rollup/rollup-win32-ia32-msvc": "4.62.0",
        "@rollup/rollup-win32-x64-gnu": "4.62.0",
        "@rollup/rollup-win32-x64-msvc": "4.62.0",
        "fsevents": "~2.3.2"
      }
    },
    "node_modules/run-parallel": {
      "version": "1.2.0",
      "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
      "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
      "funding": [
        {
          "type": "github",
          "url": "https://github.com/sponsors/feross"
        },
        {
          "type": "patreon",
          "url": "https://www.patreon.com/feross"
        },
        {
          "type": "consulting",
          "url": "https://feross.org/support"
        }
      ],
      "license": "MIT",
      "dependencies": {
        "queue-microtask": "^1.2.2"
      }
    },
    "node_modules/safe-area-insets": {
      "version": "1.4.1",
      "resolved": "https://registry.npmjs.org/safe-area-insets/-/safe-area-insets-1.4.1.tgz",
      "integrity": "sha512-r/nRWTjFGhhm3w1Z6Kd/jY11srN+lHt2mNl1E/emQGW8ic7n3Avu4noibklfSM+Y34peNphHD/BSZecav0sXYQ==",
      "license": "ISC"
    },
    "node_modules/safe-buffer": {
      "version": "5.2.1",
      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
      "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
      "dev": true,
      "funding": [
        {
          "type": "github",
          "url": "https://github.com/sponsors/feross"
        },
        {
          "type": "patreon",
          "url": "https://www.patreon.com/feross"
        },
        {
          "type": "consulting",
          "url": "https://feross.org/support"
        }
      ],
      "license": "MIT"
    },
    "node_modules/safer-buffer": {
      "version": "2.1.2",
      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
      "dev": true,
      "license": "MIT"
    },
    "node_modules/sass": {
      "version": "1.101.0",
      "resolved": "https://registry.npmjs.org/sass/-/sass-1.101.0.tgz",
      "integrity": "sha512-OL3GoQyoUdDt843DpVmDO6y2k1sc5IhUDSpu8XucEI+35neq5QivZ1iuegnpraEVTJXlQGK1gl27zKcTLEPbQw==",
      "devOptional": true,
      "license": "MIT",
      "dependencies": {
        "chokidar": "^5.0.0",
        "immutable": "^5.1.5",
        "source-map-js": ">=0.6.2 <2.0.0"
      },
      "bin": {
        "sass": "sass.js"
      },
      "engines": {
        "node": ">=20.19.0"
      },
      "optionalDependencies": {
        "@parcel/watcher": "^2.4.1"
      }
    },
    "node_modules/sass/node_modules/chokidar": {
      "version": "5.0.0",
      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
      "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
      "devOptional": true,
      "license": "MIT",
      "dependencies": {
        "readdirp": "^5.0.0"
      },
      "engines": {
        "node": ">= 20.19.0"
      },
      "funding": {
        "url": "https://paulmillr.com/funding/"
      }
    },
    "node_modules/sass/node_modules/readdirp": {
      "version": "5.0.0",
      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz",
      "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==",
      "devOptional": true,
      "license": "MIT",
      "engines": {
        "node": ">= 20.19.0"
      },
      "funding": {
        "type": "individual",
        "url": "https://paulmillr.com/funding/"
      }
    },
    "node_modules/sax": {
      "version": "1.6.0",
      "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz",
      "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==",
      "license": "BlueOak-1.0.0",
      "engines": {
        "node": ">=11.0.0"
      }
    },
    "node_modules/saxes": {
      "version": "5.0.1",
      "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz",
      "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==",
      "dev": true,
      "license": "ISC",
      "peer": true,
      "dependencies": {
        "xmlchars": "^2.2.0"
      },
      "engines": {
        "node": ">=10"
      }
    },
    "node_modules/scule": {
      "version": "1.3.0",
      "resolved": "https://registry.npmjs.org/scule/-/scule-1.3.0.tgz",
      "integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==",
      "license": "MIT"
    },
    "node_modules/semver": {
      "version": "6.3.1",
      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
      "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
      "license": "ISC",
      "bin": {
        "semver": "bin/semver.js"
      }
    },
    "node_modules/send": {
      "version": "0.19.0",
      "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
      "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "debug": "2.6.9",
        "depd": "2.0.0",
        "destroy": "1.2.0",
        "encodeurl": "~1.0.2",
        "escape-html": "~1.0.3",
        "etag": "~1.8.1",
        "fresh": "0.5.2",
        "http-errors": "2.0.0",
        "mime": "1.6.0",
        "ms": "2.1.3",
        "on-finished": "2.4.1",
        "range-parser": "~1.2.1",
        "statuses": "2.0.1"
      },
      "engines": {
        "node": ">= 0.8.0"
      }
    },
    "node_modules/send/node_modules/debug": {
      "version": "2.6.9",
      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
      "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "ms": "2.0.0"
      }
    },
    "node_modules/send/node_modules/debug/node_modules/ms": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
      "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
      "dev": true,
      "license": "MIT"
    },
    "node_modules/send/node_modules/encodeurl": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
      "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/send/node_modules/mime": {
      "version": "1.6.0",
      "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
      "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
      "dev": true,
      "license": "MIT",
      "bin": {
        "mime": "cli.js"
      },
      "engines": {
        "node": ">=4"
      }
    },
    "node_modules/serve-static": {
      "version": "1.16.0",
      "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.0.tgz",
      "integrity": "sha512-pDLK8zwl2eKaYrs8mrPZBJua4hMplRWJ1tIFksVC3FtBEBnl8dxgeHtsaMS8DhS9i4fLObaon6ABoc4/hQGdPA==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "encodeurl": "~1.0.2",
        "escape-html": "~1.0.3",
        "parseurl": "~1.3.3",
        "send": "0.18.0"
      },
      "engines": {
        "node": ">= 0.8.0"
      }
    },
    "node_modules/serve-static/node_modules/debug": {
      "version": "2.6.9",
      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
      "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "ms": "2.0.0"
      }
    },
    "node_modules/serve-static/node_modules/debug/node_modules/ms": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
      "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
      "dev": true,
      "license": "MIT"
    },
    "node_modules/serve-static/node_modules/encodeurl": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
      "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/serve-static/node_modules/mime": {
      "version": "1.6.0",
      "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
      "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
      "dev": true,
      "license": "MIT",
      "bin": {
        "mime": "cli.js"
      },
      "engines": {
        "node": ">=4"
      }
    },
    "node_modules/serve-static/node_modules/send": {
      "version": "0.18.0",
      "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
      "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "debug": "2.6.9",
        "depd": "2.0.0",
        "destroy": "1.2.0",
        "encodeurl": "~1.0.2",
        "escape-html": "~1.0.3",
        "etag": "~1.8.1",
        "fresh": "0.5.2",
        "http-errors": "2.0.0",
        "mime": "1.6.0",
        "ms": "2.1.3",
        "on-finished": "2.4.1",
        "range-parser": "~1.2.1",
        "statuses": "2.0.1"
      },
      "engines": {
        "node": ">= 0.8.0"
      }
    },
    "node_modules/setprototypeof": {
      "version": "1.2.0",
      "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
      "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
      "dev": true,
      "license": "ISC"
    },
    "node_modules/shebang-command": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
      "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
      "dev": true,
      "license": "MIT",
      "dependenc7ies": {
        "shebang-regex": "^3.0.0"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/shebang-regex": {
      "version": "3.0.0",
      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
      "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/side-channel": {
      "version": "1.1.1",
      "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
      "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "es-errors": "^1.3.0",
        "object-inspect": "^1.13.4",
        "side-channel-list": "^1.0.1",
        "side-channel-map": "^1.0.1",
        "side-channel-weakmap": "^1.0.2"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/side-channel-list": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
      "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "es-errors": "^1.3.0",
        "object-inspect": "^1.13.4"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/side-channel-map": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
      "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "call-bound": "^1.0.2",
        "es-errors": "^1.3.0",
        "get-intrinsic": "^1.2.5",
        "object-inspect": "^1.13.3"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/side-channel-weakmap": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
      "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "call-bound": "^1.0.2",
        "es-errors": "^1.3.0",
        "get-intrinsic": "^1.2.5",
        "object-inspect": "^1.13.3",
        "side-channel-map": "^1.0.1"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/signal-exit": {
      "version": "3.0.7",
      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
      "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
      "dev": true,
      "license": "ISC"
    },
    "node_modules/sisteransi": {
      "version": "1.0.5",
      "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
      "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==",
      "dev": true,
      "license": "MIT",
      "peer": true
    },
    "node_modules/slash": {
      "version": "3.0.0",
      "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
      "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/source-map": {
      "version": "0.6.1",
      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
      "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
      "license": "BSD-3-Clause",
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/source-map-js": {
      "version": "1.2.1",
      "resolved": "https://registry.npmjs.org/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/source-map-support": {
      "version": "0.5.21",
      "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
      "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
      "devOptional": true,
      "license": "MIT",
      "dependencies": {
        "buffer-from": "^1.0.0",
        "source-map": "^0.6.0"
      }
    },
    "node_modules/sprintf-js": {
      "version": "1.0.3",
      "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
      "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
      "dev": true,
      "license": "BSD-3-Clause",
      "peer": true
    },
    "node_modules/stack-utils": {
      "version": "2.0.6",
      "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
      "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "escape-string-regexp": "^2.0.0"
      },
      "engines": {
        "node": ">=10"
      }
    },
    "node_modules/stack-utils/node_modules/escape-string-regexp": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
      "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/statuses": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
      "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/string-hash": {
      "version": "1.1.3",
      "resolved": "https://registry.npmjs.org/string-hash/-/string-hash-1.1.3.tgz",
      "integrity": "sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A==",
      "license": "CC0-1.0"
    },
    "node_modules/string-length": {
      "version": "4.0.2",
      "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz",
      "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "char-regex": "^1.0.2",
        "strip-ansi": "^6.0.0"
      },
      "engines": {
        "node": ">=10"
      }
    },
    "node_modules/string-width": {
      "version": "4.2.3",
      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "emoji-regex": "^8.0.0",
        "is-fullwidth-code-point": "^3.0.0",
        "strip-ansi": "^6.0.1"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/strip-ansi": {
      "version": "6.0.1",
      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "ansi-regex": "^5.0.1"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/strip-bom": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz",
      "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/strip-final-newline": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
      "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=6"
      }
    },
    "node_modules/strip-json-comments": {
      "version": "3.1.1",
      "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
      "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "engines": {
        "node": ">=8"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/strip-literal": {
      "version": "3.1.0",
      "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz",
      "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==",
      "license": "MIT",
      "dependencies": {
        "js-tokens": "^9.0.1"
      },
      "funding": {
        "url": "https://github.com/sponsors/antfu"
      }
    },
    "node_modules/strip-literal/node_modules/js-tokens": {
      "version": "9.0.1",
      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",
      "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==",
      "license": "MIT"
    },
    "node_modules/supports-color": {
      "version": "5.5.0",
      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
      "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
      "license": "MIT",
      "dependencies": {
        "has-flag": "^3.0.0"
      },
      "engines": {
        "node": ">=4"
      }
    },
    "node_modules/supports-hyperlinks": {
      "version": "2.3.0",
      "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz",
      "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "has-flag": "^4.0.0",
        "supports-color": "^7.0.0"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/supports-hyperlinks/node_modules/has-flag": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/supports-hyperlinks/node_modules/supports-color": {
      "version": "7.2.0",
      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "has-flag": "^4.0.0"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/supports-preserve-symlinks-flag": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
      "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/symbol-tree": {
      "version": "3.2.4",
      "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
      "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
      "dev": true,
      "license": "MIT",
      "peer": true
    },
    "node_modules/systemjs": {
      "version": "6.15.1",
      "resolved": "https://registry.npmjs.org/systemjs/-/systemjs-6.15.1.tgz",
      "integrity": "sha512-Nk8c4lXvMB98MtbmjX7JwJRgJOL8fluecYCfCeYBznwmpOs8Bf15hLM6z4z71EDAhQVrQrI+wt1aLWSXZq+hXA==",
      "dev": true,
      "license": "MIT"
    },
    "node_modules/tapable": {
      "version": "2.3.3",
      "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz",
      "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==",
      "license": "MIT",
      "engines": {
        "node": ">=6"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/webpack"
      }
    },
    "node_modules/terminal-link": {
      "version": "2.1.1",
      "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz",
      "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "ansi-escapes": "^4.2.1",
        "supports-hyperlinks": "^2.0.0"
      },
      "engines": {
        "node": ">=8"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/terser": {
      "version": "5.48.0",
      "resolved": "https://registry.npmjs.org/terser/-/terser-5.48.0.tgz",
      "integrity": "sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==",
      "devOptional": true,
      "license": "BSD-2-Clause",
      "dependencies": {
        "@jridgewell/source-map": "^0.3.3",
        "acorn": "^8.15.0",
        "commander": "^2.20.0",
        "source-map-support": "~0.5.20"
      },
      "bin": {
        "terser": "bin/terser"
      },
      "engines": {
        "node": ">=10"
      }
    },
    "node_modules/test-exclude": {
      "version": "6.0.0",
      "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
      "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==",
      "dev": true,
      "license": "ISC",
      "peer": true,
      "dependencies": {
        "@istanbuljs/schema": "^0.1.2",
        "glob": "^7.1.4",
        "minimatch": "^3.0.4"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/throat": {
      "version": "6.0.2",
      "resolved": "https://registry.npmjs.org/throat/-/throat-6.0.2.tgz",
      "integrity": "sha512-WKexMoJj3vEuK0yFEapj8y64V0A6xcuPuK9Gt1d0R+dzCSJc0lHqQytAbSB4cDAK0dWh4T0E2ETkoLE2WZ41OQ==",
      "dev": true,
      "license": "MIT",
      "peer": true
    },
    "node_modules/timm": {
      "version": "1.7.1",
      "resolved": "https://registry.npmjs.org/timm/-/timm-1.7.1.tgz",
      "integrity": "sha512-IjZc9KIotudix8bMaBW6QvMuq64BrJWFs1+4V0lXwWGQZwH+LnX87doAYhem4caOEusRP9/g6jVDQmZ8XOk1nw==",
      "license": "MIT"
    },
    "node_modules/tinycolor2": {
      "version": "1.6.0",
      "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz",
      "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==",
      "license": "MIT"
    },
    "node_modules/tmpl": {
      "version": "1.0.5",
      "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
      "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==",
      "dev": true,
      "license": "BSD-3-Clause",
      "peer": true
    },
    "node_modules/to-fast-properties": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
      "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==",
      "license": "MIT",
      "engines": {
        "node": ">=4"
      }
    },
    "node_modules/to-regex-range": {
      "version": "5.0.1",
      "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
      "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
      "license": "MIT",
      "dependencies": {
        "is-number": "^7.0.0"
      },
      "engines": {
        "node": ">=8.0"
      }
    },
    "node_modules/toidentifier": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
      "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=0.6"
      }
    },
    "node_modules/tough-cookie": {
      "version": "4.1.4",
      "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz",
      "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==",
      "dev": true,
      "license": "BSD-3-Clause",
      "peer": true,
      "dependencies": {
        "psl": "^1.1.33",
        "punycode": "^2.1.1",
        "universalify": "^0.2.0",
        "url-parse": "^1.5.3"
      },
      "engines": {
        "node": ">=6"
      }
    },
    "node_modules/tough-cookie/node_modules/universalify": {
      "version": "0.2.0",
      "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
      "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "engines": {
        "node": ">= 4.0.0"
      }
    },
    "node_modules/tr46": {
      "version": "2.1.0",
      "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz",
      "integrity": "sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "punycode": "^2.1.1"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/type-detect": {
      "version": "4.0.8",
      "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
      "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "engines": {
        "node": ">=4"
      }
    },
    "node_modules/type-fest": {
      "version": "0.21.3",
      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
      "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
      "dev": true,
      "license": "(MIT OR CC0-1.0)",
      "peer": true,
      "engines": {
        "node": ">=10"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/type-is": {
      "version": "1.6.18",
      "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
      "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "media-typer": "0.3.0",
        "mime-types": "~2.1.24"
      },
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/typedarray-to-buffer": {
      "version": "3.1.5",
      "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
      "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "is-typedarray": "^1.0.0"
      }
    },
    "node_modules/ufo": {
      "version": "1.6.4",
      "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz",
      "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==",
      "license": "MIT"
    },
    "node_modules/undici-types": {
      "version": "7.24.6",
      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz",
      "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==",
      "devOptional": true,
      "license": "MIT",
      "peer": true
    },
    "node_modules/unicode-canonical-property-names-ecmascript": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz",
      "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=4"
      }
    },
    "node_modules/unicode-match-property-ecmascript": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz",
      "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "unicode-canonical-property-names-ecmascript": "^2.0.0",
        "unicode-property-aliases-ecmascript": "^2.0.0"
      },
      "engines": {
        "node": ">=4"
      }
    },
    "node_modules/unicode-match-property-value-ecmascript": {
      "version": "2.2.1",
      "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz",
      "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=4"
      }
    },
    "node_modules/unicode-property-aliases-ecmascript": {
      "version": "2.2.0",
      "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz",
      "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=4"
      }
    },
    "node_modules/unimport": {
      "version": "4.1.1",
      "resolved": "https://registry.npmjs.org/unimport/-/unimport-4.1.1.tgz",
      "integrity": "sha512-j9+fijH6aDd05yv1fXlyt7HSxtOWtGtrZeYTVBsSUg57Iuf+Ps2itIZjeyu7bEQ4k0WOgYhHrdW8m/pJgOpl5g==",
      "license": "MIT",
      "dependencies": {
        "acorn": "^8.14.0",
        "escape-string-regexp": "^5.0.0",
        "estree-walker": "^3.0.3",
        "fast-glob": "^3.3.3",
        "local-pkg": "^1.0.0",
        "magic-string": "^0.30.17",
        "mlly": "^1.7.4",
        "pathe": "^2.0.2",
        "picomatch": "^4.0.2",
        "pkg-types": "^1.3.1",
        "scule": "^1.3.0",
        "strip-literal": "^3.0.0",
        "unplugin": "^2.1.2",
        "unplugin-utils": "^0.2.3"
      },
      "engines": {
        "node": ">=18.12.0"
      }
    },
    "node_modules/unimport/node_modules/escape-string-regexp": {
      "version": "5.0.0",
      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
      "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
      "license": "MIT",
      "engines": {
        "node": ">=12"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/unimport/node_modules/estree-walker": {
      "version": "3.0.3",
      "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
      "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
      "license": "MIT",
      "dependencies": {
        "@types/estree": "^1.0.0"
      }
    },
    "node_modules/unimport/node_modules/magic-string": {
      "version": "0.30.21",
      "resolved": "https://registry.npmjs.org/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/unimport/node_modules/picomatch": {
      "version": "4.0.4",
      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
      "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
      "license": "MIT",
      "engines": {
        "node": ">=12"
      },
      "funding": {
        "url": "https://github.com/sponsors/jonschlinkert"
      }
    },
    "node_modules/universalify": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
      "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
      "license": "MIT",
      "engines": {
        "node": ">= 10.0.0"
      }
    },
    "node_modules/unpipe": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
      "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/unplugin": {
      "version": "2.3.11",
      "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz",
      "integrity": "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==",
      "license": "MIT",
      "dependencies": {
        "@jridgewell/remapping": "^2.3.5",
        "acorn": "^8.15.0",
        "picomatch": "^4.0.3",
        "webpack-virtual-modules": "^0.6.2"
      },
      "engines": {
        "node": ">=18.12.0"
      }
    },
    "node_modules/unplugin-utils": {
      "version": "0.2.5",
      "resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.2.5.tgz",
      "integrity": "sha512-gwXJnPRewT4rT7sBi/IvxKTjsms7jX7QIDLOClApuZwR49SXbrB1z2NLUZ+vDHyqCj/n58OzRRqaW+B8OZi8vg==",
      "license": "MIT",
      "dependencies": {
        "pathe": "^2.0.3",
        "picomatch": "^4.0.3"
      },
      "engines": {
        "node": ">=18.12.0"
      },
      "funding": {
        "url": "https://github.com/sponsors/sxzz"
      }
    },
    "node_modules/unplugin-utils/node_modules/picomatch": {
      "version": "4.0.4",
      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
      "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
      "license": "MIT",
      "engines": {
        "node": ">=12"
      },
      "funding": {
        "url": "https://github.com/sponsors/jonschlinkert"
      }
    },
    "node_modules/unplugin/node_modules/picomatch": {
      "version": "4.0.4",
      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
      "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
      "license": "MIT",
      "engines": {
        "node": ">=12"
      },
      "funding": {
        "url": "https://github.com/sponsors/jonschlinkert"
      }
    },
    "node_modules/unquote": {
      "version": "1.1.1",
      "resolved": "https://registry.npmjs.org/unquote/-/unquote-1.1.1.tgz",
      "integrity": "sha512-vRCqFv6UhXpWxZPyGDh/F3ZpNv8/qo7w6iufLpQg9aKnQ71qM4B5KiI7Mia9COcjEhrO9LueHpMYjYzsWH3OIg==",
      "license": "MIT"
    },
    "node_modules/update-browserslist-db": {
      "version": "1.2.3",
      "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
      "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
      "funding": [
        {
          "type": "opencollective",
          "url": "https://opencollective.com/browserslist"
        },
        {
          "type": "tidelift",
          "url": "https://tidelift.com/funding/github/npm/browserslist"
        },
        {
          "type": "github",
          "url": "https://github.com/sponsors/ai"
        }
      ],
      "license": "MIT",
      "dependencies": {
        "escalade": "^3.2.0",
        "picocolors": "^1.1.1"
      },
      "bin": {
        "update-browserslist-db": "cli.js"
      },
      "peerDependencies": {
        "browserslist": ">= 4.21.0"
      }
    },
    "node_modules/update-browserslist-db/node_modules/picocolors": {
      "version": "1.1.1",
      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
      "license": "ISC"
    },
    "node_modules/url-parse": {
      "version": "1.5.10",
      "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
      "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "querystringify": "^2.1.1",
        "requires-port": "^1.0.0"
      }
    },
    "node_modules/utif": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/utif/-/utif-2.0.1.tgz",
      "integrity": "sha512-Z/S1fNKCicQTf375lIP9G8Sa1H/phcysstNrrSdZKj1f9g58J4NMgb5IgiEZN9/nLMPDwF0W7hdOe9Qq2IYoLg==",
      "license": "MIT",
      "dependencies": {
        "pako": "^1.0.5"
      }
    },
    "node_modules/util-deprecate": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
      "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
      "license": "MIT"
    },
    "node_modules/utils-merge": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
      "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">= 0.4.0"
      }
    },
    "node_modules/uview-plus": {
      "version": "3.8.55",
      "resolved": "https://registry.npmjs.org/uview-plus/-/uview-plus-3.8.55.tgz",
      "integrity": "sha512-OcjitgqLWZzMZuq93YSfpvOzyFvOuhGC6Lh4cljX28bNmDImoe14IhOpW8tcLcjLilbEOZJBYxbvpZpGAdiQxQ==",
      "engines": {
        "HBuilderX": "^3.1.0",
        "uni-app": "^4.66",
        "uni-app-x": ""
      }
    },
    "node_modules/v8-to-istanbul": {
      "version": "8.1.1",
      "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-8.1.1.tgz",
      "integrity": "sha512-FGtKtv3xIpR6BYhvgH8MI/y78oT7d8Au3ww4QIxymrCtZEh5b8gCw2siywE+puhEmuWKDtmfrvF5UlB298ut3w==",
      "dev": true,
      "license": "ISC",
      "peer": true,
      "dependencies": {
        "@types/istanbul-lib-coverage": "^2.0.1",
        "convert-source-map": "^1.6.0",
        "source-map": "^0.7.3"
      },
      "engines": {
        "node": ">=10.12.0"
      }
    },
    "node_modules/v8-to-istanbul/node_modules/convert-source-map": {
      "version": "1.9.0",
      "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
      "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
      "dev": true,
      "license": "MIT",
      "peer": true
    },
    "node_modules/v8-to-istanbul/node_modules/source-map": {
      "version": "0.7.6",
      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz",
      "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==",
      "dev": true,
      "license": "BSD-3-Clause",
      "peer": true,
      "engines": {
        "node": ">= 12"
      }
    },
    "node_modules/vary": {
      "version": "1.1.2",
      "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
      "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/vite": {
      "version": "5.2.8",
      "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.8.tgz",
      "integrity": "sha512-OyZR+c1CE8yeHw5V5t59aXsUPPVTHMDjEZz8MgguLL/Q7NblxhZUlTu9xSPqlsUO/y+X7dlU05jdhvyycD55DA==",
      "license": "MIT",
      "dependencies": {
        "esbuild": "^0.20.1",
        "postcss": "^8.4.38",
        "rollup": "^4.13.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": "*",
        "stylus": "*",
        "sugarss": "*",
        "terser": "^5.4.0"
      },
      "peerDependenciesMeta": {
        "@types/node": {
          "optional": true
        },
        "less": {
          "optional": true
        },
        "lightningcss": {
          "optional": true
        },
        "sass": {
          "optional": true
        },
        "stylus": {
          "optional": true
        },
        "sugarss": {
          "optional": true
        },
        "terser": {
          "optional": true
        }
      }
    },
    "node_modules/vue": {
      "version": "3.4.21",
      "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.21.tgz",
      "integrity": "sha512-5hjyV/jLEIKD/jYl4cavMcnzKwjMKohureP8ejn3hhEjwhWIhWeuzL2kJAjzl/WyVsgPY56Sy4Z40C3lVshxXA==",
      "license": "MIT",
      "dependencies": {
        "@vue/compiler-dom": "3.4.21",
        "@vue/compiler-sfc": "3.4.21",
        "@vue/runtime-dom": "3.4.21",
        "@vue/server-renderer": "3.4.21",
        "@vue/shared": "3.4.21"
      },
      "peerDependencies": {
        "typescript": "*"
      },
      "peerDependenciesMeta": {
        "typescript": {
          "optional": true
        }
      }
    },
    "node_modules/vue-i18n": {
      "version": "9.14.5",
      "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.14.5.tgz",
      "integrity": "sha512-0jQ9Em3ymWngyiIkj0+c/k7WgaPO+TNzjKSNq9BvBQaKJECqn9cd9fL4tkDhB5G1QBskGl9YxxbDAhgbFtpe2g==",
      "deprecated": "v9 and v10 no longer supported. please migrate to v11. about maintenance status, see https://vue-i18n.intlify.dev/guide/maintenance.html",
      "license": "MIT",
      "dependencies": {
        "@intlify/core-base": "9.14.5",
        "@intlify/shared": "9.14.5",
        "@vue/devtools-api": "^6.5.0"
      },
      "engines": {
        "node": ">= 16"
      },
      "funding": {
        "url": "https://github.com/sponsors/kazupon"
      },
      "peerDependencies": {
        "vue": "^3.0.0"
      }
    },
    "node_modules/vue-i18n/node_modules/@intlify/core-base": {
      "version": "9.14.5",
      "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.14.5.tgz",
      "integrity": "sha512-5ah5FqZG4pOoHjkvs8mjtv+gPKYU0zCISaYNjBNNqYiaITxW8ZtVih3GS/oTOqN8d9/mDLyrjD46GBApNxmlsA==",
      "license": "MIT",
      "dependencies": {
        "@intlify/message-compiler": "9.14.5",
        "@intlify/shared": "9.14.5"
      },
      "engines": {
        "node": ">= 16"
      },
      "funding": {
        "url": "https://github.com/sponsors/kazupon"
      }
    },
    "node_modules/vue-i18n/node_modules/@intlify/message-compiler": {
      "version": "9.14.5",
      "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.14.5.tgz",
      "integrity": "sha512-IHzgEu61/YIpQV5Pc3aRWScDcnFKWvQA9kigcINcCBXN8mbW+vk9SK+lDxA6STzKQsVJxUPg9ACC52pKKo3SVQ==",
      "license": "MIT",
      "dependencies": {
        "@intlify/shared": "9.14.5",
        "source-map-js": "^1.0.2"
      },
      "engines": {
        "node": ">= 16"
      },
      "funding": {
        "url": "https://github.com/sponsors/kazupon"
      }
    },
    "node_modules/vue-i18n/node_modules/@intlify/shared": {
      "version": "9.14.5",
      "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.14.5.tgz",
      "integrity": "sha512-9gB+E53BYuAEMhbCAxVgG38EZrk59sxBtv3jSizNL2hEWlgjBjAw1AwpLHtNaeda12pe6W20OGEa0TwuMSRbyQ==",
      "license": "MIT",
      "engines": {
        "node": ">= 16"
      },
      "funding": {
        "url": "https://github.com/sponsors/kazupon"
      }
    },
    "node_modules/vue-router": {
      "version": "4.4.4",
      "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.4.4.tgz",
      "integrity": "sha512-3MlnDqwRwZwCQVbtVfpsU+nrNymNjnXSsQtXName5925NVC1+326VVfYH9vSrA0N13teGEo8z5x7gbRnGjCDiQ==",
      "license": "MIT",
      "dependencies": {
        "@vue/devtools-api": "^6.6.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/posva"
      },
      "peerDependencies": {
        "vue": "^3.2.0"
      }
    },
    "node_modules/w3c-hr-time": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz",
      "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==",
      "deprecated": "Use your platform's native performance.now() and performance.timeOrigin.",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "browser-process-hrtime": "^1.0.0"
      }
    },
    "node_modules/w3c-xmlserializer": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz",
      "integrity": "sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "xml-name-validator": "^3.0.0"
      },
      "engines": {
        "node": ">=10"
      }
    },
    "node_modules/walker": {
      "version": "1.0.8",
      "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz",
      "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==",
      "dev": true,
      "license": "Apache-2.0",
      "peer": true,
      "dependencies": {
        "makeerror": "1.0.12"
      }
    },
    "node_modules/webidl-conversions": {
      "version": "6.1.0",
      "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz",
      "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==",
      "dev": true,
      "license": "BSD-2-Clause",
      "peer": true,
      "engines": {
        "node": ">=10.4"
      }
    },
    "node_modules/webpack-virtual-modules": {
      "version": "0.6.2",
      "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
      "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==",
      "license": "MIT"
    },
    "node_modules/whatwg-encoding": {
      "version": "1.0.5",
      "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz",
      "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==",
      "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "iconv-lite": "0.4.24"
      }
    },
    "node_modules/whatwg-mimetype": {
      "version": "2.3.0",
      "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz",
      "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==",
      "dev": true,
      "license": "MIT",
      "peer": true
    },
    "node_modules/whatwg-url": {
      "version": "8.7.0",
      "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.7.0.tgz",
      "integrity": "sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "lodash": "^4.7.0",
        "tr46": "^2.1.0",
        "webidl-conversions": "^6.1.0"
      },
      "engines": {
        "node": ">=10"
      }
    },
    "node_modules/which": {
      "version": "2.0.2",
      "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
      "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
      "dev": true,
      "license": "ISC",
      "dependencies": {
        "isexe": "^2.0.0"
      },
      "bin": {
        "node-which": "bin/node-which"
      },
      "engines": {
        "node": ">= 8"
      }
    },
    "node_modules/wrap-ansi": {
      "version": "7.0.0",
      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
      "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "ansi-styles": "^4.0.0",
        "string-width": "^4.1.0",
        "strip-ansi": "^6.0.0"
      },
      "engines": {
        "node": ">=10"
      },
      "funding": {
        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
      }
    },
    "node_modules/wrap-ansi/node_modules/ansi-styles": {
      "version": "4.3.0",
      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "color-convert": "^2.0.1"
      },
      "engines": {
        "node": ">=8"
      },
      "funding": {
        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
      }
    },
    "node_modules/wrap-ansi/node_modules/color-convert": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "color-name": "~1.1.4"
      },
      "engines": {
        "node": ">=7.0.0"
      }
    },
    "node_modules/wrap-ansi/node_modules/color-name": {
      "version": "1.1.4",
      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
      "dev": true,
      "license": "MIT",
      "peer": true
    },
    "node_modules/wrappy": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
      "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
      "dev": true,
      "license": "ISC",
      "peer": true
    },
    "node_modules/write-file-atomic": {
      "version": "3.0.3",
      "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz",
      "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==",
      "dev": true,
      "license": "ISC",
      "peer": true,
      "dependencies": {
        "imurmurhash": "^0.1.4",
        "is-typedarray": "^1.0.0",
        "signal-exit": "^3.0.2",
        "typedarray-to-buffer": "^3.1.5"
      }
    },
    "node_modules/ws": {
      "version": "8.18.0",
      "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
      "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
      "license": "MIT",
      "engines": {
        "node": ">=10.0.0"
      },
      "peerDependencies": {
        "bufferutil": "^4.0.1",
        "utf-8-validate": ">=5.0.2"
      },
      "peerDependenciesMeta": {
        "bufferutil": {
          "optional": true
        },
        "utf-8-validate": {
          "optional": true
        }
      }
    },
    "node_modules/xhr": {
      "version": "2.6.0",
      "resolved": "https://registry.npmjs.org/xhr/-/xhr-2.6.0.tgz",
      "integrity": "sha512-/eCGLb5rxjx5e3mF1A7s+pLlR6CGyqWN91fv1JgER5mVWg1MZmlhBvy9kjcsOdRk8RrIujotWyJamfyrp+WIcA==",
      "license": "MIT",
      "dependencies": {
        "global": "~4.4.0",
        "is-function": "^1.0.1",
        "parse-headers": "^2.0.0",
        "xtend": "^4.0.0"
      }
    },
    "node_modules/xml-name-validator": {
      "version": "3.0.0",
      "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz",
      "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==",
      "dev": true,
      "license": "Apache-2.0",
      "peer": true
    },
    "node_modules/xml-parse-from-string": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/xml-parse-from-string/-/xml-parse-from-string-1.0.1.tgz",
      "integrity": "sha512-ErcKwJTF54uRzzNMXq2X5sMIy88zJvfN2DmdoQvy7PAFJ+tPRU6ydWuOKNMyfmOjdyBQTFREi60s0Y0SyI0G0g==",
      "license": "MIT"
    },
    "node_modules/xml2js": {
      "version": "0.5.0",
      "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz",
      "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==",
      "license": "MIT",
      "dependencies": {
        "sax": ">=0.6.0",
        "xmlbuilder": "~11.0.0"
      },
      "engines": {
        "node": ">=4.0.0"
      }
    },
    "node_modules/xmlbuilder": {
      "version": "11.0.1",
      "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
      "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
      "license": "MIT",
      "engines": {
        "node": ">=4.0"
      }
    },
    "node_modules/xmlchars": {
      "version": "2.2.0",
      "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
      "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
      "dev": true,
      "license": "MIT",
      "peer": true
    },
    "node_modules/xmlhttprequest": {
      "version": "1.8.0",
      "resolved": "https://registry.npmjs.org/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz",
      "integrity": "sha512-58Im/U0mlVBLM38NdZjHyhuMtCqa61469k2YP/AaPbvCoV9aQGUpbJBj1QRm2ytRiVQBD/fsw7L2bJGDVQswBA==",
      "license": "MIT",
      "engines": {
        "node": ">=0.4.0"
      }
    },
    "node_modules/xregexp": {
      "version": "5.1.2",
      "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-5.1.2.tgz",
      "integrity": "sha512-6hGgEMCGhqCTFEJbqmWrNIPqfpdirdGWkqshu7fFZddmTSfgv5Sn9D2SaKloR79s5VUiUlpwzg3CM3G6D3VIlw==",
      "license": "MIT",
      "dependencies": {
        "@babel/runtime-corejs3": "^7.26.9"
      }
    },
    "node_modules/xtend": {
      "version": "4.0.2",
      "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
      "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
      "license": "MIT",
      "engines": {
        "node": ">=0.4"
      }
    },
    "node_modules/y18n": {
      "version": "5.0.8",
      "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
      "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
      "dev": true,
      "license": "ISC",
      "peer": true,
      "engines": {
        "node": ">=10"
      }
    },
    "node_modules/yallist": {
      "version": "3.1.1",
      "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
      "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
      "license": "ISC"
    },
    "node_modules/yaml": {
      "version": "1.10.3",
      "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz",
      "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==",
      "license": "ISC",
      "engines": {
        "node": ">= 6"
      }
    },
    "node_modules/yargs": {
      "version": "16.2.0",
      "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
      "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "cliui": "^7.0.2",
        "escalade": "^3.1.1",
        "get-caller-file": "^2.0.5",
        "require-directory": "^2.1.1",
        "string-width": "^4.2.0",
        "y18n": "^5.0.5",
        "yargs-parser": "^20.2.2"
      },
      "engines": {
        "node": ">=10"
      }
    },
    "node_modules/yargs-parser": {
      "version": "20.2.9",
      "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
      "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
      "dev": true,
      "license": "ISC",
      "peer": true,
      "engines": {
        "node": ">=10"
      }
    }
  }
}
x8x熐*{
  "name": "lifanghe-education-backend",
|6Slifanghe-education-backend",W"Xdcors": "^2.8.5",
        "express": "^4.19.2",
        "morgan": "^1.10.0",
        "mysql2": "^3.22&?k65Pnode/-/node-26 K&Ufc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQx%p. ~8.3%bix 
Pjzk)aws-ssl-pro86#aws-ssl-profiles/-/aws-ssl-profiles1(UNZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6a% u(	basic-aut6basic-auth/-/basic-auth0(UNF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZea?kph2T`N#basic-auth/a1P4%UGd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+6жW#2#U3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9O1d?	~3.1.2X!K~ ~2.0.1-~G!=~2.4.1",
        "qs": "~6.15.1",
        "raw-body": "~2.5.34ik<Jn"_N$&OK|2 n2(ͷ|2kj8 (7471$Uyki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/fE ()O29(25#UNXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyF7c`j2.8A3cors/-/cors-2.8%TJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGx%fobject-assign": "^4[^Q=2Dexpres$%͗>d2.denqu]b6denque/-/denque@(UHVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekX,!%Qf8&n"L'>d2(x7*/n"_N$3n" 5f8`67xЄ::9YBx7M߀FW!2!3`WA%UIuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1|?AXN~1.20.5X#~X=~0.7.1X ~1.0.6*_lY\~1.3.1",
        "fresh": "~p ~Zc~eZA~0.1.12Z2~6.1551l'[~:["~1.16.2ǣ;~[NDexpres$=msi&4i31$UaA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFha?lY2Y~eZ/statuses": "~2.0.2ik_^vf8 &ys7'	ate-funct072%generate-function/-/generate-function(UeeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwK|?is-property||2(kn2n" 5
Еj`s7`w_$25#U4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLm|?~2.0.0m~£#~[$~2.0.2",
        "toidentifier": "~1G5ka	@Yexpresa%ީ>d2wpnJ3#2is-property/-/is-propertyQ(UKs/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj926lo&6long/-/long-5.31$UmNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QA#|lru.m6lru.min/-/lru.minq(UDqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdI:'bun": ">=1.0.0",
        "deno": ">=1.3>~&$wellwelw%n" 5$W s7'p#$|58`/(_F	*-nF.x /horgaI1.1q4morgan/-/morgan-1.1!%UzSkVu3t18r39pw4ixfBKvfZi3y2UOqr7d4WYwcj3m8nXpEQK4rPO6GLzs/CExoRgmX3y9EjmmcXqv6jq0SK46a?basic-auth": "~2.0.1~2.0.0M"~2.4.1",
        "on-headers": "~1a5p<	@Yexpres$o:~ŗ7mysql!?3.223mysql2/-/mysql2-3.2!k%U95uZ2TrPWAZdwpB3vvvDbmEMcNG8yIeNCyu6GUcr/QnWEE/wXm7+mhOCsdQfWQDTV7qYT/PDUZ4U4UPP4AsXq|?,aws-ssl-profiles": "^1.1.2",
        "denquePgenerate-function": "^2.3.1-^0.7.2",
        "long": "^5.3.2",
        "lru.min": "^1.1.4",
        "named-placeholders": "^1.1.6",
        "sql-escaper": "^1.33 813n 5'mysql2/Щ074671$Uim9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqx%"#3 QuNDexpres$named-placehold}\4+named-placeholders/-/named-placeholders-1.1$UTz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0x%Ѓru.min": "^1a5>(>nFobject-assigID2object-assign/-/object-assign-4#&UrJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUa%i|u)If8`vLn2Nk-head4on-headers/-/on-header*U737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8E&efs7k'om&1p3n$3o#UA/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKq7ؠn24n51$URzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9DdX9Na4Em	Pa6nF#`2r#Ts4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojU8@	~3.1.2~2.0.1-~0.4.24ik^nܪr)R	362#UVMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPra?]lYM	resh": "~p ~2.0pf7~?~2.0qh1T+<:`2!r#Ux0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMe1d?^Y@vZsend": "~0.193p+L4>|2 a+xЄ:>d29>d2|
ql-escaif2sql-escaper/-/sql-escaper-1.3q$UBsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJx:%bun": ">=1.0.0",
        "deno": ">=21@j)mysqljs/sql-escaperq
-#e2RK#UDvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRfE&D7E'Nn2O*S#85S85&Uj375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZ|%%?gs7k'Pz8{({n"_Ns!xC{
  "name": "lifanghe-education-backend",
  "version": "1.0.0",
  "private": true,
  "type": "commonjs",
  "scripts": {
    "dev": "node --watch src/server.js",
    "start": "node src/server.js"
  },
  "dependencies": {
    "cors": "^2.8.5",
    "express": "^4.19.2",
    "morgan": "^1.10.0",
    "mysql2": "^3.22.5"
  }
}
oXxI 100644 init.sql  lRrxJ	я640000 migrations vܭ}]Q&<7㵦xe*CREATE DATABASE IF NOT EXISTS lifanghe_education DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE lifanghe_education;

CREATE TABLE IF NOT EXISTS roles (
  role VARCHAR(40) PRIMARY KEY,
  name VARCHAR(80) NOT NULL,
  permissions JSON NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE IF NOT EXISTS users (
  id VARCHAR(40) PRIMARY KEY,
  username VARCHAR(80) NOT NULL UNIQUE,
  password_hash CHAR(64) NULL,
  role VARCHAR(40) NOT NULL,
  role_name VARCHAR(80) NOT NULL,
  name VARCHAR(80) NOT NULL,
  phone VARCHAR(30) NULL UNIQUE,
  wechat_openid VARCHAR(80) NULL UNIQUE,
  wechat_unionid VARCHAR(80) NULL UNIQUE,
  wechat_nickname VARCHAR(80) NULL,
  wechat_avatar VARCHAR(500) NULL,
  login_provider VARCHAR(30) NOT NULL DEFAULT 'password',
  phone_bound TINYINT(1) NOT NULL DEFAULT 1,
  phone_bound_at VARCHAR(32) NULL,
  wechat_bound_at VARCHAR(32) NULL,
  status VARCHAR(20) NOT NULL DEFAULT 'active',
  last_login_at VARCHAR(32) NULL,
  inviter_code VARCHAR(40) NULL UNIQUE,
  child_id VARCHAR(40) NULL,
  child_name VARCHAR(80) NULL,
  child_grade VARCHAR(40) NULL,
  child_focus VARCHAR(80) NULL,
  trial_booking_limit INT NOT NULL DEFAULT 1,
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  INDEX idx_users_role (role),
  INDEX idx_users_phone (phone),
  INDEX idx_users_login_provider (login_provider),
  CONSTRAINT fk_users_role FOREIGN KEY (role) REFERENCES roles(role)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE IF NOT EXISTS point_accounts (
  user_id VARCHAR(40) PRIMARY KEY,
  available INT NOT NULL DEFAULT 0,
  pending INT NOT NULL DEFAULT 0,
  frozen INT NOT NULL DEFAULT 0,
  expiring INT NOT NULL DEFAULT 0,
  CONSTRAINT fk_point_accounts_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE IF NOT EXISTS point_transactions (
  id VARCHAR(40) PRIMARY KEY,
  user_id VARCHAR(40) NOT NULL,
  title VARCHAR(200) NOT NULL,
  amount INT NOT NULL,
  status VARCHAR(30) NOT NULL,
  reference_type VARCHAR(40) NULL,
  reference_id VARCHAR(60) NULL,
  created_at VARCHAR(32) NOT NULL,
  INDEX idx_point_transactions_user (user_id),
  INDEX idx_point_transactions_reference (reference_type, reference_id),
  CONSTRAINT fk_point_transactions_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE IF NOT EXISTS point_expiry_rule (
  id INT PRIMARY KEY DEFAULT 1,
  title VARCHAR(100) NOT NULL,
  valid_days INT NOT NULL,
  description VARCHAR(500) NOT NULL,
  reminder VARCHAR(500) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE IF NOT EXISTS point_expiry_schedules (
  id VARCHAR(40) PRIMARY KEY,
  user_id VARCHAR(40) NOT NULL,
  points INT NOT NULL,
  expire_at VARCHAR(32) NOT NULL,
  source VARCHAR(200) NOT NULL,
  INDEX idx_point_expiry_user (user_id),
  CONSTRAINT fk_point_expiry_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE IF NOT EXISTS referral_rules (
  event VARCHAR(40) PRIMARY KEY,
  title VARCHAR(100) NOT NULL,
  inviter INT NOT NULL,
  invitee INT NOT NULL,
  sort_order INT NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE IF NOT EXISTS referrals (
  id VARCHAR(40) PRIMARY KEY,
  inviter_id VARCHAR(40) NOT NULL,
  invitee_id VARCHAR(40) NULL,
  name VARCHAR(80) NOT NULL,
  phone VARCHAR(30) NOT NULL,
  child_grade VARCHAR(40) NOT NULL,
  status VARCHAR(40) NOT NULL,
  reward INT NOT NULL DEFAULT 0,
  channel VARCHAR(80) NOT NULL,
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  INDEX idx_referrals_inviter (inviter_id),
  CONSTRAINT fk_referrals_inviter FOREIGN KEY (inviter_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE IF NOT EXISTS courses (
  id VARCHAR(40) PRIMARY KEY,
  name VARCHAR(120) NOT NULL,
  tags JSON NOT NULL,
  next_time VARCHAR(32) NOT NULL,
  points_back INT NOT NULL DEFAULT 0,
  seats INT NOT NULL DEFAULT 0,
  campus VARCHAR(80) NOT NULL,
  teacher VARCHAR(80) NOT NULL,
  trial_minutes INT NOT NULL DEFAULT 45,
  status VARCHAR(30) NOT NULL DEFAULT 'active',
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE IF NOT EXISTS appointments (
  id VARCHAR(40) PRIMARY KEY,
  user_id VARCHAR(40) NOT NULL,
  child_id VARCHAR(40) NULL,
  course_id VARCHAR(40) NOT NULL,
  status VARCHAR(30) NOT NULL,
  created_at VARCHAR(40) NOT NULL,
  INDEX idx_appointments_user (user_id),
  INDEX idx_appointments_course (course_id),
  CONSTRAINT fk_appointments_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
  CONSTRAINT fk_appointments_course FOREIGN KEY (course_id) REFERENCES courses(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE IF NOT EXISTS rewards (
  id VARCHAR(40) PRIMARY KEY,
  name VARCHAR(120) NOT NULL,
  cost INT NOT NULL,
  type VARCHAR(60) NOT NULL,
  stock INT NOT NULL DEFAULT 0,
  description VARCHAR(500) NOT NULL DEFAULT ''
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE IF NOT EXISTS redemption_orders (
  id VARCHAR(40) PRIMARY KEY,
  user_id VARCHAR(40) NOT NULL,
  item_id VARCHAR(40) NOT NULL,
  status VARCHAR(40) NOT NULL,
  cost_points INT NOT NULL DEFAULT 0,
  voucher_code VARCHAR(60) NULL UNIQUE,
  voucher_status VARCHAR(40) NOT NULL DEFAULT 'not_issued',
  created_at VARCHAR(40) NOT NULL,
  reviewed_by VARCHAR(40) NULL,
  reviewed_at VARCHAR(40) NULL,
  rejected_reason VARCHAR(255) NULL,
  expire_at VARCHAR(40) NULL,
  used_at VARCHAR(40) NULL,
  used_by VARCHAR(40) NULL,
  verify_note VARCHAR(255) NULL,
  INDEX idx_redemption_user (user_id),
  INDEX idx_redemption_status (status),
  INDEX idx_redemption_voucher (voucher_code),
  CONSTRAINT fk_redemption_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
  CONSTRAINT fk_redemption_reward FOREIGN KEY (item_id) REFERENCES rewards(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE IF NOT EXISTS leads (
  id VARCHAR(40) PRIMARY KEY,
  referral_id VARCHAR(40) NULL,
  invitee_id VARCHAR(40) NULL,
  parent VARCHAR(80) NOT NULL,
  source VARCHAR(100) NOT NULL,
  stage VARCHAR(40) NOT NULL,
  advisor VARCHAR(80) NOT NULL,
  risk VARCHAR(40) NOT NULL DEFAULT '正常',
  channel VARCHAR(80) NOT NULL,
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  INDEX idx_leads_referral (referral_id),
  INDEX idx_leads_invitee (invitee_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE IF NOT EXISTS channel_stats (
  channel VARCHAR(80) PRIMARY KEY,
  leads INT NOT NULL DEFAULT 0,
  attended INT NOT NULL DEFAULT 0,
  purchased INT NOT NULL DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

INSERT INTO roles (role, name, permissions) VALUES
('parent', '家长', JSON_ARRAY()),
('super_admin', '超级管理员', JSON_ARRAY('*:*:*')),
('campus_admin', '校区管理员', JSON_ARRAY('dashboard:overview:view', 'system:user:view', 'system:role:view', 'leads:referral:view', 'leads:referral:advance', 'points:account:view', 'points:rule:edit', 'points:redemption:view', 'points:redemption:review', 'points:voucher:verify', 'courses:trial:view', 'courses:trial:add', 'courses:trial:edit', 'statistics:channel:view')),
('advisor', '课程顾问', JSON_ARRAY('dashboard:overview:view', 'leads:referral:view', 'leads:referral:advance', 'courses:trial:view', 'points:account:view', 'points:redemption:view')),
('teacher', '老师', JSON_ARRAY('dashboard:overview:view', 'leads:referral:view', 'courses:trial:view')),
('finance', '财务', JSON_ARRAY('dashboard:overview:view', 'points:account:view', 'points:account:adjust', 'points:rule:edit', 'points:reward:edit', 'points:redemption:view', 'points:redemption:review', 'points:voucher:verify', 'statistics:channel:view'))
ON DUPLICATE KEY UPDATE name = VALUES(name), permissions = VALUES(permissions);

INSERT INTO users (
  id, username, password_hash, role, role_name, name, phone,
  wechat_openid, wechat_unionid, wechat_nickname, wechat_avatar,
  login_provider, phone_bound, phone_bound_at, wechat_bound_at,
  status, last_login_at, inviter_code, child_id, child_name, child_grade, child_focus
) VALUES
('admin_001', 'admin', '240be518fabd2724ddb6f04eeb1da5967448d7e831c08c8fa822809f74c720a9', 'super_admin', '超级管理员', '系统管理员', '13800009999', NULL, NULL, NULL, NULL, 'password', 1, NULL, NULL, 'active', NULL, NULL, NULL, NULL, NULL, NULL)
ON DUPLICATE KEY UPDATE username = VALUES(username), password_hash = VALUES(password_hash), role = VALUES(role), role_name = VALUES(role_name), name = VALUES(name), phone = VALUES(phone), login_provider = VALUES(login_provider), phone_bound = VALUES(phone_bound), status = VALUES(status);

INSERT INTO point_expiry_rule (id, title, valid_days, description, reminder) VALUES
(1, '积分有效期说明', 180, '积分自实际到账日起 180 天内有效，仅可兑换课程、测评和学习服务权益，不可提现或转赠。', '系统会在到期前 30 天进入“将过期”口径，运营可通过积分管理页提醒家长尽快兑换。')
ON DUPLICATE KEY UPDATE title = VALUES(title), valid_days = VALUES(valid_days), description = VALUES(description), reminder = VALUES(reminder);

INSERT INTO referral_rules (event, title, inviter, invitee, sort_order) VALUES
('registered', '新家长注册', 20, 20, 1),
('booked', '预约试听', 50, 50, 2),
('attended', '确认到课', 150, 100, 3),
('purchased', '报名正价课', 800, 500, 4)
ON DUPLICATE KEY UPDATE title = VALUES(title), inviter = VALUES(inviter), invitee = VALUES(invitee), sort_order = VALUES(sort_order);

INSERT INTO rewards (id, name, cost, type, stock, description) VALUES
('reward_001', '精品体验课 1 次', 800, '课程权益', 30, '适用于校区公开试听课，审核通过后生成课程券码，30 天内到店核销。'),
('reward_002', '学习规划咨询', 500, '服务权益', 50, '由课程顾问提供一次学习规划沟通，适合新生测评后使用。'),
('reward_003', '正价课抵扣券 100 元', 1000, '抵扣券', 20, '报名正价课程时抵扣 100 元，不可叠加现金活动使用。'),
('reward_004', '阶段测评报告', 300, '测评', 80, '兑换后安排一次阶段测评报告解读，帮助家长了解学习状态。')
ON DUPLICATE KEY UPDATE name = VALUES(name), cost = VALUES(cost), type = VALUES(type), stock = VALUES(stock), description = VALUES(description);
ɘA2x)100644 20260630_add_wechat_phone_binding_fields.sql "%Gưe"TSUa100644 20260630_clear_demo_seed_data.sql w)
^,RX100644 20260630_clear_parent_permission_codes.sql R*WI$hLqYF100644 20260701_add_points_rule_permission.sql P	%_Hy6#p100644 20260701_appointment_unique_user.sql gE'{Xt100644 20260701_redemption_voucher_workflow.sql ttKމIPQ100644 20260701_reward_description_and_stock.sql щryp
5100644 20260701_update_rbac_permissions.sql ^SS{(2C100644 20260703_add_lead_referral_identity.sql .2]cG100644 20260709_add_point_transaction_reference.sql NAhljlO{G100644 20260721_normalize_permission_codes.sql M
JРcl0;#100644 20260722_account_trial_booking_limit.sql 8"@ ]i.vV(:zxnUSE lifanghe_education;

ALTER TABLE users MODIFY password_hash CHAR(64) NULL;
ALTER TABLE users MODIFY phone VARCHAR(30) NULL;

SET @schema_name = DATABASE();

SET @sql = (
  SELECT IF(COUNT(*) = 0,
    'ALTER TABLE users ADD COLUMN wechat_openid VARCHAR(80) NULL AFTER phone',
    'SELECT 1'
  )
  FROM INFORMATION_SCHEMA.COLUMNS
  WHERE TABLE_SCHEMA = @schema_name AND TABLE_NAME = 'users' AND COLUMN_NAME = 'wechat_openid'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

SET @sql = (
  SELECT IF(COUNT(*) = 0,
    'ALTER TABLE users ADD COLUMN wechat_unionid VARCHAR(80) NULL AFTER wechat_openid',
    'SELECT 1'
  )
  FROM INFORMATION_SCHEMA.COLUMNS
  WHERE TABLE_SCHEMA = @schema_name AND TABLE_NAME = 'users' AND COLUMN_NAME = 'wechat_unionid'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

SET @sql = (
  SELECT IF(COUNT(*) = 0,
    'ALTER TABLE users ADD COLUMN wechat_nickname VARCHAR(80) NULL AFTER wechat_unionid',
    'SELECT 1'
  )
  FROM INFORMATION_SCHEMA.COLUMNS
  WHERE TABLE_SCHEMA = @schema_name AND TABLE_NAME = 'users' AND COLUMN_NAME = 'wechat_nickname'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

SET @sql = (
  SELECT IF(COUNT(*) = 0,
    'ALTER TABLE users ADD COLUMN wechat_avatar VARCHAR(500) NULL AFTER wechat_nickname',
    'SELECT 1'
  )
  FROM INFORMATION_SCHEMA.COLUMNS
  WHERE TABLE_SCHEMA = @schema_name AND TABLE_NAME = 'users' AND COLUMN_NAME = 'wechat_avatar'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

SET @sql = (
  SELECT IF(COUNT(*) = 0,
    'ALTER TABLE users ADD COLUMN login_provider VARCHAR(30) NOT NULL DEFAULT ''password'' AFTER wechat_avatar',
    'SELECT 1'
  )
  FROM INFORMATION_SCHEMA.COLUMNS
  WHERE TABLE_SCHEMA = @schema_name AND TABLE_NAME = 'users' AND COLUMN_NAME = 'login_provider'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

SET @sql = (
  SELECT IF(COUNT(*) = 0,
    'ALTER TABLE users ADD COLUMN phone_bound TINYINT(1) NOT NULL DEFAULT 1 AFTER login_provider',
    'SELECT 1'
  )
  FROM INFORMATION_SCHEMA.COLUMNS
  WHERE TABLE_SCHEMA = @schema_name AND TABLE_NAME = 'users' AND COLUMN_NAME = 'phone_bound'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

SET @sql = (
  SELECT IF(COUNT(*) = 0,
    'ALTER TABLE users ADD COLUMN phone_bound_at VARCHAR(32) NULL AFTER phone_bound',
    'SELECT 1'
  )
  FROM INFORMATION_SCHEMA.COLUMNS
  WHERE TABLE_SCHEMA = @schema_name AND TABLE_NAME = 'users' AND COLUMN_NAME = 'phone_bound_at'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

SET @sql = (
  SELECT IF(COUNT(*) = 0,
    'ALTER TABLE users ADD COLUMN wechat_bound_at VARCHAR(32) NULL AFTER phone_bound_at',
    'SELECT 1'
  )
  FROM INFORMATION_SCHEMA.COLUMNS
  WHERE TABLE_SCHEMA = @schema_name AND TABLE_NAME = 'users' AND COLUMN_NAME = 'wechat_bound_at'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

SET @sql = (
  SELECT IF(COUNT(*) = 0,
    'ALTER TABLE users ADD UNIQUE KEY uk_users_wechat_openid (wechat_openid)',
    'SELECT 1'
  )
  FROM INFORMATION_SCHEMA.STATISTICS
  WHERE TABLE_SCHEMA = @schema_name AND TABLE_NAME = 'users' AND COLUMN_NAME = 'wechat_openid' AND NON_UNIQUE = 0
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

SET @sql = (
  SELECT IF(COUNT(*) = 0,
    'ALTER TABLE users ADD UNIQUE KEY uk_users_wechat_unionid (wechat_unionid)',
    'SELECT 1'
  )
  FROM INFORMATION_SCHEMA.STATISTICS
  WHERE TABLE_SCHEMA = @schema_name AND TABLE_NAME = 'users' AND COLUMN_NAME = 'wechat_unionid' AND NON_UNIQUE = 0
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

SET @sql = (
  SELECT IF(COUNT(*) = 0,
    'ALTER TABLE users ADD INDEX idx_users_login_provider (login_provider)',
    'SELECT 1'
  )
  FROM INFORMATION_SCHEMA.STATISTICS
  WHERE TABLE_SCHEMA = @schema_name AND TABLE_NAME = 'users' AND INDEX_NAME = 'idx_users_login_provider'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

UPDATE users
SET
  login_provider = COALESCE(login_provider, 'password'),
  phone_bound = IF(phone IS NULL OR phone = '', 0, 1),
  phone_bound_at = IF((phone IS NULL OR phone = ''), NULL, COALESCE(phone_bound_at, last_login_at))
WHERE id IS NOT NULL;
x<x=USE lifanghe_education;

DELETE FROM redemption_orders WHERE user_id IN ('parent_001', 'parent_002', 'parent_003');
DELETE FROM appointments WHERE user_id IN ('parent_001', 'parent_002', 'parent_003');
DELETE FROM referrals WHERE id IN ('rel_001', 'rel_002', 'rel_003');
DELETE FROM point_expiry_schedules WHERE id IN ('exp_001', 'exp_002', 'exp_003');
DELETE FROM point_transactions WHERE id IN ('pt_001', 'pt_002', 'pt_003', 'pt_004', 'pt_005');
DELETE FROM point_accounts WHERE user_id IN ('parent_001', 'parent_002', 'parent_003');
DELETE FROM leads WHERE id IN ('lead_001', 'lead_002', 'lead_003', 'lead_004', 'lead_005');
DELETE FROM courses WHERE id IN ('course_001', 'course_002', 'course_003', 'course_004');
DELETE FROM channel_stats WHERE channel IN ('朋友圈海报', '微信好友', 'H5链接', '顾问二维码');
DELETE FROM users WHERE id IN ('parent_001', 'parent_002', 'parent_003', 'campus_001', 'advisor_001', 'teacher_001', 'finance_001');
-rx*ALTER TABLE redemption_orders
  ADD COLUMN cost_points INT NOT NULL DEFAULT 0 AFTER status,
  ADD COLUMN voucher_code VARCHAR(60) NULL UNIQUE AFTER cost_points,
  ADD COLUMN voucher_status VARCHAR(40) NOT NULL DEFAULT 'not_issued' AFTER voucher_code,
  ADD COLUMN reviewed_by VARCHAR(40) NULL AFTER created_at,
  ADD COLUMN reviewed_at VARCHAR(40) NULL AFTER reviewed_by,
  ADD COLUMN rejected_reason VARCHAR(255) NULL AFTER reviewed_at,
  ADD COLUMN expire_at VARCHAR(40) NULL AFTER rejected_reason,
  ADD COLUMN used_at VARCHAR(40) NULL AFTER expire_at,
  ADD COLUMN used_by VARCHAR(40) NULL AFTER used_at,
  ADD COLUMN verify_note VARCHAR(255) NULL AFTER used_by,
  ADD INDEX idx_redemption_status (status),
  ADD INDEX idx_redemption_voucher (voucher_code);

UPDATE redemption_orders ro
LEFT JOIN rewards r ON r.id = ro.item_id
SET ro.cost_points = COALESCE(NULLIF(ro.cost_points, 0), r.cost, 0),
    ro.voucher_status = CASE
      WHEN ro.status = 'approved' THEN 'issued'
      WHEN ro.status = 'rejected' THEN 'void'
      ELSE ro.voucher_status
    END;

UPDATE roles
SET permissions = JSON_ARRAY(
  'dashboard:view',
  'system:user:view',
  'system:role:view',
  'leads:view',
  'leads:advance',
  'points:view',
  'points:redemption:view',
  'points:redemption:review',
  'points:voucher:verify',
  'courses:view',
  'courses:add',
  'courses:edit',
  'statistics:view'
)
WHERE role = 'campus_admin';

UPDATE roles
SET permissions = JSON_ARRAY('dashboard:view', 'leads:view', 'leads:advance', 'courses:view', 'points:view', 'points:redemption:view')
WHERE role = 'advisor';

UPDATE roles
SET permissions = JSON_ARRAY(
  'dashboard:view',
  'points:view',
  'points:adjust',
  'points:reward',
  'points:redemption:view',
  'points:redemption:review',
  'points:voucher:verify',
  'statistics:view'
)
WHERE role = 'finance';
	BO7x 4USE lifanghe_education-)
WHERE role = 'parent/*:*:*')
WHERE role = 'superz4[ihuleE!coursesteacherA4adjust', 'points:rule', 'points:reward', 'statistics 
*MAeZx \\lBxUSE lifanghe_education;

UPDATE roles
SET permissions = JSON_ARRAY_APPEND(permissions, '$', 'points:rule')
WHERE role IN ('campus_admin', 'finance')
  AND JSON_CONTAINS(permissions, JSON_QUOTE('points:rule')) = 0
  AND JSON_CONTAINS(permissions, JSON_QUOTE('*:*:*')) = 0;
T$?x USE lifanghe_education;

CREATE TABLE IF NOT EXISTS appointment_duplicates_backup LIKE appointments;

INSERT INTO appointment_duplicates_backup
SELECT a.*
FROM appointments a
JOIN appointments b
  ON a.user_id = b.user_id
 AND (a.created_at < b.created_at OR (a.created_at = b.created_at AND a.id < b.id))
LEFT JOIN appointment_duplicates_backup bak ON bak.id = a.id
WHERE bak.id IS NULL;

DELETE a
FROM appointments a
JOIN appointments b
  ON a.user_id = b.user_id
 AND (a.created_at < b.created_at OR (a.created_at = b.created_at AND a.id < b.id));

SET @schema_name = DATABASE();
SET @has_unique := (
  SELECT COUNT(1)
  FROM INFORMATION_SCHEMA.STATISTICS
  WHERE TABLE_SCHEMA = @schema_name
    AND TABLE_NAME = 'appointments'
    AND INDEX_NAME = 'uk_appointments_user'
);
SET @add_unique_sql := IF(
  @has_unique = 0,
  'ALTER TABLE appointments ADD UNIQUE KEY uk_appointments_user (user_id)',
  'SELECT 1'
);
PREPARE add_unique_stmt FROM @add_unique_sql;
EXECUTE add_unique_stmt;
DEALLOCATE PREPARE add_unique_stmt;
C.xALTER TABLE rewards
  ADD COLUMN description VARCHAR(500) NOT NULL DEFAULT '' AFTER stock;

UPDATE rewards
SET stock = CASE id
  WHEN 'reward_001' THEN 30
  WHEN 'reward_002' THEN 50
  WHEN 'reward_003' THEN 20
  WHEN 'reward_004' THEN 80
  ELSE stock
END,
description = CASE id
  WHEN 'reward_001' THEN '适用于校区公开试听课，审核通过后生成课程券码，30 天内到店核销。'
  WHEN 'reward_002' THEN '由课程顾问提供一次学习规划沟通，适合新生测评后使用。'
  WHEN 'reward_003' THEN '报名正价课程时抵扣 100 元，不可叠加现金活动使用。'
  WHEN 'reward_004' THEN '兑换后安排一次阶段测评报告解读，帮助家长了解学习状态。'
  ELSE description
END;
MR0xALTER TABLE leads
  ADD COLUMN referral_id VARCHAR(40) NULL AFTER id,
  ADD COLUMN invitee_id VARCHAR(40) NULL AFTER referral_id,
  ADD INDEX idx_leads_referral (referral_id),
  ADD INDEX idx_leads_invitee (invitee_id);

UPDATE leads l
JOIN referrals r
JOIN users inviter ON inviter.id = r.inviter_id
LEFT JOIN users invitee ON invitee.id = r.invitee_id
SET l.referral_id = r.id,
    l.invitee_id = r.invitee_id,
    l.parent = COALESCE(invitee.name, r.name)
WHERE l.referral_id IS NULL
  AND (
    l.parent = r.name
    OR l.parent = COALESCE(invitee.name, r.name)
    OR l.parent = CONCAT('家长', RIGHT(r.phone, 4))
  )
  AND (l.source = inviter.name OR l.source = inviter.id OR l.source = inviter.phone)
  AND (l.channel = r.channel OR l.channel = '' OR r.channel = '');
v=?xU!	XJpoint_transactions ADD COLUMN reference_type VARCHAR(40) NULL AFTER status@	npoint_transaction`reference_type}Ppoint_transactions ADD COLUMN reference_id VARCHAR(60) NULL AFTER reference_type@	npoint_transaction`reference_i~\point_transactions ADD INDEX idx_point_transactions_reference (reference_type, reference_id)@	0AFpoint_transactions' AND INDEX_NAME = 'idx_point_transactions_referenceDγx	UPDATE roles
SET permissions = JSON_REPLACE(permissions, JSON_UNQUOTE(JSON_SEARCH(permissions, 'one', 'dashboard:view')), 'dashboard:overview:view')
WHERE JSON_SEARCH(permissions, 'one', 'dashboard:view') IS NOT NULL;

UPDATE roles
SET permissions = JSON_REPLACE(permissions, JSON_UNQUOTE(JSON_SEARCH(permissions, 'one', 'leads:view')), 'leads:referral:view')
WHERE JSON_SEARCH(permissions, 'one', 'leads:view') IS NOT NULL;

UPDATE roles
SET permissions = JSON_REPLACE(permissions, JSON_UNQUOTE(JSON_SEARCH(permissions, 'one', 'leads:advance')), 'leads:referral:advance')
WHERE JSON_SEARCH(permissions, 'one', 'leads:advance') IS NOT NULL;

UPDATE roles
SET permissions = JSON_REPLACE(permissions, JSON_UNQUOTE(JSON_SEARCH(permissions, 'one', 'points:view')), 'points:account:view')
WHERE JSON_SEARCH(permissions, 'one', 'points:view') IS NOT NULL;

UPDATE roles
SET permissions = JSON_REPLACE(permissions, JSON_UNQUOTE(JSON_SEARCH(permissions, 'one', 'points:adjust')), 'points:account:adjust')
WHERE JSON_SEARCH(permissions, 'one', 'points:adjust') IS NOT NULL;

UPDATE roles
SET permissions = JSON_REPLACE(permissions, JSON_UNQUOTE(JSON_SEARCH(permissions, 'one', 'points:rule')), 'points:rule:edit')
WHERE JSON_SEARCH(permissions, 'one', 'points:rule') IS NOT NULL;

UPDATE roles
SET permissions = JSON_REPLACE(permissions, JSON_UNQUOTE(JSON_SEARCH(permissions, 'one', 'points:reward')), 'points:reward:edit')
WHERE JSON_SEARCH(permissions, 'one', 'points:reward') IS NOT NULL;

UPDATE roles
SET permissions = JSON_REPLACE(permissions, JSON_UNQUOTE(JSON_SEARCH(permissions, 'one', 'courses:view')), 'courses:trial:view')
WHERE JSON_SEARCH(permissions, 'one', 'courses:view') IS NOT NULL;

UPDATE roles
SET permissions = JSON_REPLACE(permissions, JSON_UNQUOTE(JSON_SEARCH(permissions, 'one', 'courses:add')), 'courses:trial:add')
WHERE JSON_SEARCH(permissions, 'one', 'courses:add') IS NOT NULL;

UPDATE roles
SET permissions = JSON_REPLACE(permissions, JSON_UNQUOTE(JSON_SEARCH(permissions, 'one', 'courses:edit')), 'courses:trial:edit')
WHERE JSON_SEARCH(permissions, 'one', 'courses:edit') IS NOT NULL;

UPDATE roles
SET permissions = JSON_REPLACE(permissions, JSON_UNQUOTE(JSON_SEARCH(permissions, 'one', 'courses:delete')), 'courses:trial:delete')
WHERE JSON_SEARCH(permissions, 'one', 'courses:delete') IS NOT NULL;

UPDATE roles
SET permissions = JSON_REPLACE(permissions, JSON_UNQUOTE(JSON_SEARCH(permissions, 'one', 'statistics:view')), 'statistics:channel:view')
WHERE JSON_SEARCH(permissions, 'one', 'statistics:view') IS NOT NULL;
[8lgxxUSE lifanghe_education;

SET @schema_name = DATABASE();

SET @has_trial_booking_limit := (
  SELECT COUNT(1)
  FROM INFORMATION_SCHEMA.COLUMNS
  WHERE TABLE_SCHEMA = @schema_name
    AND TABLE_NAME = 'users'
    AND COLUMN_NAME = 'trial_booking_limit'
);
SET @add_trial_booking_limit_sql := IF(
  @has_trial_booking_limit = 0,
  'ALTER TABLE users ADD COLUMN trial_booking_limit INT NOT NULL DEFAULT 1 AFTER child_focus',
  'SELECT 1'
);
PREPARE add_trial_booking_limit_stmt FROM @add_trial_booking_limit_sql;
EXECUTE add_trial_booking_limit_stmt;
DEALLOCATE PREPARE add_trial_booking_limit_stmt;

SET @has_unique_user_index := (
  SELECT COUNT(1)
  FROM INFORMATION_SCHEMA.STATISTICS
  WHERE TABLE_SCHEMA = @schema_name
    AND TABLE_NAME = 'appointments'
    AND INDEX_NAME = 'uk_appointments_user'
);
SET @drop_unique_user_index_sql := IF(
  @has_unique_user_index > 0,
  'ALTER TABLE appointments DROP INDEX uk_appointments_user',
  'SELECT 1'
);
PREPARE drop_unique_user_index_stmt FROM @drop_unique_user_index_sql;
EXECUTE drop_unique_user_index_stmt;
DEALLOCATE PREPARE drop_unique_user_index_stmt;

SET @has_appointment_user_index := (
  SELECT COUNT(1)
  FROM INFORMATION_SCHEMA.STATISTICS
  WHERE TABLE_SCHEMA = @schema_name
    AND TABLE_NAME = 'appointments'
    AND INDEX_NAME = 'idx_appointments_user'
);
SET @add_appointment_user_index_sql := IF(
  @has_appointment_user_index = 0,
  'ALTER TABLE appointments ADD INDEX idx_appointments_user (user_id)',
  'SELECT 1'
);
PREPARE add_appointment_user_index_stmt FROM @add_appointment_user_index_sql;
EXECUTE add_appointment_user_index_stmt;
DEALLOCATE PREPARE add_appointment_user_index_stmt;
<Ovx~100644 app.js Gg&Jd5N7`YP40000 config 0+,fiu̻m40000 constants vM`0&[oO̱D640000 controllers ʢ8]f\afS%40000 data 7iNH.n340000 middleware pq~4$$7( 40000 repositories 1r[<Ef7I|	o~40000 routes pe|-q7\B100644 server.js rΧA]1{vt֜40000 services <dp.c
n40000 utils ~[sm
ɾ}w{5tUSx>const express = require('express')
const cors = require('cors')
const morgan = require('morgan')
const fs = require('fs')
const path = require('path')
const { env } = require('./config/env')
const { router } = require('./routes')
const { errorHandler } = require('./middleware/errorHandler')

const app = express()

app.disable('etag')
app.use(cors())
app.use(express.json())
app.use(morgan('dev'))
app.use('/uploads', express.static(path.resolve(__dirname, '../uploads')))

const pcAdminDistDir = env.pcAdminDistDir
  ? path.resolve(env.pcAdminDistDir)
  : path.resolve(__dirname, '../public/pc-admin')
const pcAdminIndex = path.join(pcAdminDistDir, 'index.html')
const hasPcAdmin = fs.existsSync(pcAdminIndex)
if (hasPcAdmin) {
  app.use(express.static(pcAdminDistDir))
}

app.get('/health', (req, res) => {
  res.json({ ok: true, service: 'lifanghe-education-api' })
})

app.use('/api', (req, res, next) => {
  res.set({
    'Cache-Control': 'no-store, no-cache, must-revalidate, proxy-revalidate',
    Pragma: 'no-cache',
    Expires: '0',
    'Surrogate-Control': 'no-store',
  })
  next()
}, router)

if (hasPcAdmin) {
  app.get('*', (req, res, next) => {
    if (req.path.startsWith('/api') || req.path.startsWith('/uploads')) return next()
    return res.sendFile(pcAdminIndex)
  })
}

app.use(errorHandler)

module.exports = { app }
4x" 100644 env.js  )FGyla'9 x(const fs = require('fs')
const path = require('path')

function loadDotEnv(filePath) {
  if (!fs.existsSync(filePath)) return
  const lines = fs.readFileSync(filePath, 'utf8').split(/\r?\n/)
  for (const line of lines) {
    const trimmed = line.trim()
    if (!trimmed || trimmed.startsWith('#')) continue
    const match = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/)
    if (!match) continue
    const key = match[1]
    let value = match[2].trim()
    if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
      value = value.slice(1, -1)
    }
    if (process.env[key] === undefined || process.env[key] === '') {
      process.env[key] = value
    }
  }
}

loadDotEnv(path.resolve(__dirname, '../../.env'))

function readBool(value, fallback = false) {
  if (value === undefined || value === '') return fallback
  return ['1', 'true', 'yes', 'on'].includes(String(value).trim().toLowerCase())
}

const nodeEnv = process.env.NODE_ENV || 'development'
const sessionSecret = process.env.AUTH_SESSION_SECRET || 'lifanghe-development-session-secret'
if (nodeEnv === 'production' && (!process.env.AUTH_SESSION_SECRET || sessionSecret.length < 32)) {
  throw new Error('生产环境必须配置至少 32 位的 AUTH_SESSION_SECRET')
}

const env = {
  nodeEnv,
  port: Number(process.env.PORT || 3100),
  publicBaseUrl: process.env.PUBLIC_BASE_URL || '',
  pcAdminDistDir: process.env.PC_ADMIN_DIST_DIR || '',
  dbClient: process.env.DB_CLIENT || (process.env.MYSQL_HOST ? 'mysql' : 'memory'),
  auth: {
    sessionSecret,
    sessionTtlHours: Number(process.env.AUTH_SESSION_TTL_HOURS || 168),
  },
  mysql: {
    host: process.env.MYSQL_HOST || '127.0.0.1',
    port: Number(process.env.MYSQL_PORT || 3306),
    user: process.env.MYSQL_USER || 'lifanghe',
    password: process.env.MYSQL_PASSWORD || 'lifanghe123',
    database: process.env.MYSQL_DATABASE || 'lifanghe_education',
    connectionLimit: Number(process.env.MYSQL_CONNECTION_LIMIT || 10),
  },
  wechat: {
    appId: process.env.WECHAT_APP_ID || '',
    appSecret: process.env.WECHAT_APP_SECRET || '',
    envVersion: process.env.WECHAT_ENV_VERSION || 'release',
  },
  sms: {
    provider: process.env.SMS_PROVIDER || 'aliyun',
    allowTestCode: readBool(process.env.SMS_ALLOW_TEST_CODE, true),
    testCode: process.env.SMS_TEST_CODE || '111111',
    codeLength: Number(process.env.SMS_CODE_LENGTH || 6),
    codeTtlSeconds: Number(process.env.SMS_CODE_TTL_SECONDS || 300),
    codeCooldownSeconds: Number(process.env.SMS_CODE_COOLDOWN_SECONDS || 60),
    aliyun: {
      endpoint: process.env.ALIYUN_SMS_ENDPOINT || 'dysmsapi.aliyuncs.com',
      accessKeyId: process.env.ALIYUN_ACCESS_KEY_ID || '',
      accessKeySecret: process.env.ALIYUN_ACCESS_KEY_SECRET || '',
      signName: process.env.ALIYUN_SMS_SIGN_NAME || '',
      templateCode: process.env.ALIYUN_SMS_TEMPLATE_CODE || '',
      templateParamKey: process.env.ALIYUN_SMS_TEMPLATE_PARAM_KEY || 'code',
    },
  },
}

module.exports = { env }
Ѫx* 100644 permissions.js K
Lp-<A3*C޼xconst PERMISSION_TREE = [
  {
    id: 'dashboard',
    label: '数据驾驶舱',
    children: [
      { id: 'dashboard:overview:view', label: '查看驾驶舱', permission: 'dashboard:overview:view' },
    ],
  },
  {
    id: 'system',
    label: '系统管理',
    children: [
      {
        id: 'system:user',
        label: '用户管理',
        children: [
          { id: 'system:user:view', label: '查看用户', permission: 'system:user:view' },
          { id: 'system:user:add', label: '新增用户', permission: 'system:user:add' },
          { id: 'system:user:edit', label: '编辑用户', permission: 'system:user:edit' },
          { id: 'system:user:delete', label: '删除用户', permission: 'system:user:delete' },
          { id: 'system:user:role', label: '分配角色', permission: 'system:user:role' },
          { id: 'system:user:status', label: '启停账号', permission: 'system:user:status' },
        ],
      },
      {
        id: 'system:role',
        label: '角色权限',
        children: [
          { id: 'system:role:view', label: '查看角色', permission: 'system:role:view' },
          { id: 'system:role:edit', label: '配置权限', permission: 'system:role:edit' },
        ],
      },
    ],
  },
  {
    id: 'leads',
    label: '线索管理',
    children: [
      { id: 'leads:referral:view', label: '查看线索', permission: 'leads:referral:view' },
      { id: 'leads:referral:advance', label: '推进线索', permission: 'leads:referral:advance' },
    ],
  },
  {
    id: 'points',
    label: '积分管理',
    children: [
      { id: 'points:account:view', label: '查看积分', permission: 'points:account:view' },
      { id: 'points:account:adjust', label: '调整积分', permission: 'points:account:adjust' },
      { id: 'points:rule:edit', label: '维护积分规则', permission: 'points:rule:edit' },
      { id: 'points:reward:edit', label: '维护兑换项', permission: 'points:reward:edit' },
      { id: 'points:redemption:view', label: '查看兑换申请', permission: 'points:redemption:view' },
      { id: 'points:redemption:review', label: '审核兑换申请', permission: 'points:redemption:review' },
      { id: 'points:voucher:verify', label: '核销权益券码', permission: 'points:voucher:verify' },
    ],
  },
  {
    id: 'courses',
    label: '课程管理',
    children: [
      { id: 'courses:trial:view', label: '查看课程', permission: 'courses:trial:view' },
      { id: 'courses:trial:add', label: '新增课程', permission: 'courses:trial:add' },
      { id: 'courses:trial:edit', label: '编辑课程', permission: 'courses:trial:edit' },
      { id: 'courses:trial:delete', label: '删除课程', permission: 'courses:trial:delete' },
    ],
  },
  {
    id: 'statistics',
    label: '数据统计',
    children: [
      { id: 'statistics:channel:view', label: '查看统计', permission: 'statistics:channel:view' },
    ],
  },
]

const ROLE_PERMISSIONS = {
  parent: [],
  super_admin: ['*:*:*'],
  campus_admin: [
    'dashboard:overview:view',
    'system:user:view',
    'system:role:view',
    'leads:referral:view',
    'leads:referral:advance',
    'points:account:view',
    'points:rule:edit',
    'points:redemption:view',
    'points:redemption:review',
    'points:voucher:verify',
    'courses:trial:view',
    'courses:trial:add',
    'courses:trial:edit',
    'statistics:channel:view',
  ],
  advisor: ['dashboard:overview:view', 'leads:referral:view', 'leads:referral:advance', 'courses:trial:view', 'points:account:view', 'points:redemption:view'],
  teacher: ['dashboard:overview:view', 'leads:referral:view', 'courses:trial:view'],
  finance: ['dashboard:overview:view', 'points:account:view', 'points:account:adjust', 'points:rule:edit', 'points:reward:edit', 'points:redemption:view', 'points:redemption:review', 'points:voucher:verify', 'statistics:channel:view'],
}

function flattenPermissions(nodes = PERMISSION_TREE) {
  return nodes.flatMap((node) => {
    const current = node.permission ? [node.permission] : []
    return current.concat(flattenPermissions(node.children || []))
  })
}

const ALL_PERMISSIONS = flattenPermissions()

const LEGACY_PERMISSION_MAP = {
  'dashboard:view': ['dashboard:overview:view'],
  'leads:view': ['leads:referral:view'],
  'leads:advance': ['leads:referral:advance'],
  'points:view': ['points:account:view'],
  'points:adjust': ['points:account:adjust'],
  'points:rule': ['points:rule:edit'],
  'points:reward': ['points:reward:edit'],
  'courses:view': ['courses:trial:view'],
  'courses:add': ['courses:trial:add'],
  'courses:edit': ['courses:trial:edit'],
  'courses:delete': ['courses:trial:delete'],
  'statistics:view': ['statistics:channel:view'],
  账号权限: ['system:user:view', 'system:user:add', 'system:user:edit', 'system:user:delete', 'system:user:role', 'system:user:status', 'system:role:view', 'system:role:edit'],
  活动配置: ['courses:trial:view', 'courses:trial:add', 'courses:trial:edit', 'courses:trial:delete'],
  积分规则: ['points:account:view', 'points:account:adjust', 'points:rule:edit', 'points:reward:edit', 'points:redemption:view', 'points:redemption:review', 'points:voucher:verify'],
  财务数据: ['points:account:view', 'statistics:channel:view'],
  线索分配: ['leads:referral:view', 'leads:referral:advance'],
  校区数据: ['dashboard:overview:view', 'statistics:channel:view', 'courses:trial:view'],
  兑换审核: ['points:account:view', 'points:redemption:view', 'points:redemption:review', 'points:voucher:verify'],
  线索跟进: ['leads:referral:view', 'leads:referral:advance'],
  预约管理: ['courses:trial:view'],
  到课确认: ['leads:referral:view', 'courses:trial:view'],
  课消记录: ['courses:trial:view', 'statistics:channel:view'],
  订单核对: ['statistics:channel:view'],
  积分冲正: ['points:account:view', 'points:account:adjust'],
}

function normalizePermissions(permissions = []) {
  const normalized = permissions.flatMap((item) => LEGACY_PERMISSION_MAP[item] || item)
  return sanitizePermissions(normalized)
}

function hasPermission(account, required = []) {
  const permissions = account?.permissions || []
  if (!required.length) return true
  if (permissions.includes('*:*:*')) return true
  return required.some((item) => permissions.includes(item))
}

function sanitizePermissions(permissions = []) {
  const allowed = new Set(['*:*:*', ...ALL_PERMISSIONS])
  return [...new Set(permissions.filter((item) => allowed.has(item)))]
}

module.exports = {
  ALL_PERMISSIONS,
  PERMISSION_TREE,
  ROLE_PERMISSIONS,
  hasPermission,
  normalizePermissions,
  sanitizePermissions,
}
qXxJ100644 adminController.js cwCnE6E100644 authController.js $C s&` n100644 bootstrapController.js x+
O#100644 courseController.js %W4B!5YeE100644 pointController.js 8x0ygGr</:100644 referralController.js x*!9'PVo-n100644 uploadController.js nZ9칚Y=}u+^hY#} x&const { pointService } = require('../services')

async function getPointCenter(req, res, next) {
  try {
    res.json({ ok: true, data: await pointService.getCenter(req.account.id) })
  } catch (error) {
    next(error)
  }
}

async function redeemReward(req, res, next) {
  try {
    res.status(201).json({ ok: true, data: await pointService.redeem(req.account.id, req.body.itemId) })
  } catch (error) {
    next(error)
  }
}

async function listUserRewards(req, res, next) {
  try {
    res.json({ ok: true, data: await pointService.listUserRewards(req.account.id, req.query) })
  } catch (error) {
    next(error)
  }
}

async function listUserTransactions(req, res, next) {
  try {
    res.json({ ok: true, data: await pointService.listUserTransactions(req.account.id, req.query) })
  } catch (error) {
    next(error)
  }
}

async function listUserRedemptions(req, res, next) {
  try {
    res.json({ ok: true, data: await pointService.listUserRedemptions(req.account.id, req.query) })
  } catch (error) {
    next(error)
  }
}

async function getAdminPointManager(req, res, next) {
  try {
    res.json({ ok: true, data: await pointService.getAdminManager() })
  } catch (error) {
    next(error)
  }
}

async function listPointAccounts(req, res, next) {
  try {
    res.json({ ok: true, data: await pointService.listPointAccounts(req.query) })
  } catch (error) {
    next(error)
  }
}

async function listPointTransactions(req, res, next) {
  try {
    res.json({ ok: true, data: await pointService.listPointTransactions(req.query) })
  } catch (error) {
    next(error)
  }
}

async function adjustPoints(req, res, next) {
  try {
    res.status(201).json({
      ok: true,
      data: await pointService.adjust(req.body.userId, req.body.amount, req.body.title, req.body.status),
    })
  } catch (error) {
    next(error)
  }
}

async function settlePendingPoints(req, res, next) {
  try {
    res.json({
      ok: true,
      data: await pointService.settlePending(req.body.userId, req.body.amount, req.body.title),
    })
  } catch (error) {
    next(error)
  }
}

async function updateReward(req, res, next) {
  try {
    res.json({ ok: true, data: await pointService.updateReward(req.params.id, req.body) })
  } catch (error) {
    next(error)
  }
}

async function updateReferralRule(req, res, next) {
  try {
    res.json({ ok: true, data: await pointService.updateReferralRule(req.params.event, req.body) })
  } catch (error) {
    next(error)
  }
}

async function createReward(req, res, next) {
  try {
    res.status(201).json({ ok: true, data: await pointService.createReward(req.body) })
  } catch (error) {
    next(error)
  }
}

async function listRewards(req, res, next) {
  try {
    res.json({ ok: true, data: await pointService.listRewards(req.query) })
  } catch (error) {
    next(error)
  }
}

async function getRewardDetail(req, res, next) {
  try {
    res.json({ ok: true, data: await pointService.getRewardDetail(req.params.id) })
  } catch (error) {
    next(error)
  }
}

async function listRedemptions(req, res, next) {
  try {
    res.json({ ok: true, data: await pointService.listRedemptions(req.query) })
  } catch (error) {
    next(error)
  }
}

async function approveRedemption(req, res, next) {
  try {
    res.json({ ok: true, data: await pointService.approveRedemption(req.params.id, req.operator?.id || req.body.operatorId, req.body) })
  } catch (error) {
    next(error)
  }
}

async function rejectRedemption(req, res, next) {
  try {
    res.json({ ok: true, data: await pointService.rejectRedemption(req.params.id, req.operator?.id || req.body.operatorId, req.body) })
  } catch (error) {
    next(error)
  }
}

async function verifyVoucher(req, res, next) {
  try {
    res.json({ ok: true, data: await pointService.verifyVoucher({
      ...req.body,
      operatorId: req.operator?.id || req.body.operatorId,
    }) })
  } catch (error) {
    next(error)
  }
}

module.exports = {
  adjustPoints,
  approveRedemption,
  createReward,
  getAdminPointManager,
  getPointCenter,
  getRewardDetail,
  listPointAccounts,
  listPointTransactions,
  listRedemptions,
  listRewards,
  listUserRedemptions,
  listUserRewards,
  listUserTransactions,
  redeemReward,
  rejectRedemption,
  settlePendingPoints,
  updateReward,
  updateReferralRule,
  verifyVoucher,
}
Z?gx!const { admin6	Dashboarda@adminService.getDashboard(A?getLeadQ*adminService.listLea
KupdateLeadStagea@adminService.updateLeadStage
body.stageA?confirmLeadPurchasea@+adminService.confirmLeadPurchase(req.paramsB@	cOverviewa@adminService.getPcOverview
LAccouO,adminService.ge0RgetRoleQ*adminService.getRoles(A?-
olePermissD2adminService.updateRolePermissions(req.params.roleIcreateAccounta#	)adminService.createAccount(m	GupdateAccounta@adminService.updateAccountbody, req.operatorA?deleteAccounta@%adminService.deleteAccount(req.paramsBupdateAccountRolea@adminService.updateAccountRole	body.roleA?updateAccountStatuQ* adminService.updateAccountStatusbody.statusA0NgetDashboard,
  getLeads,
  confirmLeadPurchase,
  updateLeadStage,
  getPcOverview,
  getAccounts,
  getRoles,
  createAccountk,
  updateAccount,
  deleteAccount,
  updateAccountRole,
  updateAccountStatus,
  updateRolePermissions,
}
r'sx!const { auth3logina@authService.login(m	G
phoneLogina@authService.loginByPhone(m	GsendSmsCodea4const data = await authService.sendSmsCode(req.body)/, message: data.message || '验证码已发送'>wechatLogina@authService.loginByWechat(m	GbindWechatPhonea@authService.bindWechatPhone(m	GupdateProfilea@authService.updateProfileIPassworda@authService.updatePasswordIPhonea@authService.updatePhoneCcurrenta@authService.current?Nlogin,
  phoneLogin,
  sendSmsCode,
  wechatLogin,
  bindWechatPhone,
  updateProfile,
  updatePassword,
  updatePhone,
  current,
}
,錻DxKconst { adminService, courseService, referralService, repository } = require('../services')
const { hasPermission } = require('../constants/permissions')

async function getBootstrap(req, res, next) {
  try {
    const profile = await repository.findUserById(req.account.id)
    if (!profile) {
      const error = new Error('账号不存在')
      error.status = 404
      throw error
    }
    const safeProfile = await repository.withPermissions(profile)
    const account = await repository.getPointAccount(profile.id)
    const referralCenter = await referralService.getCenter(profile.id)

    res.json({
      ok: true,
      data: {
        profile: { ...safeProfile, points: account },
        points: account,
        stats: referralCenter.stats,
        referralStats: referralCenter.stats,
        rules: referralCenter.rules,
        courses: await courseService.listCourses(),
        admin: hasPermission(safeProfile, ['dashboard:overview:view']) ? await adminService.getDashboard() : null,
      },
    })
  } catch (error) {
    next(error)
  }
}

module.exports = { getBootstrap }
[pR0rxconst { course3
listCourse!AcourseService.listCourses{IlistAdminCourse!AcourseService.listAdminCourses{M
dminCourse!@courseService.getAdminCourseMlistAppointment!AcourseService.listAppointments{IlistUserAppointment!A1courseService.listUserAppointments(req.account.id1?attendAppointment!@courseService.attendAppointmeOcreateCourse!#)courseService.createCoursePNCourse!@courseService.updateCourseY?deleteCourse!@courseService.deleteCourseM
bookCourse!#):courseService.book({ ...req.body, userId: req.account.id }10	listCourses,
  listAdminCourses,
  listAppointments,
  listUserAppointments,
  attendAppointment,
  bookCourse,
  createCourse,5
  deleteCourse,
  getAdminCourse,
  updateCourse,
}
mxk!const { referral6ReferralCentera@referralService.getCenterYcreateShareMateriala:Z{ share: await referralService.createShare(req.body.channel || '默认', req.account.id) }>createMiniProgramCodea@%referralService.createMiniProgramCodeNbindReferrala#	)referralService.bindReferral(m	8NUgetReferralCenter,
  createShareMaterial,
  createMiniProgramCode,
  bindReferral,
}
bxx\const fs = require('fs')
const path = require('path')
const { createId } = require('../utils/ids')
const { env } = require('../config/env')

const MAX_AVATAR_SIZE = 2 * 1024 * 1024
const AVATAR_DIR = path.resolve(__dirname, '../../uploads/avatars')
const MIME_EXTENSIONS = {
  'image/jpeg': 'jpg',
  'image/png': 'png',
  'image/webp': 'webp',
  'image/gif': 'gif',
}

function ensureAvatarDir() {
  fs.mkdirSync(AVATAR_DIR, { recursive: true })
}

function collectRequest(req) {
  return new Promise((resolve, reject) => {
    const chunks = []
    let size = 0
    req.on('data', (chunk) => {
      size += chunk.length
      if (size > MAX_AVATAR_SIZE + 1024 * 1024) {
        reject(Object.assign(new Error('头像文件不能超过 2MB'), { status: 413 }))
        req.destroy()
        return
      }
      chunks.push(chunk)
    })
    req.on('end', () => resolve(Buffer.concat(chunks)))
    req.on('error', reject)
  })
}

function parseMultipart(buffer, contentType) {
  const boundaryMatch = String(contentType || '').match(/boundary=(?:"([^"]+)"|([^;]+))/i)
  if (!boundaryMatch) {
    const error = new Error('上传格式不正确')
    error.status = 400
    throw error
  }
  const boundary = Buffer.from(`--${boundaryMatch[1] || boundaryMatch[2]}`)
  let cursor = buffer.indexOf(boundary)
  while (cursor >= 0) {
    const next = buffer.indexOf(boundary, cursor + boundary.length)
    if (next < 0) break
    const part = buffer.subarray(cursor + boundary.length + 2, next - 2)
    const headerEnd = part.indexOf(Buffer.from('\r\n\r\n'))
    if (headerEnd > 0) {
      const header = part.subarray(0, headerEnd).toString('utf8')
      const body = part.subarray(headerEnd + 4)
      const fileNameMatch = header.match(/filename="([^"]+)"/i)
      const contentTypeMatch = header.match(/Content-Type:\s*([^\r\n]+)/i)
      if (fileNameMatch && contentTypeMatch) {
        return {
          filename: fileNameMatch[1],
          mime: contentTypeMatch[1].trim().toLowerCase(),
          data: body,
        }
      }
    }
    cursor = next
  }
  const error = new Error('没有找到上传文件')
  error.status = 400
  throw error
}

async function uploadAvatar(req, res, next) {
  try {
    const buffer = await collectRequest(req)
    const file = parseMultipart(buffer, req.headers['content-type'])
    const extension = MIME_EXTENSIONS[file.mime]
    if (!extension) {
      const error = new Error('仅支持 JPG、PNG、WEBP、GIF 图片')
      error.status = 400
      throw error
    }
    if (!file.data.length || file.data.length > MAX_AVATAR_SIZE) {
      const error = new Error('头像文件不能超过 2MB')
      error.status = 413
      throw error
    }

    ensureAvatarDir()
    const filename = `${createId('avatar')}.${extension}`
    const absolutePath = path.join(AVATAR_DIR, filename)
    fs.writeFileSync(absolutePath, file.data)

    const pathUrl = `/uploads/avatars/${filename}`
    const baseUrl = env.publicBaseUrl || `${req.protocol}://${req.get('host')}`
    res.status(201).json({
      ok: true,
      data: {
        filename,
        path: pathUrl,
        url: `${baseUrl}${pathUrl}`,
      },
    })
  } catch (error) {
    next(error)
  }
}

module.exports = { uploadAvatar }
ghx# 100644 seed.js y̎3PG8|CKIxZconst { ROLE_PERMISSIONS } = require('../constants/permissions')

const seed = {
  roles: [
    { role: 'parent', name: '家长', permissions: ROLE_PERMISSIONS.parent },
    { role: 'super_admin', name: '超级管理员', permissions: ROLE_PERMISSIONS.super_admin },
    { role: 'campus_admin', name: '校区管理员', permissions: ROLE_PERMISSIONS.campus_admin },
    { role: 'advisor', name: '课程顾问', permissions: ROLE_PERMISSIONS.advisor },
    { role: 'teacher', name: '老师', permissions: ROLE_PERMISSIONS.teacher },
    { role: 'finance', name: '财务', permissions: ROLE_PERMISSIONS.finance },
  ],
  users: [
    {
      id: 'admin_001',
      username: 'admin',
      passwordHash: '240be518fabd2724ddb6f04eeb1da5967448d7e831c08c8fa822809f74c720a9',
      role: 'super_admin',
      roleName: '超级管理员',
      name: '系统管理员',
      phone: '13800009999',
      status: 'active',
      trialBookingLimit: 1,
      lastLoginAt: '',
      wechatOpenid: null,
      wechatUnionid: null,
      wechatNickname: null,
      wechatAvatar: null,
      loginProvider: 'password',
      phoneBound: true,
      phoneBoundAt: '',
      wechatBoundAt: null,
    },
  ],
  pointAccounts: [],
  pointTransactions: [],
  pointExpiryRule: {
    title: '积分有效期说明',
    validDays: 180,
    description: '积分自实际到账日起 180 天内有效，仅可兑换课程、测评和学习服务权益，不可提现或转赠。',
    reminder: '系统会在到期前 30 天进入“将过期”口径，运营可通过积分管理页提醒家长尽快兑换。',
  },
  pointExpirySchedules: [],
  referralRules: [
    { event: 'registered', title: '新家长注册', inviter: 20, invitee: 20 },
    { event: 'booked', title: '预约试听', inviter: 50, invitee: 50 },
    { event: 'attended', title: '确认到课', inviter: 150, invitee: 100 },
    { event: 'purchased', title: '报名正价课', inviter: 800, invitee: 500 },
  ],
  referrals: [],
  courses: [],
  rewards: [
    { id: 'reward_001', name: '精品体验课 1 次', cost: 800, type: '课程权益', stock: 30, description: '适用于校区公开试听课，审核通过后生成课程券码，30 天内到店核销。' },
    { id: 'reward_002', name: '学习规划咨询', cost: 500, type: '服务权益', stock: 50, description: '由课程顾问提供一次学习规划沟通，适合新生测评后使用。' },
    { id: 'reward_003', name: '正价课抵扣券 100 元', cost: 1000, type: '抵扣券', stock: 20, description: '报名正价课程时抵扣 100 元，不可叠加现金活动使用。' },
    { id: 'reward_004', name: '阶段测评报告', cost: 300, type: '测评', stock: 80, description: '兑换后安排一次阶段测评报告解读，帮助家长了解学习状态。' },
  ],
  appointments: [],
  redemptionOrders: [],
  leads: [],
  channelStats: [],
}

module.exports = { seed }
Uxz 100644 errorHandler.js !kvDioD>100644 permission.js t"m(i@100644 session.js thb1VH3{@NW36xfunction errorHandler(error, req, res, next) {
  if (res.headersSent) {
    next(error)
    return
  }

  const status = error.status || 500
  res.status(status).json({
    ok: false,
    message: error.message || '服务器异常',
  })
}

module.exports = { errorHandler }
_R"x$const { hasPermission } = require('../constants/permissions')
const { authenticateRequest } = require('./session')

function requirePermission(...required) {
  return async (req, res, next) => {
    try {
      const account = await authenticateRequest(req)
      if (!hasPermission(account, required)) {
        const error = new Error('没有操作权限')
        error.status = 403
        throw error
      }
      req.operator = account
      next()
    } catch (error) {
      next(error)
    }
  }
}

module.exports = { requirePermission }
U]x)const { repository } = require('../repositories')
const { verifySessionToken } = require('../utils/sessionToken')

function readSessionToken(req) {
  const authorization = String(req.get('authorization') || '').trim()
  if (authorization.toLowerCase().startsWith('bearer ')) return authorization.slice(7).trim()
  return String(req.get('x-session-token') || '').trim()
}

async function authenticateRequest(req) {
  if (req.account) return req.account
  const payload = verifySessionToken(readSessionToken(req))
  if (!payload) {
    const error = new Error('登录状态无效或已过期，请重新登录')
    error.status = 401
    throw error
  }
  const account = await repository.withPermissions(await repository.findUserById(payload.sub))
  if (!account || account.status !== 'active') {
    const error = new Error('账号不存在或已停用')
    error.status = 403
    throw error
  }
  req.account = account
  return account
}

async function requireSession(req, res, next) {
  try {
    await authenticateRequest(req)
    next()
  } catch (error) {
    next(error)
  }
}

function requireSelf(source, key) {
  return (req, res, next) => {
    const targetId = String(req[source]?.[key] || '').trim()
    if (targetId && targetId !== req.account?.id) {
      const error = new Error('不能访问或修改其他用户的数据')
      error.status = 403
      next(error)
      return
    }
    next()
  }
}

module.exports = { authenticateRequest, requireSelf, requireSession }
x ~100644 index.js '&aݵ100644 memoryRepository.js 83("qr~100644 mysqlRepository.js sʜy 1Y@qh6"x"const { env } = require('../config/env')
const { repository: memoryRepository } = require('./memoryRepository')
const { MySqlRepository } = require('./mysqlRepository')

const repository = env.dbClient === 'mysql' ? new MySqlRepository() : memoryRepository

module.exports = { repository }
8g	xKconst { seed } = require('../data/seed')
const { hashPassword } = require('../utils/password')
const { normalizePermissions } = require('../constants/permissions')

class MemoryRepository {
  constructor(initialState) {
    this.state = structuredClone(initialState)
  }

  findUserById(id) {
    return this.state.users.find((user) => user.id === id)
  }

  findUserByInviteCode(inviterCode) {
    return this.state.users.find((user) => user.inviterCode === inviterCode)
  }

  findUserByPhone(phone) {
    return this.state.users.find((user) => user.phone === phone)
  }

  findUserByLoginAccount(account) {
    const value = String(account || '').trim()
    return this.state.users.find((user) => [user.username, user.phone, user.id, user.wechatOpenid, user.wechatUnionid].filter(Boolean).includes(value))
  }

  listAccounts() {
    return this.state.users
      .map((user) => this.withPermissions(user))
  }

  listParentUsers() {
    return this.state.users
      .filter((user) => user.role === 'parent')
      .map((user) => this.withPermissions(user))
  }

  getRoleMatrix() {
    return this.state.roles.filter((role) => role.role !== 'parent').map((role) => ({
      ...role,
      permissions: normalizePermissions(role.permissions || []),
    }))
  }

  findRole(roleName) {
    return this.state.roles.find((item) => item.role === roleName)
  }

  updateRolePermissions(roleName, permissions) {
    const role = this.findRole(roleName)
    if (!role) return null
    role.permissions = [...(permissions || [])]
    return role
  }

  withPermissions(user) {
    const role = this.findRole(user.role)
    const hasPassword = Boolean(user.passwordHash)
    return {
      ...user,
      trialBookingLimit: Number.isInteger(Number(user.trialBookingLimit)) ? Number(user.trialBookingLimit) : 1,
      passwordHash: undefined,
      hasPassword,
      roleName: role?.name || user.roleName || user.role,
      permissions: user.role === 'parent' ? [] : normalizePermissions(role?.permissions || []),
    }
  }

  touchLogin(id) {
    const user = this.findUserById(id)
    if (!user) return null
    user.lastLoginAt = new Date().toISOString().slice(0, 16).replace('T', ' ')
    return this.withPermissions(user)
  }

  addUser(user) {
    if (!user.username) user.username = user.phone || user.id
    if (!user.passwordHash && user.password) user.passwordHash = hashPassword(user.password)
    if (user.passwordHash === undefined) user.passwordHash = null
    user.phone = user.phone || null
    user.wechatOpenid = user.wechatOpenid || null
    user.wechatUnionid = user.wechatUnionid || null
    user.wechatNickname = user.wechatNickname || null
    user.wechatAvatar = user.wechatAvatar || null
    user.loginProvider = user.loginProvider || (user.wechatOpenid ? 'wechat' : 'password')
    user.phoneBound = user.phoneBound === undefined ? Boolean(user.phone) : Boolean(user.phoneBound)
    user.phoneBoundAt = user.phoneBoundAt || (user.phone ? new Date().toISOString().slice(0, 16).replace('T', ' ') : null)
    user.wechatBoundAt = user.wechatBoundAt || (user.wechatOpenid ? new Date().toISOString().slice(0, 16).replace('T', ' ') : null)
    user.trialBookingLimit = Number.isInteger(Number(user.trialBookingLimit)) ? Number(user.trialBookingLimit) : 1
    user.createdAt = user.createdAt || new Date().toISOString().slice(0, 16).replace('T', ' ')
    delete user.password
    this.state.users.push(user)
    return user
  }

  updateUser(id, patch) {
    const user = this.findUserById(id)
    if (!user) return null
    if (patch.passwordHash !== undefined) user.passwordHash = patch.passwordHash
    if (patch.username !== undefined) user.username = patch.username
    if (patch.role !== undefined) user.role = patch.role
    if (patch.roleName !== undefined) user.roleName = patch.roleName
    if (patch.name !== undefined) user.name = patch.name
    if (patch.phone !== undefined) user.phone = patch.phone || null
    if (patch.wechatOpenid !== undefined) user.wechatOpenid = patch.wechatOpenid || null
    if (patch.wechatUnionid !== undefined) user.wechatUnionid = patch.wechatUnionid || null
    if (patch.wechatNickname !== undefined) user.wechatNickname = patch.wechatNickname || null
    if (patch.wechatAvatar !== undefined) user.wechatAvatar = patch.wechatAvatar || null
    if (patch.loginProvider !== undefined) user.loginProvider = patch.loginProvider
    if (patch.phoneBound !== undefined) user.phoneBound = Boolean(patch.phoneBound)
    if (patch.phoneBoundAt !== undefined) user.phoneBoundAt = patch.phoneBoundAt
    if (patch.wechatBoundAt !== undefined) user.wechatBoundAt = patch.wechatBoundAt
    if (patch.status !== undefined) user.status = patch.status
    if (patch.inviterCode !== undefined) user.inviterCode = patch.inviterCode
    if (patch.trialBookingLimit !== undefined) user.trialBookingLimit = Number(patch.trialBookingLimit)
    if (patch.child !== undefined) user.child = patch.child
    return user
  }

  deleteUser(id) {
    const index = this.state.users.findIndex((user) => user.id === id)
    if (index < 0) return null
    const [user] = this.state.users.splice(index, 1)
    this.state.pointAccounts = this.state.pointAccounts.filter((item) => item.userId !== id)
    this.state.pointTransactions = this.state.pointTransactions.filter((item) => item.userId !== id)
    this.state.referrals = this.state.referrals.filter((item) => item.inviterId !== id && item.inviteeId !== id)
    this.state.appointments = this.state.appointments.filter((item) => item.userId !== id)
    this.state.redemptionOrders = this.state.redemptionOrders.filter((item) => item.userId !== id)
    return user
  }

  updateAccountRole(id, roleName) {
    const user = this.findUserById(id)
    const role = this.findRole(roleName)
    if (!user || !role) return null
    user.role = role.role
    user.roleName = role.name
    return this.withPermissions(user)
  }

  updateAccountStatus(id, status) {
    const user = this.findUserById(id)
    if (!user) return null
    user.status = status
    return this.withPermissions(user)
  }

  getPointAccount(userId) {
    let account = this.state.pointAccounts.find((item) => item.userId === userId)
    if (!account) {
      account = { userId, available: 0, pending: 0, frozen: 0, expiring: 0 }
      this.state.pointAccounts.push(account)
    }
    return account
  }

  updatePointAccount(userId, patch) {
    const account = this.getPointAccount(userId)
    Object.assign(account, patch)
    return account
  }

  addPointTransaction(transaction) {
    this.state.pointTransactions.unshift(transaction)
    return transaction
  }

  listPointTransactions() {
    return this.state.pointTransactions
  }

  getPointTransactions(userId) {
    return this.state.pointTransactions.filter((item) => item.userId === userId)
  }

  getPointExpiryRule() {
    return this.state.pointExpiryRule
  }

  listPointExpirySchedules(userId) {
    return this.state.pointExpirySchedules.filter((item) => !userId || item.userId === userId)
  }

  getReferralRules() {
    return this.state.referralRules
  }

  updateReferralRule(event, patch) {
    const rule = this.state.referralRules.find((item) => item.event === event)
    if (!rule) return null
    Object.assign(rule, patch)
    return rule
  }

  getReferralsByInviter(inviterId) {
    return this.state.referrals.filter((item) => item.inviterId === inviterId)
  }

  findReferralByInvitee(inviteeId) {
    return this.state.referrals.find((item) => item.inviteeId === inviteeId)
  }

  findReferralById(id) {
    return this.state.referrals.find((item) => item.id === id)
  }

  findReferralForLead(lead = {}) {
    if (lead.referralId) {
      const referral = this.findReferralById(lead.referralId)
      if (referral) return referral
    }
    if (lead.inviteeId) {
      const referral = this.findReferralByInvitee(lead.inviteeId)
      if (referral) return referral
    }

    const parent = String(lead.parent || '').trim()
    if (!parent) return null
    const source = String(lead.source || '').trim()
    const channel = String(lead.channel || '').trim()
    const candidates = this.state.referrals
      .map((referral) => {
        const inviter = this.findUserById(referral.inviterId)
        const invitee = referral.inviteeId ? this.findUserById(referral.inviteeId) : null
        const fallbackNames = [
          { value: invitee?.name, score: 40 },
          { value: referral.name, score: 30 },
          { value: invitee?.phone ? `家长${String(invitee.phone).slice(-4)}` : '', score: 20 },
          { value: referral.phone ? `家长${String(referral.phone).slice(-4)}` : '', score: 20 },
        ].filter((item) => item.value)
        const parentHit = fallbackNames.find((item) => item.value === parent)
        if (!parentHit) return null
        const sourceValues = [inviter?.name, inviter?.id, inviter?.phone].filter(Boolean)
        const sourceScore = !source || sourceValues.includes(source) ? 8 : 0
        const channelScore = !channel || !referral.channel || referral.channel === channel ? 4 : 0
        return { referral, score: parentHit.score + sourceScore + channelScore }
      })
      .filter(Boolean)
      .sort((a, b) => b.score - a.score)

    return candidates[0]?.referral || null
  }

  addReferral(referral) {
    this.state.referrals.unshift(referral)
    return referral
  }

  updateReferral(id, patch) {
    const referral = this.state.referrals.find((item) => item.id === id)
    if (!referral) return null
    Object.assign(referral, patch)
    return referral
  }

  updateReferralInviteeInfo(inviteeId, patch = {}) {
    const updated = []
    this.state.referrals.forEach((referral) => {
      if (referral.inviteeId !== inviteeId) return
      if (patch.name !== undefined) referral.name = patch.name
      if (patch.phone !== undefined) referral.phone = patch.phone
      if (patch.childGrade !== undefined) referral.childGrade = patch.childGrade
      updated.push(referral)
    })
    return updated
  }

  getCourses() {
    return this.state.courses
  }

  findCourse(id) {
    return this.state.courses.find((course) => course.id === id)
  }

  addCourse(course) {
    this.state.courses.unshift(course)
    return course
  }

  updateCourse(id, patch) {
    const course = this.findCourse(id)
    if (!course) return null
    Object.assign(course, patch)
    return course
  }

  deleteCourse(id) {
    const index = this.state.courses.findIndex((course) => course.id === id)
    if (index < 0) return null
    const [course] = this.state.courses.splice(index, 1)
    return course
  }

  addAppointment(appointment) {
    this.state.appointments.unshift(appointment)
    return appointment
  }

  findAppointmentById(id) {
    return this.decorateAppointment(this.state.appointments.find((appointment) => appointment.id === id))
  }

  updateAppointment(id, patch) {
    const appointment = this.state.appointments.find((item) => item.id === id)
    if (!appointment) return null
    Object.assign(appointment, patch)
    return this.decorateAppointment(appointment)
  }

  decorateAppointment(appointment) {
    if (!appointment) return null
    const user = this.findUserById(appointment.userId)
    const course = this.findCourse(appointment.courseId)
    return {
      ...appointment,
      user: user
        ? {
            id: user.id,
            name: user.name,
            phone: user.phone,
          }
        : undefined,
      course: course
        ? {
            id: course.id,
            name: course.name,
            campus: course.campus,
            teacher: course.teacher,
            nextTime: course.nextTime,
            pointsBack: Number(course.pointsBack || 0),
          }
        : undefined,
    }
  }

  findAppointmentByUserId(userId) {
    return this.decorateAppointment(this.state.appointments.find((appointment) => appointment.userId === userId))
  }

  listAppointments(filters = {}) {
    const keyword = String(filters.keyword || '').trim()
    return this.state.appointments
      .map((appointment) => this.decorateAppointment(appointment))
      .filter((appointment) => {
        const matchUser = !filters.userId || appointment.userId === filters.userId
        const matchCourse = !filters.courseId || appointment.courseId === filters.courseId
        const matchStatus = !filters.status || filters.status === 'all' || appointment.status === filters.status
        const matchKeyword = !keyword || [appointment.user?.name, appointment.user?.phone, appointment.course?.name, appointment.course?.campus, appointment.course?.teacher].some((value) => String(value || '').includes(keyword))
        return matchUser && matchCourse && matchStatus && matchKeyword
      })
  }

  getRewards() {
    return this.state.rewards
  }

  listRewards(filters = {}) {
    const keyword = String(filters.keyword || '').trim()
    return this.state.rewards.filter((reward) => {
      const matchKeyword = !keyword || [reward.name, reward.type, reward.description].some((value) => String(value || '').includes(keyword))
      const matchType = !filters.type || filters.type === 'all' || reward.type === filters.type
      const matchStock = !filters.stockStatus
        || filters.stockStatus === 'all'
        || (filters.stockStatus === 'in_stock' && Number(reward.stock || 0) > 0)
        || (filters.stockStatus === 'sold_out' && Number(reward.stock || 0) <= 0)
      return matchKeyword && matchType && matchStock
    })
  }

  findReward(id) {
    return this.state.rewards.find((reward) => reward.id === id)
  }

  addReward(reward) {
    this.state.rewards.unshift(reward)
    return reward
  }

  updateReward(id, patch) {
    const reward = this.findReward(id)
    if (!reward) return null
    Object.assign(reward, patch)
    return reward
  }

  decorateRedemption(order) {
    if (!order) return null
    const user = this.findUserById(order.userId)
    const reward = this.findReward(order.itemId)
    return {
      ...order,
      costPoints: Number(order.costPoints || reward?.cost || 0),
      voucherCode: order.voucherCode || '',
      voucherStatus: order.voucherStatus || 'not_issued',
      reviewedBy: order.reviewedBy || '',
      reviewedAt: order.reviewedAt || '',
      rejectedReason: order.rejectedReason || '',
      expireAt: order.expireAt || '',
      usedAt: order.usedAt || '',
      usedBy: order.usedBy || '',
      verifyNote: order.verifyNote || '',
      user: user
        ? {
            id: user.id,
            name: user.name,
            phone: user.phone,
          }
        : undefined,
      reward: reward
        ? {
            id: reward.id,
            name: reward.name,
            type: reward.type,
            cost: Number(reward.cost || 0),
          }
        : undefined,
    }
  }

  addRedemption(order) {
    const next = {
      ...order,
      costPoints: Number(order.costPoints || 0),
      voucherCode: order.voucherCode || '',
      voucherStatus: order.voucherStatus || 'not_issued',
      reviewedBy: order.reviewedBy || '',
      reviewedAt: order.reviewedAt || '',
      rejectedReason: order.rejectedReason || '',
      expireAt: order.expireAt || '',
      usedAt: order.usedAt || '',
      usedBy: order.usedBy || '',
      verifyNote: order.verifyNote || '',
    }
    this.state.redemptionOrders.unshift(next)
    return this.decorateRedemption(next)
  }

  findRedemptionById(id) {
    return this.decorateRedemption(this.state.redemptionOrders.find((order) => order.id === id))
  }

  findRedemptionByVoucherCode(voucherCode) {
    return this.decorateRedemption(this.state.redemptionOrders.find((order) => order.voucherCode === voucherCode))
  }

  listRedemptions(filters = {}) {
    const keyword = String(filters.keyword || '').trim()
    return this.state.redemptionOrders
      .map((order) => this.decorateRedemption(order))
      .filter((order) => {
        if (!order) return false
        const matchUser = !filters.userId || order.userId === filters.userId
        const matchItem = !filters.itemId || order.itemId === filters.itemId
        const matchStatus = !filters.status || filters.status === 'all' || order.status === filters.status
        const matchVoucherStatus = !filters.voucherStatus || filters.voucherStatus === 'all' || order.voucherStatus === filters.voucherStatus
        const matchType = !filters.type || filters.type === 'all' || order.reward?.type === filters.type
        const matchKeyword = !keyword || [order.id, order.voucherCode, order.user?.name, order.user?.phone, order.reward?.name].some((value) => String(value || '').includes(keyword))
        return matchUser && matchItem && matchStatus && matchVoucherStatus && matchType && matchKeyword
      })
  }

  updateRedemption(id, patch) {
    const order = this.state.redemptionOrders.find((item) => item.id === id)
    if (!order) return null
    Object.assign(order, patch)
    return this.decorateRedemption(order)
  }

  getLeads() {
    return this.state.leads
  }

  getChannelStats() {
    return this.state.channelStats
  }

  addLead(lead) {
    const next = {
      referralId: '',
      inviteeId: '',
      createdAt: new Date().toISOString().slice(0, 16).replace('T', ' '),
      ...lead,
    }
    this.state.leads.unshift(next)
    return next
  }

  updateLeadStage(id, stage) {
    const lead = this.state.leads.find((item) => item.id === id)
    if (!lead) return null
    lead.stage = stage
    return lead
  }

  updateLeadsByParent(parent, patch = {}) {
    const updated = []
    this.state.leads.forEach((lead) => {
      if (lead.parent !== parent) return
      Object.assign(lead, patch)
      updated.push(lead)
    })
    return updated
  }

  findLeadForReferral(referral, previous = {}) {
    if (!referral) return null
    const inviter = this.findUserById(referral.inviterId)
    const sources = [inviter?.name, inviter?.id, inviter?.phone].filter(Boolean)
    const fallbackNames = [
      referral.name,
      previous.name,
      referral.phone ? `家长${String(referral.phone).slice(-4)}` : '',
      previous.phone ? `家长${String(previous.phone).slice(-4)}` : '',
    ].filter(Boolean)

    const exactLead = this.state.leads.find((lead) => referral.id && lead.referralId === referral.id)
      || this.state.leads.find((lead) => referral.inviteeId && lead.inviteeId === referral.inviteeId)
    if (exactLead) return exactLead

    const candidates = this.state.leads
      .map((lead) => {
        if (!fallbackNames.includes(lead.parent)) return null
        const sourceScore = !lead.source || sources.includes(lead.source) ? 8 : 0
        const channelScore = !referral.channel || !lead.channel || lead.channel === referral.channel ? 4 : 0
        return { lead, score: sourceScore + channelScore }
      })
      .filter(Boolean)
      .sort((a, b) => b.score - a.score)
    return candidates[0]?.lead || null
  }

  updateLeadInfoByReferral(referral, patch = {}, previous = {}) {
    const lead = this.findLeadForReferral(referral, previous)
    if (!lead) return []
    Object.assign(lead, patch)
    if (referral.id && !lead.referralId) lead.referralId = referral.id
    if (referral.inviteeId && !lead.inviteeId) lead.inviteeId = referral.inviteeId
    return [lead]
  }

  updateLeadStageByReferral(referral, stage) {
    return this.updateLeadInfoByReferral(referral, { stage })
  }
}

const repository = new MemoryRepository(seed)

module.exports = { repository }
 ҂xlpconst mysql = require('mysql2/promise')
const { env } = require('../config/env')
const { normalizePermissions } = require('../constants/permissions')

function parseJson(value, fallback = []) {
  if (Array.isArray(value)) return value
  if (!value) return fallback
  try {
    return JSON.parse(value)
  } catch (error) {
    return fallback
  }
}

function toDate(value) {
  if (!value) return ''
  if (value instanceof Date) return value.toISOString().slice(0, 10)
  return String(value).slice(0, 10)
}

function toDateTime(value) {
  if (!value) return ''
  if (value instanceof Date) return value.toISOString().slice(0, 16).replace('T', ' ')
  return String(value).slice(0, 16).replace('T', ' ')
}

class MySqlRepository {
  constructor() {
    this.pool = mysql.createPool({
      host: env.mysql.host,
      port: env.mysql.port,
      user: env.mysql.user,
      password: env.mysql.password,
      database: env.mysql.database,
      waitForConnections: true,
      connectionLimit: env.mysql.connectionLimit,
      namedPlaceholders: true,
    })
  }

  async query(sql, params = {}) {
    const [rows] = await this.pool.execute(sql, params)
    return rows
  }

  rowToUser(row) {
    if (!row) return null
    return {
      id: row.id,
      username: row.username,
      passwordHash: row.password_hash,
      role: row.role,
      roleName: row.role_name,
      name: row.name,
      phone: row.phone,
      wechatOpenid: row.wechat_openid,
      wechatUnionid: row.wechat_unionid,
      wechatNickname: row.wechat_nickname,
      wechatAvatar: row.wechat_avatar,
      loginProvider: row.login_provider || 'password',
      phoneBound: Boolean(row.phone_bound),
      phoneBoundAt: toDateTime(row.phone_bound_at),
      wechatBoundAt: toDateTime(row.wechat_bound_at),
      status: row.status,
      lastLoginAt: toDateTime(row.last_login_at),
      createdAt: toDateTime(row.created_at),
      inviterCode: row.inviter_code,
      trialBookingLimit: Number(row.trial_booking_limit ?? 1),
      child: row.child_id
        ? {
            id: row.child_id,
            name: row.child_name,
            grade: row.child_grade,
            focus: row.child_focus,
          }
        : undefined,
    }
  }

  rowToRole(row) {
    if (!row) return null
    return {
      role: row.role,
      name: row.name,
      permissions: normalizePermissions(parseJson(row.permissions)),
    }
  }

  rowToCourse(row) {
    return {
      id: row.id,
      name: row.name,
      tags: parseJson(row.tags),
      nextTime: toDateTime(row.next_time),
      pointsBack: Number(row.points_back || 0),
      seats: Number(row.seats || 0),
      campus: row.campus,
      teacher: row.teacher,
      trialMinutes: Number(row.trial_minutes || 0),
      status: row.status,
    }
  }

  rowToAppointment(row) {
    return {
      id: row.id,
      userId: row.user_id,
      childId: row.child_id,
      courseId: row.course_id,
      status: row.status,
      createdAt: toDateTime(row.created_at),
      user: row.user_name
        ? {
            id: row.user_id,
            name: row.user_name,
            phone: row.user_phone,
          }
        : undefined,
      course: row.course_name
        ? {
            id: row.course_id,
            name: row.course_name,
            campus: row.course_campus,
            teacher: row.course_teacher,
            nextTime: toDateTime(row.course_next_time),
            pointsBack: Number(row.course_points_back || 0),
          }
        : undefined,
    }
  }

  rowToReward(row) {
    return {
      id: row.id,
      name: row.name,
      cost: Number(row.cost || 0),
      type: row.type,
      stock: Number(row.stock || 0),
      description: row.description || '',
    }
  }

  rowToPointAccount(row) {
    return {
      userId: row.user_id,
      available: Number(row.available || 0),
      pending: Number(row.pending || 0),
      frozen: Number(row.frozen || 0),
      expiring: Number(row.expiring || 0),
    }
  }

  rowToPointTransaction(row) {
    return {
      id: row.id,
      userId: row.user_id,
      title: row.title,
      amount: Number(row.amount || 0),
      status: row.status,
      referenceType: row.reference_type || '',
      referenceId: row.reference_id || '',
      createdAt: toDate(row.created_at),
    }
  }

  rowToReferral(row) {
    return {
      id: row.id,
      inviterId: row.inviter_id,
      inviteeId: row.invitee_id,
      name: row.name,
      phone: row.phone,
      childGrade: row.child_grade,
      status: row.status,
      reward: Number(row.reward || 0),
      channel: row.channel,
    }
  }

  rowToRedemption(row) {
    return {
      id: row.id,
      userId: row.user_id,
      itemId: row.item_id,
      status: row.status,
      costPoints: Number(row.cost_points || row.reward_cost || 0),
      voucherCode: row.voucher_code || '',
      voucherStatus: row.voucher_status || 'not_issued',
      createdAt: toDateTime(row.created_at),
      reviewedBy: row.reviewed_by || '',
      reviewedAt: toDateTime(row.reviewed_at),
      rejectedReason: row.rejected_reason || '',
      expireAt: toDateTime(row.expire_at),
      usedAt: toDateTime(row.used_at),
      usedBy: row.used_by || '',
      verifyNote: row.verify_note || '',
      user: row.user_name
        ? {
            id: row.user_id,
            name: row.user_name,
            phone: row.user_phone,
          }
        : undefined,
      reward: row.reward_name
        ? {
            id: row.item_id,
            name: row.reward_name,
            type: row.reward_type,
            cost: Number(row.reward_cost || row.cost_points || 0),
          }
        : undefined,
    }
  }

  rowToLead(row) {
    return {
      id: row.id,
      referralId: row.referral_id || '',
      inviteeId: row.invitee_id || '',
      parent: row.parent,
      source: row.source,
      stage: row.stage,
      advisor: row.advisor,
      risk: row.risk,
      channel: row.channel,
      createdAt: toDateTime(row.created_at),
    }
  }

  async findUserById(id) {
    const rows = await this.query('SELECT * FROM users WHERE id = :id LIMIT 1', { id })
    return this.rowToUser(rows[0])
  }

  async findUserByLoginAccount(account) {
    const value = String(account || '').trim()
    const rows = await this.query('SELECT * FROM users WHERE username = :value OR phone = :value OR id = :value OR wechat_openid = :value OR wechat_unionid = :value LIMIT 1', { value })
    return this.rowToUser(rows[0])
  }

  async findUserByInviteCode(inviterCode) {
    const rows = await this.query('SELECT * FROM users WHERE inviter_code = :inviterCode LIMIT 1', { inviterCode })
    return this.rowToUser(rows[0])
  }

  async findUserByPhone(phone) {
    const rows = await this.query('SELECT * FROM users WHERE phone = :phone LIMIT 1', { phone })
    return this.rowToUser(rows[0])
  }

  async findRole(roleName) {
    const rows = await this.query('SELECT * FROM roles WHERE role = :roleName LIMIT 1', { roleName })
    return this.rowToRole(rows[0])
  }

  async withPermissions(user) {
    if (!user) return null
    const role = await this.findRole(user.role)
    const { passwordHash, ...safeUser } = user
    return {
      ...safeUser,
      hasPassword: Boolean(user.passwordHash),
      roleName: role?.name || user.roleName || user.role,
      permissions: user.role === 'parent' ? [] : normalizePermissions(role?.permissions || []),
    }
  }

  async touchLogin(id) {
    await this.query('UPDATE users SET last_login_at = NOW() WHERE id = :id', { id })
    return this.withPermissions(await this.findUserById(id))
  }

  async listAccounts() {
    const rows = await this.query("SELECT * FROM users ORDER BY FIELD(role, 'super_admin', 'campus_admin', 'advisor', 'teacher', 'finance', 'parent'), id")
    return Promise.all(rows.map((row) => this.withPermissions(this.rowToUser(row))))
  }

  async listParentUsers() {
    const rows = await this.query("SELECT * FROM users WHERE role = 'parent' ORDER BY id")
    return Promise.all(rows.map((row) => this.withPermissions(this.rowToUser(row))))
  }

  async getRoleMatrix() {
    const rows = await this.query("SELECT * FROM roles WHERE role <> 'parent' ORDER BY FIELD(role, 'super_admin', 'campus_admin', 'advisor', 'teacher', 'finance')")
    return rows.map((row) => this.rowToRole(row))
  }

  async updateRolePermissions(role, permissions) {
    await this.query('UPDATE roles SET permissions = :permissions WHERE role = :role', {
      role,
      permissions: JSON.stringify(permissions || []),
    })
    return this.findRole(role)
  }

  async addUser(user) {
    await this.query(
      `INSERT INTO users (
         id, username, password_hash, role, role_name, name, phone,
         wechat_openid, wechat_unionid, wechat_nickname, wechat_avatar,
         login_provider, phone_bound, phone_bound_at, wechat_bound_at,
         status, inviter_code, trial_booking_limit, child_id, child_name, child_grade, child_focus
       )
       VALUES (
         :id, :username, :passwordHash, :role, :roleName, :name, :phone,
         :wechatOpenid, :wechatUnionid, :wechatNickname, :wechatAvatar,
         :loginProvider, :phoneBound, :phoneBoundAt, :wechatBoundAt,
         :status, :inviterCode, :trialBookingLimit, :childId, :childName, :childGrade, :childFocus
       )`,
      {
        id: user.id,
        username: user.username || user.phone || user.id,
        passwordHash: user.passwordHash || null,
        role: user.role || 'parent',
        roleName: user.roleName || '家长',
        name: user.name,
        phone: user.phone || null,
        wechatOpenid: user.wechatOpenid || null,
        wechatUnionid: user.wechatUnionid || null,
        wechatNickname: user.wechatNickname || null,
        wechatAvatar: user.wechatAvatar || null,
        loginProvider: user.loginProvider || (user.wechatOpenid ? 'wechat' : 'password'),
        phoneBound: user.phoneBound === undefined ? Boolean(user.phone) : Boolean(user.phoneBound),
        phoneBoundAt: user.phoneBoundAt || (user.phone ? new Date().toISOString().slice(0, 16).replace('T', ' ') : null),
        wechatBoundAt: user.wechatBoundAt || (user.wechatOpenid ? new Date().toISOString().slice(0, 16).replace('T', ' ') : null),
        status: user.status || 'active',
        inviterCode: user.inviterCode,
        trialBookingLimit: Number.isInteger(Number(user.trialBookingLimit)) ? Number(user.trialBookingLimit) : 1,
        childId: user.child?.id || null,
        childName: user.child?.name || null,
        childGrade: user.child?.grade || null,
        childFocus: user.child?.focus || null,
      },
    )
    return this.findUserById(user.id)
  }

  async updateUser(id, patch) {
    const current = await this.findUserById(id)
    if (!current) return null
    const next = {
      ...current,
      ...patch,
      child: patch.child !== undefined ? patch.child : current.child,
    }
    await this.query(
      `UPDATE users
       SET username = :username,
           password_hash = :passwordHash,
           role = :role,
           role_name = :roleName,
           name = :name,
           phone = :phone,
           wechat_openid = :wechatOpenid,
           wechat_unionid = :wechatUnionid,
           wechat_nickname = :wechatNickname,
           wechat_avatar = :wechatAvatar,
           login_provider = :loginProvider,
           phone_bound = :phoneBound,
           phone_bound_at = :phoneBoundAt,
           wechat_bound_at = :wechatBoundAt,
           status = :status,
           inviter_code = :inviterCode,
           trial_booking_limit = :trialBookingLimit,
           child_id = :childId,
           child_name = :childName,
           child_grade = :childGrade,
           child_focus = :childFocus
       WHERE id = :id`,
      {
        id,
        username: next.username || next.phone || id,
        passwordHash: next.passwordHash || null,
        role: next.role || 'parent',
        roleName: next.roleName || next.role || '家长',
        name: next.name,
        phone: next.phone || null,
        wechatOpenid: next.wechatOpenid || null,
        wechatUnionid: next.wechatUnionid || null,
        wechatNickname: next.wechatNickname || null,
        wechatAvatar: next.wechatAvatar || null,
        loginProvider: next.loginProvider || 'password',
        phoneBound: Boolean(next.phoneBound),
        phoneBoundAt: next.phoneBoundAt || null,
        wechatBoundAt: next.wechatBoundAt || null,
        status: next.status || 'active',
        inviterCode: next.inviterCode || null,
        trialBookingLimit: Number.isInteger(Number(next.trialBookingLimit)) ? Number(next.trialBookingLimit) : 1,
        childId: next.child?.id || null,
        childName: next.child?.name || null,
        childGrade: next.child?.grade || null,
        childFocus: next.child?.focus || null,
      },
    )
    return this.findUserById(id)
  }

  async deleteUser(id) {
    const user = await this.findUserById(id)
    if (!user) return null
    await this.query('DELETE FROM referrals WHERE inviter_id = :id OR invitee_id = :id', { id })
    await this.query('DELETE FROM point_expiry_schedules WHERE user_id = :id', { id })
    await this.query('DELETE FROM users WHERE id = :id', { id })
    return user
  }

  async updateAccountRole(id, roleName) {
    const role = await this.findRole(roleName)
    const user = await this.findUserById(id)
    if (!role || !user) return null
    await this.query('UPDATE users SET role = :roleName, role_name = :roleDisplay WHERE id = :id', { id, roleName, roleDisplay: role.name })
    return this.withPermissions(await this.findUserById(id))
  }

  async updateAccountStatus(id, status) {
    const user = await this.findUserById(id)
    if (!user) return null
    await this.query('UPDATE users SET status = :status WHERE id = :id', { id, status })
    return this.withPermissions(await this.findUserById(id))
  }

  async getPointAccount(userId) {
    let rows = await this.query('SELECT * FROM point_accounts WHERE user_id = :userId LIMIT 1', { userId })
    if (!rows[0]) {
      await this.query('INSERT INTO point_accounts (user_id, available, pending, frozen, expiring) VALUES (:userId, 0, 0, 0, 0)', { userId })
      rows = await this.query('SELECT * FROM point_accounts WHERE user_id = :userId LIMIT 1', { userId })
    }
    return this.rowToPointAccount(rows[0])
  }

  async updatePointAccount(userId, patch) {
    await this.getPointAccount(userId)
    await this.query(
      'UPDATE point_accounts SET available = :available, pending = :pending, frozen = :frozen, expiring = :expiring WHERE user_id = :userId',
      {
        userId,
        available: Number(patch.available || 0),
        pending: Number(patch.pending || 0),
        frozen: Number(patch.frozen || 0),
        expiring: Number(patch.expiring || 0),
      },
    )
    return this.getPointAccount(userId)
  }

  async addPointTransaction(transaction) {
    await this.query(
      `INSERT INTO point_transactions (id, user_id, title, amount, status, reference_type, reference_id, created_at)
       VALUES (:id, :userId, :title, :amount, :status, :referenceType, :referenceId, :createdAt)`,
      {
        ...transaction,
        referenceType: transaction.referenceType || null,
        referenceId: transaction.referenceId || null,
      },
    )
    return transaction
  }

  async listPointTransactions() {
    const rows = await this.query('SELECT * FROM point_transactions ORDER BY created_at DESC, id DESC')
    return rows.map((row) => this.rowToPointTransaction(row))
  }

  async getPointTransactions(userId) {
    const rows = await this.query('SELECT * FROM point_transactions WHERE user_id = :userId ORDER BY created_at DESC, id DESC', { userId })
    return rows.map((row) => this.rowToPointTransaction(row))
  }

  async getPointExpiryRule() {
    const rows = await this.query('SELECT * FROM point_expiry_rule ORDER BY id LIMIT 1')
    const row = rows[0]
    return {
      title: row?.title || '积分有效期说明',
      validDays: Number(row?.valid_days || 180),
      description: row?.description || '',
      reminder: row?.reminder || '',
    }
  }

  async listPointExpirySchedules(userId) {
    const rows = await this.query(
      `SELECT * FROM point_expiry_schedules
       WHERE (:userId IS NULL OR user_id = :userId)
       ORDER BY expire_at ASC`,
      { userId: userId || null },
    )
    return rows.map((row) => ({
      id: row.id,
      userId: row.user_id,
      points: Number(row.points || 0),
      expireAt: toDateTime(row.expire_at),
      source: row.source,
    }))
  }

  async getReferralRules() {
    const rows = await this.query('SELECT * FROM referral_rules ORDER BY sort_order ASC')
    return rows.map((row) => ({
      event: row.event,
      title: row.title,
      inviter: Number(row.inviter || 0),
      invitee: Number(row.invitee || 0),
      sortOrder: Number(row.sort_order || 0),
    }))
  }

  async updateReferralRule(event, patch) {
    const rows = await this.query('SELECT * FROM referral_rules WHERE event = :event LIMIT 1', { event })
    if (!rows[0]) return null
    const current = {
      event: rows[0].event,
      title: rows[0].title,
      inviter: Number(rows[0].inviter || 0),
      invitee: Number(rows[0].invitee || 0),
      sortOrder: Number(rows[0].sort_order || 0),
    }
    const next = { ...current, ...patch }
    await this.query(
      'UPDATE referral_rules SET title = :title, inviter = :inviter, invitee = :invitee WHERE event = :event',
      { event, title: next.title, inviter: next.inviter, invitee: next.invitee },
    )
    return (await this.getReferralRules()).find((rule) => rule.event === event)
  }

  async getReferralsByInviter(inviterId) {
    const rows = await this.query('SELECT * FROM referrals WHERE inviter_id = :inviterId ORDER BY created_at DESC, id DESC', { inviterId })
    return rows.map((row) => this.rowToReferral(row))
  }

  async findReferralByInvitee(inviteeId) {
    const rows = await this.query('SELECT * FROM referrals WHERE invitee_id = :inviteeId ORDER BY created_at DESC, id DESC LIMIT 1', { inviteeId })
    return rows[0] ? this.rowToReferral(rows[0]) : null
  }

  async findReferralById(id) {
    const rows = await this.query('SELECT * FROM referrals WHERE id = :id LIMIT 1', { id })
    return rows[0] ? this.rowToReferral(rows[0]) : null
  }

  async findReferralForLead(lead = {}) {
    if (lead.referralId) {
      const referral = await this.findReferralById(lead.referralId)
      if (referral) return referral
    }
    if (lead.inviteeId) {
      const referral = await this.findReferralByInvitee(lead.inviteeId)
      if (referral) return referral
    }

    const parent = String(lead.parent || '').trim()
    if (!parent) return null
    const rows = await this.query(
      `SELECT r.*,
              CASE
                WHEN invitee.name = :parent THEN 40
                WHEN r.name = :parent THEN 30
                WHEN CONCAT('家长', RIGHT(COALESCE(invitee.phone, ''), 4)) = :parent THEN 20
                WHEN CONCAT('家长', RIGHT(COALESCE(r.phone, ''), 4)) = :parent THEN 20
                ELSE 0
              END AS parent_score,
              CASE
                WHEN :source = '' THEN 8
                WHEN inviter.name = :source OR inviter.id = :source OR inviter.phone = :source THEN 8
                ELSE 0
              END AS source_score,
              CASE
                WHEN :channel = '' OR r.channel = :channel THEN 4
                ELSE 0
              END AS channel_score
       FROM referrals r
       LEFT JOIN users invitee ON invitee.id = r.invitee_id
       LEFT JOIN users inviter ON inviter.id = r.inviter_id
       WHERE invitee.name = :parent
          OR r.name = :parent
          OR CONCAT('家长', RIGHT(COALESCE(invitee.phone, ''), 4)) = :parent
          OR CONCAT('家长', RIGHT(COALESCE(r.phone, ''), 4)) = :parent
       ORDER BY parent_score DESC, source_score DESC, channel_score DESC, r.created_at DESC, r.id DESC
       LIMIT 1`,
      {
        parent,
        source: String(lead.source || '').trim(),
        channel: String(lead.channel || '').trim(),
      },
    )
    return rows[0] ? this.rowToReferral(rows[0]) : null
  }

  async addReferral(referral) {
    await this.query(
      `INSERT INTO referrals (id, inviter_id, invitee_id, name, phone, child_grade, status, reward, channel)
       VALUES (:id, :inviterId, :inviteeId, :name, :phone, :childGrade, :status, :reward, :channel)`,
      referral,
    )
    return referral
  }

  async updateReferral(id, patch) {
    const rows = await this.query('SELECT * FROM referrals WHERE id = :id LIMIT 1', { id })
    if (!rows[0]) return null
    const current = this.rowToReferral(rows[0])
    const next = { ...current, ...patch }
    await this.query(
      'UPDATE referrals SET status = :status, reward = :reward WHERE id = :id',
      { id, status: next.status, reward: next.reward },
    )
    const nextRows = await this.query('SELECT * FROM referrals WHERE id = :id LIMIT 1', { id })
    return nextRows[0] ? this.rowToReferral(nextRows[0]) : null
  }

  async updateReferralInviteeInfo(inviteeId, patch = {}) {
    const assignments = []
    const params = { inviteeId }
    if (patch.name !== undefined) {
      assignments.push('name = :name')
      params.name = patch.name
    }
    if (patch.phone !== undefined) {
      assignments.push('phone = :phone')
      params.phone = patch.phone
    }
    if (patch.childGrade !== undefined) {
      assignments.push('child_grade = :childGrade')
      params.childGrade = patch.childGrade
    }
    if (!assignments.length) return []
    await this.query(`UPDATE referrals SET ${assignments.join(', ')} WHERE invitee_id = :inviteeId`, params)
    const rows = await this.query('SELECT * FROM referrals WHERE invitee_id = :inviteeId ORDER BY created_at DESC, id DESC', { inviteeId })
    return rows.map((row) => this.rowToReferral(row))
  }

  async getCourses() {
    const rows = await this.query('SELECT * FROM courses ORDER BY created_at DESC, id')
    return rows.map((row) => this.rowToCourse(row))
  }

  async findCourse(id) {
    const rows = await this.query('SELECT * FROM courses WHERE id = :id LIMIT 1', { id })
    return rows[0] ? this.rowToCourse(rows[0]) : null
  }

  async addCourse(course) {
    await this.query(
      `INSERT INTO courses (id, name, tags, next_time, points_back, seats, campus, teacher, trial_minutes, status)
       VALUES (:id, :name, :tags, :nextTime, :pointsBack, :seats, :campus, :teacher, :trialMinutes, :status)`,
      { ...course, tags: JSON.stringify(course.tags || []) },
    )
    return this.findCourse(course.id)
  }

  async updateCourse(id, patch) {
    const course = await this.findCourse(id)
    if (!course) return null
    const next = { ...course, ...patch }
    await this.query(
      `UPDATE courses
       SET name = :name, tags = :tags, next_time = :nextTime, points_back = :pointsBack, seats = :seats,
           campus = :campus, teacher = :teacher, trial_minutes = :trialMinutes, status = :status
       WHERE id = :id`,
      { ...next, id, tags: JSON.stringify(next.tags || []) },
    )
    return this.findCourse(id)
  }

  async deleteCourse(id) {
    const course = await this.findCourse(id)
    if (!course) return null
    await this.query('DELETE FROM courses WHERE id = :id', { id })
    return course
  }

  async addAppointment(appointment) {
    await this.query(
      'INSERT INTO appointments (id, user_id, child_id, course_id, status, created_at) VALUES (:id, :userId, :childId, :courseId, :status, :createdAt)',
      appointment,
    )
    return appointment
  }

  async findAppointmentById(id) {
    const rows = await this.query(`${this.appointmentSelectSql()} WHERE a.id = :id LIMIT 1`, { id })
    return rows[0] ? this.rowToAppointment(rows[0]) : null
  }

  async updateAppointment(id, patch) {
    const appointment = await this.findAppointmentById(id)
    if (!appointment) return null
    const next = { ...appointment, ...patch }
    await this.query('UPDATE appointments SET status = :status WHERE id = :id', { id, status: next.status })
    return this.findAppointmentById(id)
  }

  appointmentSelectSql() {
    return `SELECT a.*, u.name AS user_name, u.phone AS user_phone,
                   c.name AS course_name, c.campus AS course_campus, c.teacher AS course_teacher,
                   c.next_time AS course_next_time, c.points_back AS course_points_back
            FROM appointments a
            LEFT JOIN users u ON u.id = a.user_id
            LEFT JOIN courses c ON c.id = a.course_id`
  }

  async findAppointmentByUserId(userId) {
    const rows = await this.query(`${this.appointmentSelectSql()} WHERE a.user_id = :userId ORDER BY a.created_at DESC LIMIT 1`, { userId })
    return rows[0] ? this.rowToAppointment(rows[0]) : null
  }

  async listAppointments(filters = {}) {
    const conditions = []
    const params = {}
    if (filters.userId) {
      conditions.push('a.user_id = :userId')
      params.userId = filters.userId
    }
    if (filters.courseId) {
      conditions.push('a.course_id = :courseId')
      params.courseId = filters.courseId
    }
    if (filters.status && filters.status !== 'all') {
      conditions.push('a.status = :status')
      params.status = filters.status
    }
    if (filters.keyword) {
      conditions.push('(u.name LIKE :keyword OR u.phone LIKE :keyword OR c.name LIKE :keyword OR c.campus LIKE :keyword OR c.teacher LIKE :keyword)')
      params.keyword = `%${filters.keyword}%`
    }
    const where = conditions.length ? ` WHERE ${conditions.join(' AND ')}` : ''
    const rows = await this.query(`${this.appointmentSelectSql()}${where} ORDER BY a.created_at DESC, a.id DESC`, params)
    return rows.map((row) => this.rowToAppointment(row))
  }

  async getRewards() {
    const rows = await this.query('SELECT * FROM rewards ORDER BY id')
    return rows.map((row) => this.rowToReward(row))
  }

  async listRewards(filters = {}) {
    const conditions = []
    const params = {}
    if (filters.keyword) {
      conditions.push('(name LIKE :keyword OR type LIKE :keyword OR description LIKE :keyword)')
      params.keyword = `%${filters.keyword}%`
    }
    if (filters.type && filters.type !== 'all') {
      conditions.push('type = :type')
      params.type = filters.type
    }
    if (filters.stockStatus === 'in_stock') conditions.push('stock > 0')
    if (filters.stockStatus === 'sold_out') conditions.push('stock <= 0')
    const where = conditions.length ? ` WHERE ${conditions.join(' AND ')}` : ''
    const rows = await this.query(`SELECT * FROM rewards${where} ORDER BY id`, params)
    return rows.map((row) => this.rowToReward(row))
  }

  async findReward(id) {
    const rows = await this.query('SELECT * FROM rewards WHERE id = :id LIMIT 1', { id })
    return rows[0] ? this.rowToReward(rows[0]) : null
  }

  async addReward(reward) {
    await this.query(
      'INSERT INTO rewards (id, name, cost, type, stock, description) VALUES (:id, :name, :cost, :type, :stock, :description)',
      reward,
    )
    return this.findReward(reward.id)
  }

  async updateReward(id, patch) {
    const reward = await this.findReward(id)
    if (!reward) return null
    const next = { ...reward, ...patch }
    await this.query('UPDATE rewards SET name = :name, type = :type, cost = :cost, stock = :stock, description = :description WHERE id = :id', { ...next, id })
    return this.findReward(id)
  }

  async addRedemption(order) {
    await this.query(
      `INSERT INTO redemption_orders (
         id, user_id, item_id, status, cost_points, voucher_code, voucher_status,
         created_at, reviewed_by, reviewed_at, rejected_reason, expire_at, used_at, used_by, verify_note
       )
       VALUES (
         :id, :userId, :itemId, :status, :costPoints, :voucherCode, :voucherStatus,
         :createdAt, :reviewedBy, :reviewedAt, :rejectedReason, :expireAt, :usedAt, :usedBy, :verifyNote
       )`,
      {
        ...order,
        costPoints: order.costPoints || 0,
        voucherCode: order.voucherCode || null,
        voucherStatus: order.voucherStatus || 'not_issued',
        reviewedBy: order.reviewedBy || null,
        reviewedAt: order.reviewedAt || null,
        rejectedReason: order.rejectedReason || null,
        expireAt: order.expireAt || null,
        usedAt: order.usedAt || null,
        usedBy: order.usedBy || null,
        verifyNote: order.verifyNote || null,
      },
    )
    return this.findRedemptionById(order.id)
  }

  redemptionSelectSql() {
    return `SELECT ro.*, u.name AS user_name, u.phone AS user_phone, r.name AS reward_name, r.type AS reward_type, r.cost AS reward_cost
            FROM redemption_orders ro
            LEFT JOIN users u ON u.id = ro.user_id
            LEFT JOIN rewards r ON r.id = ro.item_id`
  }

  async findRedemptionById(id) {
    const rows = await this.query(`${this.redemptionSelectSql()} WHERE ro.id = :id LIMIT 1`, { id })
    return rows[0] ? this.rowToRedemption(rows[0]) : null
  }

  async findRedemptionByVoucherCode(voucherCode) {
    const rows = await this.query(`${this.redemptionSelectSql()} WHERE ro.voucher_code = :voucherCode LIMIT 1`, { voucherCode })
    return rows[0] ? this.rowToRedemption(rows[0]) : null
  }

  async listRedemptions(filters = {}) {
    const conditions = []
    const params = {}
    if (filters.userId) {
      conditions.push('ro.user_id = :userId')
      params.userId = filters.userId
    }
    if (filters.itemId) {
      conditions.push('ro.item_id = :itemId')
      params.itemId = filters.itemId
    }
    if (filters.status && filters.status !== 'all') {
      conditions.push('ro.status = :status')
      params.status = filters.status
    }
    if (filters.voucherStatus && filters.voucherStatus !== 'all') {
      conditions.push('ro.voucher_status = :voucherStatus')
      params.voucherStatus = filters.voucherStatus
    }
    if (filters.type && filters.type !== 'all') {
      conditions.push('r.type = :type')
      params.type = filters.type
    }
    if (filters.keyword) {
      conditions.push('(u.name LIKE :keyword OR u.phone LIKE :keyword OR r.name LIKE :keyword OR ro.voucher_code LIKE :keyword OR ro.id LIKE :keyword)')
      params.keyword = `%${filters.keyword}%`
    }
    const where = conditions.length ? ` WHERE ${conditions.join(' AND ')}` : ''
    const rows = await this.query(`${this.redemptionSelectSql()}${where} ORDER BY ro.created_at DESC, ro.id DESC`, params)
    return rows.map((row) => this.rowToRedemption(row))
  }

  async updateRedemption(id, patch) {
    const columnMap = {
      status: 'status',
      costPoints: 'cost_points',
      voucherCode: 'voucher_code',
      voucherStatus: 'voucher_status',
      reviewedBy: 'reviewed_by',
      reviewedAt: 'reviewed_at',
      rejectedReason: 'rejected_reason',
      expireAt: 'expire_at',
      usedAt: 'used_at',
      usedBy: 'used_by',
      verifyNote: 'verify_note',
    }
    const assignments = []
    const params = { id }
    Object.entries(columnMap).forEach(([key, column]) => {
      if (patch[key] !== undefined) {
        assignments.push(`${column} = :${key}`)
        params[key] = patch[key] || null
      }
    })
    if (!assignments.length) return this.findRedemptionById(id)
    await this.query(`UPDATE redemption_orders SET ${assignments.join(', ')} WHERE id = :id`, params)
    return this.findRedemptionById(id)
  }

  async getLeads() {
    const rows = await this.query('SELECT * FROM leads ORDER BY created_at DESC, id DESC')
    return rows.map((row) => this.rowToLead(row))
  }

  async getChannelStats() {
    const rows = await this.query('SELECT * FROM channel_stats ORDER BY leads DESC')
    return rows.map((row) => ({
      channel: row.channel,
      leads: Number(row.leads || 0),
      attended: Number(row.attended || 0),
      purchased: Number(row.purchased || 0),
    }))
  }

  async addLead(lead) {
    await this.query(
      `INSERT INTO leads (id, referral_id, invitee_id, parent, source, stage, advisor, risk, channel)
       VALUES (:id, :referralId, :inviteeId, :parent, :source, :stage, :advisor, :risk, :channel)`,
      {
        id: lead.id,
        referralId: lead.referralId || null,
        inviteeId: lead.inviteeId || null,
        parent: lead.parent,
        source: lead.source,
        stage: lead.stage,
        advisor: lead.advisor,
        risk: lead.risk || '正常',
        channel: lead.channel || '小程序邀请码',
      },
    )
    return lead
  }

  async updateLeadStage(id, stage) {
    const rows = await this.query('SELECT * FROM leads WHERE id = :id LIMIT 1', { id })
    if (!rows[0]) return null
    await this.query('UPDATE leads SET stage = :stage WHERE id = :id', { id, stage })
    return { ...this.rowToLead(rows[0]), stage }
  }

  async updateLeadsByParent(parent, patch = {}) {
    const assignments = []
    const params = { parent }
    if (patch.parent !== undefined) {
      assignments.push('parent = :nextParent')
      params.nextParent = patch.parent
    }
    if (patch.stage !== undefined) {
      assignments.push('stage = :stage')
      params.stage = patch.stage
    }
    if (patch.channel !== undefined) {
      assignments.push('channel = :channel')
      params.channel = patch.channel
    }
    if (!assignments.length) return []
    await this.query(`UPDATE leads SET ${assignments.join(', ')} WHERE parent = :parent`, params)
    return this.getLeads()
  }

  async findLeadForReferral(referral, previous = {}) {
    if (!referral) return null
    const exactRows = await this.query(
      `SELECT * FROM leads
       WHERE (:referralId <> '' AND referral_id = :referralId)
          OR (:inviteeId <> '' AND invitee_id = :inviteeId)
       ORDER BY created_at DESC, id DESC
       LIMIT 1`,
      {
        referralId: referral.id || '',
        inviteeId: referral.inviteeId || '',
      },
    )
    if (exactRows[0]) return this.rowToLead(exactRows[0])

    const inviter = referral.inviterId ? await this.findUserById(referral.inviterId) : null
    const sources = [inviter?.name, inviter?.id, inviter?.phone].filter(Boolean)
    const fallbackNames = [
      referral.name,
      previous.name,
      referral.phone ? `家长${String(referral.phone).slice(-4)}` : '',
      previous.phone ? `家长${String(previous.phone).slice(-4)}` : '',
    ].filter(Boolean)
    if (!fallbackNames.length) return null

    const params = {
      channel: referral.channel || '',
    }
    const parentConditions = fallbackNames.map((name, index) => {
      const key = `parent${index}`
      params[key] = name
      return `parent = :${key}`
    })
    const sourceConditions = sources.map((source, index) => {
      const key = `source${index}`
      params[key] = source
      return `source = :${key}`
    })
    const rows = await this.query(
      `SELECT *,
              CASE
                WHEN ${sourceConditions.length ? sourceConditions.join(' OR ') : '1 = 1'} THEN 8
                ELSE 0
              END AS source_score,
              CASE
                WHEN :channel = '' OR channel = :channel OR channel = '' THEN 4
                ELSE 0
              END AS channel_score
       FROM leads
       WHERE (${parentConditions.join(' OR ')})
       ORDER BY source_score DESC, channel_score DESC, created_at DESC, id DESC
       LIMIT 1`,
      params,
    )
    return rows[0] ? this.rowToLead(rows[0]) : null
  }

  async updateLeadInfoByReferral(referral, patch = {}, previous = {}) {
    const lead = await this.findLeadForReferral(referral, previous)
    if (!lead) return []

    const nextPatch = { ...patch }
    if (referral.id && !lead.referralId) nextPatch.referralId = referral.id
    if (referral.inviteeId && !lead.inviteeId) nextPatch.inviteeId = referral.inviteeId

    const columnMap = {
      referralId: 'referral_id',
      inviteeId: 'invitee_id',
      parent: 'parent',
      source: 'source',
      stage: 'stage',
      advisor: 'advisor',
      risk: 'risk',
      channel: 'channel',
    }
    const assignments = []
    const params = { id: lead.id }
    Object.entries(columnMap).forEach(([key, column]) => {
      if (nextPatch[key] !== undefined) {
        assignments.push(`${column} = :${key}`)
        params[key] = nextPatch[key] || null
      }
    })
    if (!assignments.length) return [lead]

    await this.query(`UPDATE leads SET ${assignments.join(', ')} WHERE id = :id`, params)
    const rows = await this.query('SELECT * FROM leads WHERE id = :id LIMIT 1', { id: lead.id })
    return rows[0] ? [this.rowToLead(rows[0])] : []
  }

  async updateLeadStageByReferral(referral, stage) {
    return this.updateLeadInfoByReferral(referral, { stage })
  }
}

module.exports = { MySqlRepository }
^x$ 100644 index.js h3CJNޟN.vTx{const express = require('express')
const {
  getAccounts,
  createAccount,
  deleteAccount,
  getDashboard,
  getLeads,
  confirmLeadPurchase,
  getPcOverview,
  getRoles,
  updateAccount,
  updateAccountRole,
  updateAccountStatus,
  updateLeadStage,
  updateRolePermissions,
} = require('../controllers/adminController')
const { bindWechatPhone, current, login, phoneLogin, sendSmsCode, updatePassword, updatePhone, updateProfile, wechatLogin } = require('../controllers/authController')
const { getBootstrap } = require('../controllers/bootstrapController')
const { attendAppointment, bookCourse, createCourse, deleteCourse, getAdminCourse, listAdminCourses, listAppointments, listCourses, listUserAppointments, updateCourse } = require('../controllers/courseController')
const {
  adjustPoints,
  approveRedemption,
  createReward,
  getRewardDetail,
  getAdminPointManager,
  getPointCenter,
  listPointAccounts,
  listPointTransactions,
  listRedemptions,
  listRewards,
  listUserRedemptions,
  listUserRewards,
  listUserTransactions,
  redeemReward,
  rejectRedemption,
  settlePendingPoints,
  updateReward,
  updateReferralRule,
  verifyVoucher,
} = require('../controllers/pointController')
const { bindReferral, createMiniProgramCode, createShareMaterial, getReferralCenter } = require('../controllers/referralController')
const { uploadAvatar } = require('../controllers/uploadController')
const { requirePermission } = require('../middleware/permission')
const { requireSelf, requireSession } = require('../middleware/session')

const router = express.Router()

router.get('/bootstrap', requireSession, requireSelf('query', 'accountId'), getBootstrap)

router.get('/auth/current', requireSession, current)
router.post('/auth/login', login)
router.post('/auth/sms-code', sendSmsCode)
router.post('/auth/phone-login', phoneLogin)
router.post('/auth/wechat-login', wechatLogin)
router.post('/auth/wechat-bind-phone', bindWechatPhone)
router.patch('/auth/profile/:id', requireSession, requireSelf('params', 'id'), updateProfile)
router.patch('/auth/password/:id', requireSession, requireSelf('params', 'id'), updatePassword)
router.patch('/auth/phone/:id', requireSession, requireSelf('params', 'id'), updatePhone)

router.post('/uploads/avatar', requireSession, uploadAvatar)

router.get('/referrals', requireSession, requireSelf('query', 'inviterId'), getReferralCenter)
router.post('/referrals/share', requireSession, requireSelf('body', 'inviterId'), createShareMaterial)
router.post('/referrals/mini-code', requireSession, requireSelf('body', 'inviterId'), createMiniProgramCode)
router.post('/referrals/bind', bindReferral)

router.get('/points/:userId/rewards', requireSession, requireSelf('params', 'userId'), listUserRewards)
router.get('/points/:userId/transactions', requireSession, requireSelf('params', 'userId'), listUserTransactions)
router.get('/points/:userId/redemptions', requireSession, requireSelf('params', 'userId'), listUserRedemptions)
router.get('/points/:userId', requireSession, requireSelf('params', 'userId'), getPointCenter)
router.post('/redemptions', requireSession, requireSelf('body', 'userId'), redeemReward)

router.get('/courses', listCourses)
router.get('/appointments', requireSession, requireSelf('query', 'userId'), listUserAppointments)
router.post('/appointments', requireSession, requireSelf('body', 'userId'), bookCourse)

router.get('/admin/dashboard', requirePermission('dashboard:overview:view'), getDashboard)
router.get('/admin/leads', requirePermission('leads:referral:view'), getLeads)
router.post('/admin/leads/:id/purchase', requirePermission('leads:referral:advance'), confirmLeadPurchase)
router.get('/admin/courses', requirePermission('courses:trial:view'), listAdminCourses)
router.get('/admin/courses/:id', requirePermission('courses:trial:view'), getAdminCourse)
router.get('/admin/appointments', requirePermission('courses:trial:view'), listAppointments)
router.patch('/admin/appointments/:id/attend', requirePermission('courses:trial:edit'), attendAppointment)
router.post('/admin/courses', requirePermission('courses:trial:add'), createCourse)
router.patch('/admin/courses/:id', requirePermission('courses:trial:edit'), updateCourse)
router.delete('/admin/courses/:id', requirePermission('courses:trial:delete'), deleteCourse)
router.get('/admin/pc-overview', requirePermission('dashboard:overview:view', 'statistics:channel:view'), getPcOverview)
router.get('/admin/accounts', requirePermission('system:user:view'), getAccounts)
router.post('/admin/accounts', requirePermission('system:user:add'), createAccount)
router.patch('/admin/accounts/:id', requirePermission('system:user:edit'), updateAccount)
router.delete('/admin/accounts/:id', requirePermission('system:user:delete'), deleteAccount)
router.get('/admin/roles', requirePermission('system:role:view', 'system:user:view'), getRoles)
router.patch('/admin/roles/:role/permissions', requirePermission('system:role:edit'), updateRolePermissions)
router.get('/admin/points', requirePermission('points:account:view'), getAdminPointManager)
router.get('/admin/point-accounts', requirePermission('points:account:view'), listPointAccounts)
router.get('/admin/point-transactions', requirePermission('points:account:view'), listPointTransactions)
router.post('/admin/points/adjust', requirePermission('points:account:adjust'), adjustPoints)
router.post('/admin/points/settle-pending', requirePermission('points:account:adjust'), settlePendingPoints)
router.patch('/admin/referral-rules/:event', requirePermission('points:rule:edit', 'points:account:adjust'), updateReferralRule)
router.get('/admin/redemptions', requirePermission('points:redemption:view', 'points:voucher:verify'), listRedemptions)
router.patch('/admin/redemptions/:id/approve', requirePermission('points:redemption:review'), approveRedemption)
router.patch('/admin/redemptions/:id/reject', requirePermission('points:redemption:review'), rejectRedemption)
router.post('/admin/vouchers/verify', requirePermission('points:voucher:verify'), verifyVoucher)
router.patch('/admin/leads/:id/stage', requirePermission('leads:referral:advance'), updateLeadStage)
router.patch('/admin/accounts/:id/role', requirePermission('system:user:role'), updateAccountRole)
router.patch('/admin/accounts/:id/status', requirePermission('system:user:status'), updateAccountStatus)
router.get('/admin/rewards', requirePermission('points:reward:edit', 'points:redemption:view'), listRewards)
router.post('/admin/rewards', requirePermission('points:reward:edit'), createReward)
router.get('/admin/rewards/:id', requirePermission('points:reward:edit', 'points:redemption:view'), getRewardDetail)
router.patch('/admin/rewards/:id', requirePermission('points:reward:edit'), updateReward)

module.exports = { router }
ox Aconst { app } = require('./app')
const { env } = require('./config/env')

app.listen(env.port, () => {
  console.log(`LiFangHe Education API running at http://localhost:${env.port}/api`)
})
]?Ux'100644 adminService.js ސ~ۣ=L5100644 authService.js Vܡnb5
G100644 courseService.js S 1OYt100644 index.js \7t';^F| 100644 pointService.js oD}PW3'tOP[100644 referralService.js i[*"zBfjSݛ100644 smsService.js bfMI:6yxe,const { createId } = require('../utils/ids')
const { hashPassword } = require('../utils/password')
const { PERMISSION_TREE, hasPermission, sanitizePermissions } = require('../constants/permissions')

const VALID_ROLES = ['parent', 'super_admin', 'campus_admin', 'advisor', 'teacher', 'finance']
const VALID_STATUSES = ['active', 'disabled']
const LEAD_STAGE_FLOW = ['待跟进', '已注册', '已预约', '已到课', '已报名']
const REFERRAL_STAGE_FLOW = ['待注册', '已注册', '已预约', '已到课', '已报名']
const REFERRAL_STAGE_EVENTS = {
  已注册: 'registered',
  已预约: 'booked',
  已到课: 'attended',
  已报名: 'purchased',
}
const DAY_MS = 24 * 60 * 60 * 1000

function formatDateKey(date) {
  const year = date.getFullYear()
  const month = String(date.getMonth() + 1).padStart(2, '0')
  const day = String(date.getDate()).padStart(2, '0')
  return `${year}-${month}-${day}`
}

function parseDateKey(value) {
  const match = String(value || '').match(/^(\d{4})-(\d{2})-(\d{2})$/)
  if (!match) return null
  const date = new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3]))
  return formatDateKey(date) === match[0] ? date : null
}

function shiftDate(date, days) {
  const next = new Date(date)
  next.setDate(next.getDate() + days)
  return next
}

function snapshotAccount(account) {
  if (!account) return null
  return {
    ...account,
    child: account.child ? { ...account.child } : account.child,
  }
}

function paginate(list = [], filters = {}) {
  const pageNum = Math.max(1, Number(filters.pageNum || filters.page || 1))
  const pageSize = Math.min(100, Math.max(1, Number(filters.pageSize || filters.limit || 10)))
  const start = (pageNum - 1) * pageSize
  return {
    list: list.slice(start, start + pageSize),
    total: list.length,
    pageNum,
    pageSize,
  }
}

function parseTrialBookingLimit(value, fallback = 1) {
  const limit = Number(value ?? fallback)
  if (!Number.isInteger(limit) || limit < 0 || limit > 99) {
    const error = new Error('可预约试听次数必须是 0 到 99 之间的整数')
    error.status = 400
    throw error
  }
  return limit
}

class AdminService {
  constructor(repository, pointService) {
    this.repository = repository
    this.pointService = pointService
  }

  async getDashboard() {
    const leads = await this.getDecoratedLeads()
    const transactions = await this.repository.listPointTransactions()
    const overview = this.getLeadOverview(leads, transactions)

    return {
      overview,
      funnel: [
        { label: '触达', value: leads.length },
        { label: '注册', value: leads.filter((lead) => lead.stage !== '待跟进').length },
        { label: '预约', value: leads.filter((lead) => ['已预约', '已到课', '已报名'].includes(lead.stage)).length },
        { label: '到课', value: overview.attended },
        { label: '报名', value: overview.purchased },
      ],
      leads,
    }
  }

  getLeadOverview(leads, transactions) {
    return {
      leads: leads.length,
      attended: leads.filter((lead) => ['已到课', '已报名'].includes(lead.stage)).length,
      purchased: leads.filter((lead) => lead.stage === '已报名').length,
      pointsCost: transactions
        .filter((tx) => tx.status !== 'pending')
        .reduce((sum, tx) => sum + Math.max(0, tx.amount), 0),
      revenue: 0,
    }
  }

  async listLeads(filters = {}) {
    const keyword = String(filters.keyword || '').trim()
    const stage = String(filters.stage || '').trim()
    const advisor = String(filters.advisor || '').trim()
    const leads = await this.getDecoratedLeads()
    const list = leads.filter((lead) => {
      const matchKeyword = !keyword || [lead.parent, lead.phone, lead.childGrade, lead.source, lead.channel].some((value) => String(value || '').includes(keyword))
      const matchStage = !stage || stage === '全部' || lead.stage === stage
      const matchAdvisor = !advisor || advisor === '全部' || lead.advisor === advisor
      return matchKeyword && matchStage && matchAdvisor
    })

    return {
      list,
      total: list.length,
    }
  }

  async getPcOverview(filters = {}) {
    const accounts = await this.repository.listAccounts()
    const leads = await this.getDecoratedLeads()
    const transactions = await this.repository.listPointTransactions()
    const purchased = leads.filter((lead) => lead.stage === '已报名').length
    const totalLeads = leads.length
    return {
      overview: {
        accounts: accounts.length,
        activeAccounts: accounts.filter((account) => account.status === 'active').length,
        leads: totalLeads,
        conversion: totalLeads ? Math.round((purchased / totalLeads) * 100) : 0,
        pointsCost: transactions
          .filter((tx) => tx.status !== 'pending')
          .reduce((sum, tx) => sum + Math.max(0, tx.amount), 0),
        revenue: 0,
      },
      accounts,
      roleMatrix: await this.repository.getRoleMatrix(),
      channelStats: this.buildChannelStats(leads),
      referralRules: await this.repository.getReferralRules(),
      trend: this.buildDailyTrend(accounts, leads, filters),
    }
  }

  buildDailyTrend(accounts = [], leads = [], filters = {}) {
    const today = new Date()
    today.setHours(0, 0, 0, 0)
    let endDate = parseDateKey(filters.endDate) || today
    if (endDate > today) endDate = today
    let startDate = parseDateKey(filters.startDate) || shiftDate(endDate, -29)
    if (startDate > endDate) startDate = shiftDate(endDate, -29)
    if ((endDate - startDate) / DAY_MS > 365) startDate = shiftDate(endDate, -365)

    const dates = []
    const usersByDate = new Map()
    const leadsByDate = new Map()
    for (let cursor = new Date(startDate); cursor <= endDate; cursor = shiftDate(cursor, 1)) {
      const key = formatDateKey(cursor)
      dates.push(key)
      usersByDate.set(key, 0)
      leadsByDate.set(key, 0)
    }

    accounts
      .filter((account) => account.role === 'parent')
      .forEach((account) => this.incrementDailyCount(usersByDate, account.createdAt))
    leads.forEach((lead) => this.incrementDailyCount(leadsByDate, lead.createdAt))

    return {
      startDate: formatDateKey(startDate),
      endDate: formatDateKey(endDate),
      dates,
      users: dates.map((date) => usersByDate.get(date) || 0),
      leads: dates.map((date) => leadsByDate.get(date) || 0),
    }
  }

  incrementDailyCount(counter, createdAt) {
    const date = String(createdAt || '').slice(0, 10)
    if (counter.has(date)) counter.set(date, counter.get(date) + 1)
  }

  buildChannelStats(leads = []) {
    const rows = new Map()
    leads.forEach((lead) => {
      const channel = String(lead.channel || '未标记渠道').trim() || '未标记渠道'
      const current = rows.get(channel) || { channel, leads: 0, attended: 0, purchased: 0 }
      current.leads += 1
      if (['已到课', '已报名'].includes(lead.stage)) current.attended += 1
      if (lead.stage === '已报名') current.purchased += 1
      rows.set(channel, current)
    })
    return [...rows.values()].sort((left, right) => right.leads - left.leads || left.channel.localeCompare(right.channel, 'zh-CN'))
  }

  async getAccounts(filters = {}) {
    const keyword = String(filters.keyword || '').trim()
    const role = String(filters.role || '').trim()
    const status = String(filters.status || '').trim()
    const wechatBound = String(filters.wechatBound || '').trim()
    const accounts = await this.repository.listAccounts()
    const filtered = accounts.filter((account) => {
      const matchKeyword = !keyword || [
        account.name,
        account.phone,
        account.username,
        account.id,
        account.roleName,
        account.role,
        account.wechatNickname,
        account.wechatOpenid,
        account.wechatUnionid,
      ].some((value) => String(value || '').toLowerCase().includes(keyword.toLowerCase()))
      const matchRole = !role || role === 'all' || account.role === role
      const matchStatus = !status || status === 'all' || account.status === status
      const isWechatBound = Boolean(account.wechatOpenid || account.wechatUnionid)
      const matchWechat = !wechatBound
        || wechatBound === 'all'
        || (wechatBound === 'bound' && isWechatBound)
        || (wechatBound === 'unbound' && !isWechatBound)
      return matchKeyword && matchRole && matchStatus && matchWechat
    })
    if (String(filters.paged || '') !== '1') return this.decorateInvitationRelations(filtered)
    const page = paginate(filtered, filters)
    return {
      ...page,
      list: await this.decorateInvitationRelations(page.list),
      overview: {
        total: filtered.length,
        active: filtered.filter((item) => item.status === 'active').length,
        parents: filtered.filter((item) => item.role === 'parent').length,
        admins: filtered.filter((item) => item.role !== 'parent').length,
        wechatBound: filtered.filter((item) => item.wechatOpenid || item.wechatUnionid).length,
      },
    }
  }

  async decorateInvitationRelations(accounts = []) {
    const appointments = await this.repository.listAppointments()
    const appointmentUsage = appointments.reduce((result, item) => {
      if (item.status !== 'cancelled') result.set(item.userId, (result.get(item.userId) || 0) + 1)
      return result
    }, new Map())

    return Promise.all(accounts.map(async (account) => {
      const trialBookingLimit = Number.isInteger(Number(account.trialBookingLimit)) ? Number(account.trialBookingLimit) : 1
      const trialBookingUsed = appointmentUsage.get(account.id) || 0
      const bookingQuota = {
        trialBookingLimit,
        trialBookingUsed,
        trialBookingRemaining: Math.max(0, trialBookingLimit - trialBookingUsed),
      }
      if (account.role !== 'parent') {
        return {
          ...account,
          ...bookingQuota,
          invitedBy: null,
          invitedUsers: [],
          invitedCount: 0,
        }
      }

      const referral = await this.repository.findReferralByInvitee?.(account.id)
      const inviter = referral?.inviterId ? await this.repository.findUserById(referral.inviterId) : null
      const invitedRecords = await this.repository.getReferralsByInviter(account.id)
      const invitedUsers = await Promise.all(invitedRecords.map(async (item) => {
        const invitee = item.inviteeId ? await this.repository.findUserById(item.inviteeId) : null
        return {
          id: invitee?.id || item.inviteeId || '',
          name: invitee?.name || item.name,
          phone: invitee?.phone || item.phone,
          status: item.status,
          childGrade: item.childGrade,
          channel: item.channel,
        }
      }))

      return {
        ...account,
        ...bookingQuota,
        invitedBy: inviter
          ? {
              id: inviter.id,
              name: inviter.name,
              phone: inviter.phone,
            }
          : null,
        invitedUsers,
        invitedCount: invitedUsers.length,
      }
    }))
  }

  async getRoles() {
    return {
      roles: await this.repository.getRoleMatrix(),
      permissionTree: PERMISSION_TREE,
    }
  }

  async updateRolePermissions(role, payload = {}) {
    if (role === 'parent') {
      const error = new Error('家长角色不参与后台权限配置')
      error.status = 400
      throw error
    }
    const roleInfo = await this.repository.findRole(role)
    if (!roleInfo) {
      const error = new Error('角色不存在')
      error.status = 404
      throw error
    }
    const permissions = sanitizePermissions(payload.permissions || [])
    if (role === 'super_admin' && !permissions.includes('*:*:*')) {
      permissions.unshift('*:*:*')
    }
    return this.repository.updateRolePermissions(role, permissions)
  }

  async createAccount(payload = {}) {
    const role = String(payload.role || 'parent').trim()
    const phone = String(payload.phone || '').trim()
    const username = String(payload.username || phone || '').trim()
    const name = String(payload.name || '').trim()
    const password = String(payload.password || '').trim()
    const status = String(payload.status || 'active').trim()
    const trialBookingLimit = parseTrialBookingLimit(payload.trialBookingLimit)

    if (!VALID_ROLES.includes(role)) {
      const error = new Error('角色无效')
      error.status = 400
      throw error
    }
    if (!VALID_STATUSES.includes(status)) {
      const error = new Error('账号状态无效')
      error.status = 400
      throw error
    }
    if (!name || !phone || !/^1\d{10}$/.test(phone)) {
      const error = new Error('姓名和正确手机号必填')
      error.status = 400
      throw error
    }
    if (!username) {
      const error = new Error('登录账号不能为空')
      error.status = 400
      throw error
    }
    const existed = await this.repository.findUserByLoginAccount(username)
    const phoneExisted = await this.repository.findUserByPhone(phone)
    if (existed || phoneExisted) {
      const error = new Error('账号或手机号已存在')
      error.status = 409
      throw error
    }

    const roleInfo = await this.repository.findRole(role)
    const user = await this.repository.addUser({
      id: createId(role === 'parent' ? 'parent' : 'user'),
      username,
      passwordHash: password ? hashPassword(password) : null,
      role,
      roleName: roleInfo?.name || role,
      name,
      phone,
      status,
      trialBookingLimit,
      inviterCode: role === 'parent' ? `LFH-${Math.random().toString(36).slice(2, 7).toUpperCase()}` : null,
      phoneBound: true,
      phoneBoundAt: new Date().toISOString().slice(0, 16).replace('T', ' '),
      child: payload.childName
        ? {
            id: createId('child'),
            name: String(payload.childName).trim(),
            grade: String(payload.childGrade || '待确认').trim(),
            focus: String(payload.childFocus || '待测评').trim(),
          }
        : undefined,
    })

    if (role === 'parent') await this.repository.getPointAccount(user.id)
    return this.repository.withPermissions(user)
  }

  async updateAccount(id, payload = {}, operator = null) {
    const account = await this.repository.findUserById(id)
    if (!account) {
      const error = new Error('账号不存在')
      error.status = 404
      throw error
    }

    const role = payload.role !== undefined ? String(payload.role || '').trim() : account.role
    const status = payload.status !== undefined ? String(payload.status || '').trim() : account.status
    const phone = payload.phone !== undefined ? String(payload.phone || '').trim() : account.phone
    const username = payload.username !== undefined ? String(payload.username || '').trim() : account.username
    const name = payload.name !== undefined ? String(payload.name || '').trim() : account.name
    const password = String(payload.password || '').trim()
    const trialBookingLimit = parseTrialBookingLimit(payload.trialBookingLimit, account.trialBookingLimit ?? 1)

    if (operator && role !== account.role && !hasPermission(operator, ['system:user:role'])) {
      const error = new Error('没有分配角色权限')
      error.status = 403
      throw error
    }
    if (operator && status !== account.status && !hasPermission(operator, ['system:user:status'])) {
      const error = new Error('没有启停账号权限')
      error.status = 403
      throw error
    }

    if (!VALID_ROLES.includes(role)) {
      const error = new Error('角色无效')
      error.status = 400
      throw error
    }
    if (!VALID_STATUSES.includes(status)) {
      const error = new Error('账号状态无效')
      error.status = 400
      throw error
    }
    if (!name || !phone || !/^1\d{10}$/.test(phone)) {
      const error = new Error('姓名和正确手机号必填')
      error.status = 400
      throw error
    }
    if (!username) {
      const error = new Error('登录账号不能为空')
      error.status = 400
      throw error
    }

    const accounts = await this.repository.listAccounts()
    const duplicated = accounts.find((item) => item.id !== id && (item.username === username || item.phone === phone))
    if (duplicated) {
      const error = new Error('账号或手机号已存在')
      error.status = 409
      throw error
    }

    const previous = snapshotAccount(account)
    const roleInfo = await this.repository.findRole(role)
    const childName = String(payload.childName !== undefined ? payload.childName : account.child?.name || '').trim()
    const childGrade = String(payload.childGrade !== undefined ? payload.childGrade : account.child?.grade || '').trim()
    const childFocus = String(payload.childFocus !== undefined ? payload.childFocus : account.child?.focus || '').trim()
    const patch = {
      username,
      role,
      roleName: roleInfo?.name || role,
      name,
      phone,
      status,
      trialBookingLimit,
      phoneBound: true,
      phoneBoundAt: account.phoneBoundAt || new Date().toISOString().slice(0, 16).replace('T', ' '),
      inviterCode: role === 'parent' ? account.inviterCode || `LFH-${Math.random().toString(36).slice(2, 7).toUpperCase()}` : account.inviterCode,
      child: role === 'parent'
        ? {
            id: account.child?.id || createId('child'),
            name: childName || '未填写',
            grade: childGrade || '待确认',
            focus: childFocus || '待测评',
          }
        : null,
    }
    if (password) patch.passwordHash = hashPassword(password)

    const updated = await this.repository.updateUser(id, patch)
    if (role === 'parent') await this.repository.getPointAccount(id)
    await this.syncParentDisplayInfo(previous, updated)
    return this.repository.withPermissions(updated)
  }

  async deleteAccount(id) {
    const account = await this.repository.findUserById(id)
    if (!account) {
      const error = new Error('账号不存在')
      error.status = 404
      throw error
    }
    if (account.role === 'super_admin') {
      const superAdmins = (await this.repository.listAccounts()).filter((item) => item.role === 'super_admin' && item.status !== 'disabled')
      if (superAdmins.length <= 1) {
        const error = new Error('至少保留一个启用的超级管理员')
        error.status = 400
        throw error
      }
    }
    const [transactions, appointments, redemptions, invitedRecords, invitedBy] = await Promise.all([
      this.repository.getPointTransactions(id),
      this.repository.listAppointments({ userId: id }),
      this.repository.listRedemptions({ userId: id }),
      this.repository.getReferralsByInviter(id),
      this.repository.findReferralByInvitee?.(id),
    ])
    if (transactions.length || appointments.length || redemptions.length || invitedRecords.length || invitedBy) {
      const error = new Error('该用户已有推荐、积分、预约或兑换记录，请停用账号以保留业务历史')
      error.status = 409
      throw error
    }
    await this.repository.deleteUser(id)
    return this.repository.withPermissions(account)
  }

  async updateAccountRole(id, role) {
    const account = await this.repository.updateAccountRole(id, role)
    if (!account) {
      const error = new Error('账号或角色不存在')
      error.status = 404
      throw error
    }
    return account
  }

  async updateAccountStatus(id, status) {
    if (!VALID_STATUSES.includes(status)) {
      const error = new Error('账号状态无效')
      error.status = 400
      throw error
    }
    const account = await this.repository.updateAccountStatus(id, status)
    if (!account) {
      const error = new Error('账号不存在')
      error.status = 404
      throw error
    }
    return account
  }

  async updateLeadStage(id, stage) {
    if (!LEAD_STAGE_FLOW.includes(stage)) {
      const error = new Error('线索阶段无效')
      error.status = 400
      throw error
    }
    const lead = await this.repository.updateLeadStage(id, stage)
    if (!lead) {
      const error = new Error('线索不存在')
      error.status = 404
      throw error
    }
    await this.syncReferralWithLeadStage(lead, stage)
    return lead
  }

  async confirmLeadPurchase(id) {
    return this.updateLeadStage(id, '已报名')
  }

  async getDecoratedLeads() {
    const leads = await this.repository.getLeads()
    return Promise.all(leads.map((lead) => this.decorateLead(lead)))
  }

  async decorateLead(lead) {
    const referral = await this.findReferralByLead(lead)
    if (!referral) return lead
    const invitee = referral.inviteeId ? await this.repository.findUserById(referral.inviteeId) : null
    const hasIdentityLink = Boolean(
      (lead.referralId && lead.referralId === referral.id)
      || (lead.inviteeId && referral.inviteeId && lead.inviteeId === referral.inviteeId),
    )
    return {
      ...lead,
      referralId: referral.id || lead.referralId,
      inviteeId: referral.inviteeId || lead.inviteeId,
      parent: invitee?.name || referral.name || lead.parent,
      phone: invitee?.phone || referral.phone || '',
      childGrade: invitee?.child?.grade || referral.childGrade || '',
      stage: hasIdentityLink ? this.maxLeadStage(lead.stage, referral.status) : lead.stage,
    }
  }

  maxLeadStage(leadStage, referralStage) {
    const leadRank = LEAD_STAGE_FLOW.indexOf(leadStage)
    const referralRank = LEAD_STAGE_FLOW.indexOf(referralStage)
    if (referralRank < 0) return leadStage
    if (leadRank < 0) return referralStage
    return LEAD_STAGE_FLOW[Math.max(leadRank, referralRank)]
  }

  async syncParentDisplayInfo(previous, current) {
    if (!current || current.role !== 'parent') return
    const referrals = await this.repository.updateReferralInviteeInfo?.(current.id, {
      name: current.name,
      phone: current.phone,
      childGrade: current.child?.grade || '待确认',
    })
    if (Array.isArray(referrals) && referrals.length && this.repository.updateLeadInfoByReferral) {
      await Promise.all(referrals.map((referral) => this.repository.updateLeadInfoByReferral(referral, {
        parent: current.name,
      }, previous || {})))
      return
    }
    if (previous?.name && previous.name !== current.name) {
      await this.repository.updateLeadsByParent?.(previous.name, { parent: current.name })
    }
  }

  async syncReferralWithLeadStage(lead, targetStage) {
    if (!this.pointService || !REFERRAL_STAGE_EVENTS[targetStage]) return null
    const referral = await this.findReferralByLead(lead)
    if (!referral) return null

    const currentRank = Math.max(0, REFERRAL_STAGE_FLOW.indexOf(referral.status))
    const targetRank = REFERRAL_STAGE_FLOW.indexOf(targetStage)
    if (targetRank <= currentRank) return referral

    let nextReferral = referral
    for (let rank = currentRank + 1; rank <= targetRank; rank += 1) {
      const nextStatus = REFERRAL_STAGE_FLOW[rank]
      const event = REFERRAL_STAGE_EVENTS[nextStatus]
      if (!event) continue
      nextReferral = await this.applyReferralStageReward(nextReferral, nextStatus, event)
    }
    return nextReferral
  }

  async findReferralByLead(lead) {
    const matchedReferral = await this.repository.findReferralForLead?.(lead)
    if (matchedReferral) return matchedReferral

    if (lead?.referralId && this.repository.findReferralById) {
      const referral = await this.repository.findReferralById(lead.referralId)
      if (referral) return referral
    }
    if (lead?.inviteeId && this.repository.findReferralByInvitee) {
      const referral = await this.repository.findReferralByInvitee(lead.inviteeId)
      if (referral) return referral
    }
    if (!lead?.parent || !lead?.source) return null
    const accounts = await this.repository.listAccounts()
    const inviter = accounts.find((account) => account.role === 'parent' && [account.id, account.name, account.phone].filter(Boolean).includes(lead.source))
    if (!inviter) return null

    const records = await this.repository.getReferralsByInviter(inviter.id)
    const matches = []
    for (const record of records) {
      const invitee = record.inviteeId ? await this.repository.findUserById(record.inviteeId) : null
      const fallbackNames = [
        invitee?.name,
        record.name,
        invitee?.phone ? `家长${String(invitee.phone).slice(-4)}` : '',
        record.phone ? `家长${String(record.phone).slice(-4)}` : '',
      ].filter(Boolean)
      if (fallbackNames.includes(lead.parent)) matches.push(record)
    }
    if (!matches.length) return null
    return matches.find((record) => !lead.channel || record.channel === lead.channel) || matches[0]
  }

  async applyReferralStageReward(referral, nextStatus, event) {
    const rule = await this.pointService.getReferralRule(event) || { inviter: 0, invitee: 0 }
    const inviterPoints = Number(rule.inviter || 0)
    const inviteePoints = Number(rule.invitee || 0)
    const inviteeId = referral.inviteeId

    if (event === 'attended') {
      await this.settleReferralPending(referral.inviterId, `${referral.name}确认到课`)
      if (inviteeId) await this.settleReferralPending(inviteeId, '推荐确认到课')
    }

    if (inviterPoints > 0) {
      await this.pointService.grant(referral.inviterId, `${referral.name}${rule.title || nextStatus}`, inviterPoints, event === 'booked' ? 'pending' : 'available')
    }
    if (inviteeId && inviteePoints > 0) {
      await this.pointService.grant(inviteeId, `推荐${rule.title || nextStatus}奖励`, inviteePoints, event === 'booked' ? 'pending' : 'available')
    }

    return this.repository.updateReferral(referral.id, {
      status: nextStatus,
      reward: Number(referral.reward || 0) + inviterPoints,
    })
  }

  async settleReferralPending(userId, title) {
    const account = await this.repository.getPointAccount(userId)
    if (Number(account.pending || 0) > 0) {
      await this.pointService.settlePending(userId, account.pending, title)
    }
  }
}

module.exports = { AdminService }
	蹲x)Cּconst { hashPassword, verifyPassword } = require('../utils/password')
const { createId } = require('../utils/ids')
const { env } = require('../config/env')
const { createSessionToken } = require('../utils/sessionToken')

function snapshotAccount(account) {
  if (!account) return null
  return {
    ...account,
    child: account.child ? { ...account.child } : account.child,
  }
}

class AuthService {
  constructor(repository, pointService, smsService) {
    this.repository = repository
    this.pointService = pointService
    this.smsService = smsService
  }

  withSession(account) {
    if (!account) return account
    return {
      ...account,
      sessionToken: createSessionToken(account.id),
    }
  }

  async login(credentials = {}) {
    const loginAccount = String(credentials.account || credentials.username || credentials.phone || credentials.accountId || '').trim()
    const password = String(credentials.password || '')
    if (!loginAccount || !password) {
      const error = new Error('账号和密码不能为空')
      error.status = 400
      throw error
    }
    const rawAccount = await this.repository.findUserByLoginAccount(loginAccount)
    if (!rawAccount) {
      const error = new Error('账号或密码错误')
      error.status = 401
      throw error
    }
    if (!verifyPassword(password, rawAccount.passwordHash)) {
      const error = new Error('账号或密码错误')
      error.status = 401
      throw error
    }
    const account = await this.repository.touchLogin(rawAccount.id)
    if (!account) {
      const error = new Error('账号不存在')
      error.status = 404
      throw error
    }
    if (account.status === 'disabled') {
      const error = new Error('账号已停用')
      error.status = 403
      throw error
    }
    return this.withSession(account)
  }

  async loginByPhone(payload = {}) {
    const phone = String(payload.phone || '').trim()
    const code = String(payload.code || '').trim()
    const inviterCode = String(payload.inviterCode || payload.inviteCode || '').trim().toUpperCase()
    if (!/^1\d{10}$/.test(phone)) {
      const error = new Error('手机号格式不正确')
      error.status = 400
      throw error
    }
    this.verifySmsCode(phone, code, 'login')

    let account = await this.repository.findUserByPhone(phone)
    const isNewAccount = !account
    if (isNewAccount && inviterCode) await this.assertInviteCode(inviterCode)
    if (!account) {
      account = await this.repository.addUser({
        id: createId('parent'),
        username: phone,
        role: 'parent',
        roleName: '家长',
        name: `家长${phone.slice(-4)}`,
        phone,
        status: 'active',
        inviterCode: `LFH-${Math.random().toString(36).slice(2, 7).toUpperCase()}`,
        phoneBound: true,
        phoneBoundAt: new Date().toISOString().slice(0, 16).replace('T', ' '),
      })
      await this.repository.getPointAccount(account.id)
      if (inviterCode) await this.applyReferralCode(account, inviterCode, '小程序邀请码')
    }

    const safeAccount = await this.repository.touchLogin(account.id)
    if (!safeAccount) {
      const error = new Error('账号不存在')
      error.status = 404
      throw error
    }
    if (safeAccount.status === 'disabled') {
      const error = new Error('账号已停用')
      error.status = 403
      throw error
    }
    if (!safeAccount.phoneBound) {
      const error = new Error('请先绑定手机号')
      error.status = 403
      throw error
    }
    return this.withSession(safeAccount)
  }

  async loginByWechat(payload = {}) {
    const code = String(payload.code || '').trim()
    if (!code) {
      const error = new Error('缺少微信登录 code')
      error.status = 400
      throw error
    }

    const wechat = await this.exchangeWechatCode(code)
    const account = await this.findWechatAccount(wechat)
    if (!account) {
      return { needPhoneBinding: true }
    }
    if (account.status === 'disabled') {
      const error = new Error('账号已停用')
      error.status = 403
      throw error
    }
    if (!account.phoneBound || !account.phone) {
      return { needPhoneBinding: true, account: await this.repository.withPermissions(account) }
    }
    return { needPhoneBinding: false, account: this.withSession(await this.repository.touchLogin(account.id)) }
  }

  async bindWechatPhone(payload = {}) {
    const phone = String(payload.phone || '').trim()
    const code = String(payload.code || '').trim()
    const name = String(payload.name || '').trim()
    const inviterCode = String(payload.inviterCode || payload.inviteCode || '').trim().toUpperCase()
    if (!/^1\d{10}$/.test(phone)) {
      const error = new Error('手机号格式不正确')
      error.status = 400
      throw error
    }
    this.verifySmsCode(phone, code, 'bind_wechat')

    const wechatCode = String(payload.wechatCode || payload.wxCode || '').trim()
    if (!wechatCode) {
      const error = new Error('缺少微信登录 code')
      error.status = 400
      throw error
    }
    const wechat = await this.exchangeWechatCode(wechatCode)
    const wechatAccount = await this.findWechatAccount(wechat)
    const phoneAccount = await this.repository.findUserByPhone(phone)
    if (wechatAccount?.phone && wechatAccount.phone !== phone) {
      const error = new Error('该微信已绑定其他手机号，请在当前账号的账号安全中换绑手机号')
      error.status = 409
      throw error
    }
    if (phoneAccount && this.isAccountBoundToOtherWechat(phoneAccount, wechat)) {
      const error = new Error('该手机号已绑定其他微信，请使用原微信登录或联系管理员处理')
      error.status = 409
      throw error
    }
    if (wechatAccount && phoneAccount && wechatAccount.id !== phoneAccount.id) {
      const error = new Error('该微信和手机号已分别绑定不同账号，请联系管理员处理')
      error.status = 409
      throw error
    }

    let account = phoneAccount || wechatAccount
    const isNewAccount = !account
    if (isNewAccount && inviterCode) await this.assertInviteCode(inviterCode)
    if (account) {
      const previous = snapshotAccount(account)
      account = await this.repository.updateUser(account.id, {
        name: name || account.name,
        phone,
        wechatOpenid: wechat.openid,
        wechatUnionid: wechat.unionid || account.wechatUnionid,
        loginProvider: 'wechat_phone',
        phoneBound: true,
        phoneBoundAt: new Date().toISOString().slice(0, 16).replace('T', ' '),
        wechatBoundAt: account.wechatBoundAt || new Date().toISOString().slice(0, 16).replace('T', ' '),
      })
      await this.syncParentDisplayInfo(previous, account)
    } else {
      account = await this.repository.addUser({
        id: createId('parent'),
        username: phone,
        role: 'parent',
        roleName: '家长',
        name: name || `家长${phone.slice(-4)}`,
        phone,
        wechatOpenid: wechat.openid,
        wechatUnionid: wechat.unionid || null,
        loginProvider: 'wechat_phone',
        status: 'active',
        inviterCode: `LFH-${Math.random().toString(36).slice(2, 7).toUpperCase()}`,
        phoneBound: true,
        phoneBoundAt: new Date().toISOString().slice(0, 16).replace('T', ' '),
        wechatBoundAt: new Date().toISOString().slice(0, 16).replace('T', ' '),
      })
      await this.repository.getPointAccount(account.id)
      if (inviterCode) await this.applyReferralCode(account, inviterCode, '微信绑定邀请码')
    }

    return this.withSession(await this.repository.touchLogin(account.id))
  }

  async updateProfile(accountId, payload = {}) {
    if (!accountId) {
      const error = new Error('缺少账号 ID')
      error.status = 400
      throw error
    }
    const account = await this.repository.findUserById(accountId)
    if (!account) {
      const error = new Error('账号不存在')
      error.status = 404
      throw error
    }

    const patch = {}
    if (payload.name !== undefined) patch.name = String(payload.name || '').trim()
    if (payload.wechatNickname !== undefined) patch.wechatNickname = String(payload.wechatNickname || '').trim()
    if (payload.wechatAvatar !== undefined) patch.wechatAvatar = String(payload.wechatAvatar || '').trim()
    const childName = String(payload.childName || account.child?.name || '').trim()
    const childGrade = String(payload.childGrade || account.child?.grade || '').trim()
    const childFocus = String(payload.childFocus || account.child?.focus || '').trim()
    if (childName || childGrade || childFocus) {
      patch.child = {
        id: account.child?.id || createId('child'),
        name: childName || '未填写',
        grade: childGrade || '待确认',
        focus: childFocus || '待测评',
      }
    }
    if (patch.name !== undefined && !patch.name) {
      const error = new Error('姓名不能为空')
      error.status = 400
      throw error
    }

    const previous = snapshotAccount(account)
    const updated = await this.repository.updateUser(accountId, patch)
    await this.syncParentDisplayInfo(previous, updated)
    return this.repository.withPermissions(updated)
  }

  async updatePassword(accountId, payload = {}) {
    const account = await this.repository.findUserById(accountId)
    if (!account) {
      const error = new Error('账号不存在')
      error.status = 404
      throw error
    }

    const oldPassword = String(payload.oldPassword || '').trim()
    const password = String(payload.password || payload.newPassword || '').trim()
    const confirmPassword = String(payload.confirmPassword || '').trim()
    if (password.length < 6) {
      const error = new Error('密码至少需要 6 位')
      error.status = 400
      throw error
    }
    if (confirmPassword && confirmPassword !== password) {
      const error = new Error('两次输入的密码不一致')
      error.status = 400
      throw error
    }
    if (account.passwordHash && !verifyPassword(oldPassword, account.passwordHash)) {
      const error = new Error('原密码不正确')
      error.status = 401
      throw error
    }

    const updated = await this.repository.updateUser(accountId, {
      passwordHash: hashPassword(password),
      loginProvider: account.wechatOpenid ? 'wechat_phone_password' : 'password',
    })
    return this.repository.withPermissions(updated)
  }

  async updatePhone(accountId, payload = {}) {
    const account = await this.repository.findUserById(accountId)
    if (!account) {
      const error = new Error('账号不存在')
      error.status = 404
      throw error
    }

    const phone = String(payload.phone || '').trim()
    const code = String(payload.code || '').trim()
    if (!/^1\d{10}$/.test(phone)) {
      const error = new Error('手机号格式不正确')
      error.status = 400
      throw error
    }
    this.verifySmsCode(phone, code, 'update_phone')

    const existed = await this.repository.findUserByPhone(phone)
    if (existed && existed.id !== accountId) {
      const error = new Error('该手机号已被其他账号绑定')
      error.status = 409
      throw error
    }

    const patch = {
      phone,
      phoneBound: true,
      phoneBoundAt: new Date().toISOString().slice(0, 16).replace('T', ' '),
    }
    if (!account.username || account.username === account.phone) {
      patch.username = phone
    }

    const previous = snapshotAccount(account)
    const updated = await this.repository.updateUser(accountId, patch)
    await this.syncParentDisplayInfo(previous, updated)
    return this.repository.withPermissions(updated)
  }

  async current(accountId) {
    if (!accountId) {
      const error = new Error('缺少账号 ID')
      error.status = 400
      throw error
    }
    const account = await this.repository.findUserById(accountId)
    if (!account) {
      const error = new Error('账号不存在')
      error.status = 404
      throw error
    }
    const safeAccount = await this.repository.withPermissions(account)
    const appointments = await this.repository.listAppointments({ userId: account.id })
    const trialBookingUsed = appointments.filter((item) => item.status !== 'cancelled').length
    const trialBookingLimit = Number.isInteger(Number(safeAccount.trialBookingLimit)) ? Number(safeAccount.trialBookingLimit) : 1
    return {
      ...safeAccount,
      trialBookingLimit,
      trialBookingUsed,
      trialBookingRemaining: Math.max(0, trialBookingLimit - trialBookingUsed),
    }
  }

  async findWechatAccount(wechat) {
    const byOpenid = wechat.openid ? await this.repository.findUserByLoginAccount(wechat.openid) : null
    if (byOpenid) return byOpenid
    return wechat.unionid ? await this.repository.findUserByLoginAccount(wechat.unionid) : null
  }

  isAccountBoundToOtherWechat(account, wechat) {
    if (!account) return false
    const openid = String(wechat?.openid || '').trim()
    const unionid = String(wechat?.unionid || '').trim()
    const accountOpenid = String(account.wechatOpenid || '').trim()
    const accountUnionid = String(account.wechatUnionid || '').trim()
    if (accountOpenid && accountOpenid !== openid) return true
    if (accountUnionid && unionid && accountUnionid !== unionid) return true
    return false
  }

  async assertInviteCode(inviterCode) {
    const inviter = await this.repository.findUserByInviteCode(inviterCode)
    if (!inviter) {
      const error = new Error('邀请码无效')
      error.status = 400
      throw error
    }
    return inviter
  }

  async sendSmsCode(payload = {}) {
    if (!this.smsService) {
      const error = new Error('短信服务未启用')
      error.status = 501
      throw error
    }
    return this.smsService.sendVerificationCode(payload)
  }

  verifySmsCode(phone, code, purpose) {
    if (!this.smsService) {
      const error = new Error('短信服务未启用')
      error.status = 501
      throw error
    }
    return this.smsService.verifyCode(phone, code, purpose)
  }

  async syncParentDisplayInfo(previous, current) {
    if (!current || current.role !== 'parent') return
    const referrals = await this.repository.updateReferralInviteeInfo?.(current.id, {
      name: current.name,
      phone: current.phone,
      childGrade: current.child?.grade || '待确认',
    })
    if (Array.isArray(referrals) && referrals.length && this.repository.updateLeadInfoByReferral) {
      await Promise.all(referrals.map((referral) => this.repository.updateLeadInfoByReferral(referral, {
        parent: current.name,
      }, previous || {})))
      return
    }
    if (previous?.name && previous.name !== current.name) {
      await this.repository.updateLeadsByParent?.(previous.name, { parent: current.name })
    }
  }

  async applyReferralCode(invitee, inviterCode, channel) {
    if (!inviterCode || !invitee || invitee.role !== 'parent') return null
    const inviter = await this.assertInviteCode(inviterCode)
    if (inviter.id === invitee.id) return null
    const records = await this.repository.getReferralsByInviter(inviter.id)
    const existed = records.some((item) => item.inviteeId === invitee.id || item.phone === invitee.phone)
    if (existed) return null

    const rule = await this.pointService.getReferralRule('registered') || { inviter: 20, invitee: 20 }
    const referral = await this.repository.addReferral({
      id: createId('rel'),
      inviterId: inviter.id,
      inviteeId: invitee.id,
      name: invitee.name,
      phone: invitee.phone,
      childGrade: invitee.child?.grade || '待确认',
      status: '已注册',
      reward: rule.inviter,
      channel,
    })

    await this.repository.addLead({
      id: createId('lead'),
      referralId: referral.id,
      inviteeId: invitee.id,
      parent: invitee.name,
      source: inviter.name,
      stage: '已注册',
      advisor: '未分配',
      risk: '正常',
      channel,
    })

    if (rule.inviter > 0) await this.pointService.grant(inviter.id, `${invitee.name}完成邀请码注册`, rule.inviter)
    if (rule.invitee > 0) await this.pointService.grant(invitee.id, '填写邀请码注册奖励', rule.invitee)
    return referral
  }

  async exchangeWechatCode(code) {
    if (!env.wechat.appId || !env.wechat.appSecret) {
      const error = new Error('微信登录尚未配置 AppID 和 AppSecret')
      error.status = 501
      throw error
    }
    const params = new URLSearchParams({
      appid: env.wechat.appId,
      secret: env.wechat.appSecret,
      js_code: code,
      grant_type: 'authorization_code',
    })
    const response = await fetch(`https://api.weixin.qq.com/sns/jscode2session?${params.toString()}`)
    const data = await response.json()
    if (!response.ok || data.errcode) {
      const error = new Error(data.errmsg || '微信登录校验失败')
      error.status = 400
      throw error
    }
    if (!data.openid) {
      const error = new Error('微信登录未返回 openid')
      error.status = 400
      throw error
    }
    return {
      openid: data.openid,
      unionid: data.unionid || null,
    }
  }
}

module.exports = { AuthService }
"rx7const { createId } = require('../utils/ids')

function readPagination(filters = {}) {
  const pageNum = Math.max(1, Number(filters.pageNum || filters.page || 1))
  const pageSize = Math.min(100, Math.max(1, Number(filters.pageSize || filters.limit || 10)))
  return { pageNum, pageSize }
}

function paginate(list = [], filters = {}) {
  const { pageNum, pageSize } = readPagination(filters)
  const start = (pageNum - 1) * pageSize
  return {
    list: list.slice(start, start + pageSize),
    total: list.length,
    pageNum,
    pageSize,
  }
}

function isValidDateTime(value) {
  const match = String(value || '').match(/^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2})$/)
  if (!match) return false
  const date = new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3]), Number(match[4]), Number(match[5]))
  return date.getFullYear() === Number(match[1])
    && date.getMonth() === Number(match[2]) - 1
    && date.getDate() === Number(match[3])
    && date.getHours() === Number(match[4])
    && date.getMinutes() === Number(match[5])
}

class CourseService {
  constructor(repository, pointService) {
    this.repository = repository
    this.pointService = pointService
  }

  normalizeCoursePayload(payload = {}) {
    const patch = {}
    if (payload.name !== undefined) patch.name = String(payload.name).trim()
    if (payload.nextTime !== undefined) {
      patch.nextTime = String(payload.nextTime).trim()
      if (patch.nextTime && !isValidDateTime(patch.nextTime)) {
        const error = new Error('试听时间格式无效，请重新选择日期和时间')
        error.status = 400
        throw error
      }
    }
    if (payload.campus !== undefined) patch.campus = String(payload.campus).trim()
    if (payload.teacher !== undefined) patch.teacher = String(payload.teacher).trim()
    if (payload.status !== undefined) patch.status = ['active', 'paused'].includes(payload.status) ? payload.status : 'active'
    if (payload.seats !== undefined) patch.seats = Number(payload.seats)
    if (payload.pointsBack !== undefined) patch.pointsBack = Number(payload.pointsBack)
    if (payload.trialMinutes !== undefined) patch.trialMinutes = Number(payload.trialMinutes)
    if (payload.tags !== undefined) patch.tags = Array.isArray(payload.tags) ? payload.tags : String(payload.tags).split(',').map((item) => item.trim()).filter(Boolean)

    for (const key of ['seats', 'pointsBack', 'trialMinutes']) {
      if (patch[key] !== undefined && (!Number.isFinite(patch[key]) || patch[key] < 0)) {
        const error = new Error('课程数值字段无效')
        error.status = 400
        throw error
      }
    }
    return patch
  }

  async listCourses(filters = {}) {
    const keyword = String(filters.keyword || '').trim()
    const campus = String(filters.campus || '').trim()
    const courses = await this.repository.getCourses()
    const active = courses.filter((course) => course.status !== 'paused')
    const filtered = active.filter((course) => {
      const matchKeyword = !keyword || [course.name, course.teacher, course.campus, ...(course.tags || [])].some((value) => String(value || '').includes(keyword))
      const matchCampus = !campus || campus === 'all' || campus === '全部' || course.campus === campus
      return matchKeyword && matchCampus
    })
    if (String(filters.paged || '') !== '1') return filtered
    return {
      ...paginate(filtered, filters),
      campusOptions: [...new Set(active.map((item) => item.campus).filter(Boolean))],
    }
  }

  async listAdminCourses(filters = {}) {
    const keyword = String(filters.keyword || '').trim()
    const status = String(filters.status || '').trim()
    const campus = String(filters.campus || '').trim()
    const courses = await this.repository.getCourses()
    const filtered = courses.filter((course) => {
      const matchKeyword = !keyword || [course.name, course.teacher, course.campus, ...(course.tags || [])].some((value) => String(value || '').includes(keyword))
      const matchStatus = !status || status === '全部' || course.status === status
      const matchCampus = !campus || campus === '全部' || course.campus === campus
      return matchKeyword && matchStatus && matchCampus
    })
    if (String(filters.paged || '') !== '1') return filtered
    const active = filtered.filter((item) => item.status === 'active')
    const overview = {
      total: filtered.length,
      active: active.length,
      seats: active.reduce((sum, item) => sum + Number(item.seats || 0), 0),
      campuses: new Set(filtered.map((item) => item.campus).filter(Boolean)).size,
      avgPoints: active.length ? Math.round(active.reduce((sum, item) => sum + Number(item.pointsBack || 0), 0) / active.length) : 0,
    }
    return {
      ...paginate(filtered, filters),
      overview,
      campusOptions: [...new Set(courses.map((item) => item.campus).filter(Boolean))],
    }
  }

  async getAdminCourse(id) {
    const course = await this.repository.findCourse(id)
    if (!course) {
      const error = new Error('课程不存在')
      error.status = 404
      throw error
    }
    return course
  }

  async listAppointments(filters = {}) {
    const appointments = await this.repository.listAppointments(filters)
    const overview = appointments.reduce(
      (result, item) => {
        result.total += 1
        result[item.status] = (result[item.status] || 0) + 1
        return result
      },
      { total: 0, booked: 0, attended: 0, cancelled: 0 },
    )
    return {
      ...paginate(appointments, filters),
      overview,
    }
  }

  async listUserAppointments(userId) {
    const value = String(userId || '').trim()
    if (!value) {
      const error = new Error('缺少预约账号')
      error.status = 400
      throw error
    }
    return this.repository.listAppointments({ userId: value })
  }

  async attendAppointment(id) {
    const appointment = await this.repository.findAppointmentById(id)
    if (!appointment) {
      const error = new Error('预约记录不存在')
      error.status = 404
      throw error
    }
    if (appointment.status === 'attended') return appointment
    if (appointment.status !== 'booked') {
      const error = new Error('当前预约状态不能确认到课')
      error.status = 400
      throw error
    }

    const referral = await this.repository.findReferralByInvitee?.(appointment.userId)
    const bookingRule = referral
      ? await this.pointService.getReferralRule('booked') || { inviter: 50, invitee: 50 }
      : { inviter: 0, invitee: 0 }
    const inviteeSettlement = await this.pointService.settlePendingReference(
      appointment.userId,
      'appointment',
      appointment.id,
      `确认${appointment.course?.name || '试听课'}到课`,
    )
    if (!inviteeSettlement) {
      await this.settleLegacyAppointmentPoints(
        appointment.userId,
        Number(appointment.course?.pointsBack || 0) + Number(bookingRule.invitee || 0),
        `确认${appointment.course?.name || '试听课'}到课`,
      )
    }

    if (referral && !['已到课', '已报名'].includes(referral.status)) {
      const inviterSettlement = await this.pointService.settlePendingReference(
        referral.inviterId,
        'appointment',
        appointment.id,
        `${referral.name}确认到课`,
      )
      if (!inviterSettlement) {
        await this.settleLegacyAppointmentPoints(
          referral.inviterId,
          Number(bookingRule.inviter || 0),
          `${referral.name}确认到课`,
        )
      }
      const rule = await this.pointService.getReferralRule('attended') || { inviter: 150, invitee: 100 }
      const updatedReferral = await this.repository.updateReferral(referral.id, {
        status: '已到课',
        reward: Number(referral.reward || 0) + Number(rule.inviter || 0),
      })
      await this.repository.updateLeadStageByReferral?.(updatedReferral || referral, '已到课')
      if (rule.inviter > 0) await this.pointService.grant(referral.inviterId, `${referral.name}确认到课`, rule.inviter)
      if (rule.invitee > 0) await this.pointService.grant(appointment.userId, '推荐到课奖励', rule.invitee)
    }

    return this.repository.updateAppointment(id, { status: 'attended' })
  }

  async settleLegacyAppointmentPoints(userId, expectedAmount, title) {
    if (expectedAmount <= 0) return null
    const account = await this.repository.getPointAccount(userId)
    const amount = Math.min(Number(account.pending || 0), expectedAmount)
    if (amount <= 0) return null
    return this.pointService.settlePending(userId, amount, title)
  }

  async createCourse(payload) {
    const course = {
      id: createId('course'),
      name: '新试听课程',
      tags: ['试听'],
      nextTime: '',
      pointsBack: 0,
      seats: 0,
      campus: '',
      teacher: '',
      trialMinutes: 45,
      status: 'paused',
      ...this.normalizeCoursePayload(payload),
    }
    if (!course.name) {
      const error = new Error('课程名称不能为空')
      error.status = 400
      throw error
    }
    return this.repository.addCourse(course)
  }

  async updateCourse(id, payload) {
    const patch = this.normalizeCoursePayload(payload)
    if (patch.name !== undefined && !patch.name) {
      const error = new Error('课程名称不能为空')
      error.status = 400
      throw error
    }
    const course = await this.repository.updateCourse(id, patch)
    if (!course) {
      const error = new Error('课程不存在')
      error.status = 404
      throw error
    }
    return course
  }

  async deleteCourse(id) {
    const appointments = await this.repository.listAppointments({ courseId: id })
    if (appointments.length) {
      const error = new Error('该课程已有预约记录，不能删除；请暂停课程以保留历史记录')
      error.status = 409
      throw error
    }
    if (patch.nextTime !== undefined && !patch.nextTime) {
      const error = new Error('请选择试听日期和时间')
      error.status = 400
      throw error
    }
    if (!course.nextTime) {
      const error = new Error('请选择试听日期和时间')
      error.status = 400
      throw error
    }
    const course = await this.repository.deleteCourse(id)
    if (!course) {
      const error = new Error('课程不存在')
      error.status = 404
      throw error
    }
    return course
  }

  async book(payload) {
    const userId = String(payload.userId || '').trim()
    if (!userId) {
      const error = new Error('缺少预约账号')
      error.status = 400
      throw error
    }
    const user = await this.repository.findUserById(userId)
    if (!user || user.role !== 'parent') {
      const error = new Error('家长账号不存在')
      error.status = 404
      throw error
    }
    if (user.status !== 'active') {
      const error = new Error('账号已停用，不能预约课程')
      error.status = 403
      throw error
    }
    if (!user.phoneBound || !user.phone) {
      const error = new Error('请先绑定手机号再预约课程')
      error.status = 400
      throw error
    }
    if (payload.childId && user.child?.id && payload.childId !== user.child.id) {
      const error = new Error('预约学生信息与当前账号不一致')
      error.status = 400
      throw error
    }
    const appointments = await this.repository.listAppointments({ userId })
    const activeAppointments = appointments.filter((item) => item.status !== 'cancelled')
    if (activeAppointments.some((item) => item.courseId === payload.courseId)) {
      const error = new Error('该试听课程已经预约，请勿重复预约')
      error.status = 400
      throw error
    }
    const trialBookingLimit = Number.isInteger(Number(user.trialBookingLimit)) ? Number(user.trialBookingLimit) : 1
    if (activeAppointments.length >= trialBookingLimit) {
      const error = new Error(trialBookingLimit > 0
        ? `当前账号最多可预约 ${trialBookingLimit} 次试听课程`
        : '当前账号暂不可预约试听课程')
      error.status = 400
      throw error
    }
    const course = await this.repository.findCourse(payload.courseId)
    if (!course) {
      const error = new Error('课程不存在')
      error.status = 404
      throw error
    }
    if (course.status !== 'active') {
      const error = new Error('该课程已暂停预约')
      error.status = 400
      throw error
    }
    if (course.seats <= 0) {
      const error = new Error('该课程已约满')
      error.status = 400
      throw error
    }

    const appointment = await this.repository.addAppointment({
      id: createId('appt'),
      userId,
      childId: payload.childId,
      courseId: payload.courseId,
      status: 'booked',
      createdAt: new Date().toISOString(),
    })

    await this.repository.updateCourse(course.id, { seats: course.seats - 1 })
    const referral = await this.repository.findReferralByInvitee?.(userId)
    if (referral && !['已预约', '已到课', '已报名'].includes(referral.status)) {
      const rule = await this.pointService.getReferralRule('booked') || { inviter: 50, invitee: 50 }
      const updatedReferral = await this.repository.updateReferral(referral.id, {
        status: '已预约',
        reward: Number(referral.reward || 0) + Number(rule.inviter || 0),
      })
      await this.repository.updateLeadStageByReferral?.(updatedReferral || referral, '已预约')
      const reference = { referenceType: 'appointment', referenceId: appointment.id }
      if (rule.inviter > 0) await this.pointService.grant(referral.inviterId, `${referral.name}预约试听`, rule.inviter, 'pending', reference)
      if (rule.invitee > 0) await this.pointService.grant(userId, '推荐预约试听奖励', rule.invitee, 'pending', reference)
    }
    if (course.pointsBack > 0) {
      await this.pointService.grant(userId, `预约${course.name}`, course.pointsBack, 'pending', {
        referenceType: 'appointment',
        referenceId: appointment.id,
      })
    }
    return appointment
  }
}

module.exports = { CourseService }
i6x`const { repository } = require('../repositories')
const { PointService } = require('./pointService')
const { ReferralService } = require('./referralService')
const { CourseService } = require('./courseService')
const { AdminService } = require('./adminService')
const { AuthService } = require('./authService')
const { SmsService } = require('./smsService')

const pointService = new PointService(repository)
const smsService = new SmsService()
const referralService = new ReferralService(repository, pointService, smsService)
const courseService = new CourseService(repository, pointService)
const adminService = new AdminService(repository, pointService)
const authService = new AuthService(repository, pointService, smsService)

module.exports = {
  repository,
  smsService,
  pointService,
  referralService,
  courseService,
  adminService,
  authService,
}
h4[xM]const { createId } = require('../utils/ids')

const REDEMPTION_STATUS_LABELS = {
  pending_review: '待审核',
  approved: '已发券',
  rejected: '已驳回',
  used: '已核销',
  expired: '已过期',
}

const VOUCHER_STATUS_LABELS = {
  not_issued: '未发券',
  issued: '待核销',
  used: '已核销',
  void: '已作废',
  expired: '已过期',
}

function toDateTime(value = new Date()) {
  return new Date(value).toISOString().slice(0, 16).replace('T', ' ')
}

function addDays(days) {
  const date = new Date()
  date.setDate(date.getDate() + days)
  return toDateTime(date)
}

function readPagination(filters = {}) {
  const pageNum = Math.max(1, Number(filters.pageNum || filters.page || 1))
  const pageSize = Math.min(100, Math.max(1, Number(filters.pageSize || filters.limit || 10)))
  return { pageNum, pageSize }
}

function paginate(list = [], filters = {}) {
  const { pageNum, pageSize } = readPagination(filters)
  const start = (pageNum - 1) * pageSize
  return {
    list: list.slice(start, start + pageSize),
    total: list.length,
    pageNum,
    pageSize,
  }
}

class PointService {
  constructor(repository) {
    this.repository = repository
  }

  async getCenter(userId) {
    const redemptions = await this.repository.listRedemptions({ userId })
    return {
      account: await this.repository.getPointAccount(userId),
      transactions: await this.repository.getPointTransactions(userId),
      rewards: await this.repository.getRewards(),
      redemptions: redemptions.map((item) => this.decorateRedemption(item)),
      expiryRule: await this.repository.getPointExpiryRule(),
      expirySchedules: await this.repository.listPointExpirySchedules(userId),
    }
  }

  async listUserRewards(userId, filters = {}) {
    const keyword = String(filters.keyword || '').trim()
    const rewards = await this.repository.listRewards(filters)
    const filtered = rewards.filter((reward) => {
      const matchKeyword = !keyword || [reward.name, reward.type, reward.description].some((value) => String(value || '').includes(keyword))
      return matchKeyword
    })
    return paginate(filtered, filters)
  }

  async listUserTransactions(userId, filters = {}) {
    const keyword = String(filters.keyword || '').trim()
    const transactions = await this.repository.getPointTransactions(userId)
    const filtered = transactions.filter((item) => {
      const matchKeyword = !keyword || [item.title, item.createdAt, item.status].some((value) => String(value || '').includes(keyword))
      const matchStatus = !filters.status || filters.status === 'all' || item.status === filters.status
      return matchKeyword && matchStatus
    })
    return paginate(filtered, filters)
  }

  async listUserRedemptions(userId, filters = {}) {
    const keyword = String(filters.keyword || '').trim()
    const redemptions = (await this.repository.listRedemptions({ userId })).map((item) => this.decorateRedemption(item))
    const filtered = redemptions.filter((item) => {
      const matchKeyword = !keyword || [item.id, item.reward?.name, item.voucherCode, item.statusLabel, item.rejectedReason].some((value) => String(value || '').includes(keyword))
      const matchStatus = !filters.status || filters.status === 'all' || item.status === filters.status
      return matchKeyword && matchStatus
    })
    return paginate(filtered, filters)
  }

  async getAdminManager() {
    const parentUsers = await this.repository.listParentUsers()
    const accounts = await Promise.all(parentUsers.map(async (user) => ({
      user,
      points: await this.repository.getPointAccount(user.id),
    })))
    const rawTransactions = await this.repository.listPointTransactions()
    const transactions = await Promise.all(rawTransactions.map(async (transaction) => ({
      ...transaction,
      userName: (await this.repository.findUserById(transaction.userId))?.name || transaction.userId,
    })))
    const rawExpirySchedules = await this.repository.listPointExpirySchedules()
    const expirySchedules = await Promise.all(rawExpirySchedules.map(async (item) => ({
      ...item,
      userName: (await this.repository.findUserById(item.userId))?.name || item.userId,
    })))
    const redemptions = await this.repository.listRedemptions()
    const overview = accounts.reduce(
      (result, item) => {
        result.available += item.points.available
        result.pending += item.points.pending
        result.frozen += item.points.frozen
        result.expiring += item.points.expiring
        return result
      },
      { available: 0, pending: 0, frozen: 0, expiring: 0 },
    )

    return {
      overview: {
        ...overview,
        parents: accounts.length,
        rewards: (await this.repository.getRewards()).length,
        transactions: transactions.length,
        pendingRedemptions: redemptions.filter((item) => item.status === 'pending_review').length,
        issuedVouchers: redemptions.filter((item) => item.voucherStatus === 'issued').length,
        nextExpireAt: expirySchedules[0]?.expireAt || '',
      },
      accounts,
      transactions,
      expiryRule: await this.repository.getPointExpiryRule(),
      expirySchedules,
      rewards: await this.repository.getRewards(),
      redemptions: redemptions.slice(0, 8).map((item) => this.decorateRedemption(item)),
      rules: await this.repository.getReferralRules(),
    }
  }

  async listPointAccounts(filters = {}) {
    const keyword = String(filters.keyword || '').trim()
    const parentUsers = await this.repository.listParentUsers()
    const rows = await Promise.all(parentUsers.map(async (user) => ({
      user,
      points: await this.repository.getPointAccount(user.id),
    })))
    const filtered = rows.filter((item) => {
      const matchKeyword = !keyword || [item.user.name, item.user.phone, item.user.id, item.user.child?.name].some((value) => String(value || '').includes(keyword))
      const matchStatus = !filters.pointStatus
        || filters.pointStatus === 'all'
        || (filters.pointStatus === 'available' && item.points.available > 0)
        || (filters.pointStatus === 'pending' && item.points.pending > 0)
        || (filters.pointStatus === 'expiring' && item.points.expiring > 0)
        || (filters.pointStatus === 'frozen' && item.points.frozen > 0)
      return matchKeyword && matchStatus
    })
    const overview = filtered.reduce(
      (result, item) => {
        result.available += item.points.available
        result.pending += item.points.pending
        result.frozen += item.points.frozen
        result.expiring += item.points.expiring
        return result
      },
      { available: 0, pending: 0, frozen: 0, expiring: 0 },
    )
    return {
      ...paginate(filtered, filters),
      overview: {
        ...overview,
        parents: filtered.length,
      },
    }
  }

  async listPointTransactions(filters = {}) {
    const keyword = String(filters.keyword || '').trim()
    const startDate = String(filters.startDate || '').trim()
    const endDate = String(filters.endDate || '').trim()
    const rawTransactions = await this.repository.listPointTransactions()
    const rows = await Promise.all(rawTransactions.map(async (transaction) => ({
      ...transaction,
      userName: (await this.repository.findUserById(transaction.userId))?.name || transaction.userId,
    })))
    const filtered = rows.filter((item) => {
      const matchUser = !filters.userId || item.userId === filters.userId
      const matchStatus = !filters.status || filters.status === 'all' || item.status === filters.status
      const matchDate = (!startDate || item.createdAt >= startDate) && (!endDate || item.createdAt <= endDate)
      const matchKeyword = !keyword || [item.userName, item.title, item.userId].some((value) => String(value || '').includes(keyword))
      return matchUser && matchStatus && matchDate && matchKeyword
    })
    const overview = filtered.reduce(
      (result, item) => {
        if (item.status === 'pending') result.pending += item.amount
        if (item.status !== 'pending' && item.amount > 0) result.income += item.amount
        if (item.status !== 'pending' && item.amount < 0) result.expense += Math.abs(item.amount)
        return result
      },
      { income: 0, expense: 0, pending: 0 },
    )
    return {
      ...paginate(filtered, filters),
      overview,
      expiryRule: await this.repository.getPointExpiryRule(),
      expirySchedules: await this.listPointExpirySchedules(filters),
    }
  }

  async listPointExpirySchedules(filters = {}) {
    const keyword = String(filters.expiryKeyword || filters.keyword || '').trim()
    const rawExpirySchedules = await this.repository.listPointExpirySchedules(filters.userId)
    const rows = await Promise.all(rawExpirySchedules.map(async (item) => ({
      ...item,
      userName: (await this.repository.findUserById(item.userId))?.name || item.userId,
    })))
    return rows.filter((item) => {
      const matchKeyword = !keyword || [item.userName, item.source, item.userId].some((value) => String(value || '').includes(keyword))
      const matchDate = (!filters.expireStart || item.expireAt >= filters.expireStart) && (!filters.expireEnd || item.expireAt <= filters.expireEnd)
      return matchKeyword && matchDate
    })
  }

  async getReferralRule(event) {
    const rules = await this.repository.getReferralRules()
    return rules.find((rule) => rule.event === event) || null
  }

  normalizeReferralRulePayload(payload = {}) {
    const patch = {}
    if (payload.title !== undefined) patch.title = String(payload.title).trim()
    if (payload.inviter !== undefined) patch.inviter = Number(payload.inviter)
    if (payload.invitee !== undefined) patch.invitee = Number(payload.invitee)
    if (!patch.title) {
      const error = new Error('规则名称不能为空')
      error.status = 400
      throw error
    }
    for (const key of ['inviter', 'invitee']) {
      if (!Number.isFinite(patch[key]) || patch[key] < 0) {
        const error = new Error('奖励积分必须是不小于 0 的数字')
        error.status = 400
        throw error
      }
      patch[key] = Math.round(patch[key])
    }
    return patch
  }

  async updateReferralRule(event, payload = {}) {
    const rule = await this.repository.updateReferralRule(event, this.normalizeReferralRulePayload(payload))
    if (!rule) {
      const error = new Error('推荐奖励规则不存在')
      error.status = 404
      throw error
    }
    return rule
  }

  async listRewards(filters = {}) {
    const rewards = await this.repository.listRewards(filters)
    const overview = rewards.reduce(
      (result, item) => {
        result.total += 1
        if (item.stock > 0) result.inStock += 1
        if (item.stock <= 0) result.soldOut += 1
        return result
      },
      { total: 0, inStock: 0, soldOut: 0 },
    )
    return {
      ...paginate(rewards, filters),
      overview,
    }
  }

  async getRewardDetail(id) {
    const reward = await this.repository.findReward(id)
    if (!reward) {
      const error = new Error('兑换权益不存在')
      error.status = 404
      throw error
    }
    const redemptions = (await this.repository.listRedemptions({ itemId: id })).slice(0, 10).map((item) => this.decorateRedemption(item))
    return { reward, redemptions }
  }

  normalizeRewardPayload(payload = {}) {
    const reward = {}
    if (payload.name !== undefined) reward.name = String(payload.name).trim()
    if (payload.type !== undefined) reward.type = String(payload.type).trim()
    if (payload.cost !== undefined) reward.cost = Number(payload.cost)
    if (payload.stock !== undefined) reward.stock = Number(payload.stock)
    if (payload.description !== undefined) reward.description = String(payload.description || '').trim()
    if (!reward.name) {
      const error = new Error('兑换权益名称不能为空')
      error.status = 400
      throw error
    }
    if (!reward.type) reward.type = '课程权益'
    if (!Number.isFinite(reward.cost) || reward.cost < 0) {
      const error = new Error('兑换积分无效')
      error.status = 400
      throw error
    }
    if (!Number.isFinite(reward.stock) || reward.stock < 0) {
      const error = new Error('库存数量无效')
      error.status = 400
      throw error
    }
    return reward
  }

  async createReward(payload = {}) {
    return this.repository.addReward({
      id: createId('reward'),
      description: '',
      ...this.normalizeRewardPayload(payload),
    })
  }

  async grant(userId, title, amount, status = 'available', reference = {}) {
    const account = await this.repository.getPointAccount(userId)
    const target = status === 'pending' ? 'pending' : 'available'
    account[target] += Number(amount)
    await this.repository.updatePointAccount(userId, account)
    return this.repository.addPointTransaction({
      id: createId('pt'),
      userId,
      title,
      amount,
      status,
      referenceType: reference.referenceType || '',
      referenceId: reference.referenceId || '',
      createdAt: new Date().toISOString().slice(0, 10),
    })
  }

  async adjust(userId, amount, title, status = 'available') {
    const numericAmount = Number(amount)
    if (!Number.isFinite(numericAmount) || numericAmount === 0) {
      const error = new Error('积分调整数量无效')
      error.status = 400
      throw error
    }
    const user = await this.repository.findUserById(userId)
    if (!user || user.role !== 'parent') {
      const error = new Error('家长账号不存在')
      error.status = 404
      throw error
    }
    const target = status === 'pending' ? 'pending' : 'available'
    const account = await this.repository.getPointAccount(userId)
    if (account[target] + numericAmount < 0) {
      const error = new Error('调整后积分不能小于 0')
      error.status = 400
      throw error
    }
    account[target] += numericAmount
    await this.repository.updatePointAccount(userId, account)
    return this.repository.addPointTransaction({
      id: createId('pt'),
      userId,
      title: title || '后台手动调整',
      amount: numericAmount,
      status: target,
      createdAt: new Date().toISOString().slice(0, 10),
    })
  }

  async settlePending(userId, amount, title = '手动确认到账', reference = {}) {
    const numericAmount = Number(amount)
    if (!Number.isFinite(numericAmount) || numericAmount <= 0) {
      const error = new Error('到账积分数量无效')
      error.status = 400
      throw error
    }
    const user = await this.repository.findUserById(userId)
    if (!user || user.role !== 'parent') {
      const error = new Error('家长账号不存在')
      error.status = 404
      throw error
    }
    const account = await this.repository.getPointAccount(userId)
    if (account.pending < numericAmount) {
      const error = new Error('待到账积分不足')
      error.status = 400
      throw error
    }
    account.pending -= numericAmount
    account.available += numericAmount
    await this.repository.updatePointAccount(userId, account)

    const pendingTransaction = await this.repository.addPointTransaction({
      id: createId('pt'),
      userId,
      title: `${title || '手动确认到账'}（待到账转出）`,
      amount: -numericAmount,
      status: 'pending',
      referenceType: reference.referenceType || '',
      referenceId: reference.referenceId || '',
      createdAt: new Date().toISOString().slice(0, 10),
    })
    const transaction = await this.repository.addPointTransaction({
      id: createId('pt'),
      userId,
      title: `${title || '手动确认到账'}（已到账）`,
      amount: numericAmount,
      status: 'available',
      referenceType: reference.referenceType || '',
      referenceId: reference.referenceId || '',
      createdAt: new Date().toISOString().slice(0, 10),
    })
    return {
      account,
      pendingTransaction,
      transaction,
    }
  }

  async settlePendingReference(userId, referenceType, referenceId, title) {
    if (!referenceType || !referenceId) return null
    const transactions = await this.repository.getPointTransactions(userId)
    const amount = transactions
      .filter((item) => item.status === 'pending' && item.referenceType === referenceType && item.referenceId === referenceId)
      .reduce((sum, item) => sum + Number(item.amount || 0), 0)
    if (amount <= 0) return null
    return this.settlePending(userId, amount, title, { referenceType, referenceId })
  }

  async updateReward(id, payload) {
    const patch = {}
    if (payload.name !== undefined) patch.name = String(payload.name).trim()
    if (payload.type !== undefined) patch.type = String(payload.type).trim()
    if (payload.cost !== undefined) patch.cost = Number(payload.cost)
    if (payload.stock !== undefined) patch.stock = Number(payload.stock)
    if (payload.description !== undefined) patch.description = String(payload.description || '').trim()
    if (patch.cost !== undefined && (!Number.isFinite(patch.cost) || patch.cost < 0)) {
      const error = new Error('兑换积分无效')
      error.status = 400
      throw error
    }
    if (patch.stock !== undefined && (!Number.isFinite(patch.stock) || patch.stock < 0)) {
      const error = new Error('库存数量无效')
      error.status = 400
      throw error
    }
    const reward = await this.repository.updateReward(id, patch)
    if (!reward) {
      const error = new Error('兑换权益不存在')
      error.status = 404
      throw error
    }
    return reward
  }

  async redeem(userId, itemId) {
    const user = await this.repository.findUserById(userId)
    if (!user || user.role !== 'parent') {
      const error = new Error('家长账号不存在')
      error.status = 404
      throw error
    }
    const reward = await this.repository.findReward(itemId)
    if (!reward) {
      const error = new Error('兑换权益不存在')
      error.status = 404
      throw error
    }

    const account = await this.repository.getPointAccount(userId)
    if (account.available < reward.cost) {
      const error = new Error('可用积分不足')
      error.status = 400
      throw error
    }
    if (Number(reward.stock || 0) <= 0) {
      const error = new Error('兑换权益库存不足')
      error.status = 400
      throw error
    }

    account.available -= reward.cost
    await this.repository.updatePointAccount(userId, account)
    await this.repository.updateReward(reward.id, { stock: reward.stock - 1 })
    await this.repository.addPointTransaction({
      id: createId('pt'),
      userId,
      title: `兑换${reward.name}`,
      amount: -reward.cost,
      status: 'available',
      createdAt: new Date().toISOString().slice(0, 10),
    })

    return this.repository.addRedemption({
      id: createId('redeem'),
      userId,
      itemId,
      costPoints: reward.cost,
      status: 'pending_review',
      voucherStatus: 'not_issued',
      createdAt: toDateTime(),
    })
  }

  async listRedemptions(filters = {}) {
    const list = await this.repository.listRedemptions(filters)
    const overviewFilters = {
      ...filters,
      status: 'all',
      voucherStatus: 'all',
    }
    const overviewList = await this.repository.listRedemptions(overviewFilters)
    const overview = overviewList.reduce(
      (result, item) => {
        result.total += 1
        result[item.status] = (result[item.status] || 0) + 1
        if (item.voucherStatus === 'issued') result.issuedVouchers += 1
        return result
      },
      { total: 0, pending_review: 0, approved: 0, rejected: 0, used: 0, expired: 0, issuedVouchers: 0 },
    )
    return {
      ...paginate(list.map((item) => this.decorateRedemption(item)), filters),
      overview,
      statusLabels: REDEMPTION_STATUS_LABELS,
      voucherStatusLabels: VOUCHER_STATUS_LABELS,
    }
  }

  decorateRedemption(order) {
    return {
      ...order,
      statusLabel: REDEMPTION_STATUS_LABELS[order.status] || order.status,
      voucherStatusLabel: VOUCHER_STATUS_LABELS[order.voucherStatus] || order.voucherStatus,
    }
  }

  createVoucherCode() {
    const time = Date.now().toString(36).toUpperCase()
    const random = Math.random().toString(36).slice(2, 6).toUpperCase()
    return `LFH-${time}-${random}`
  }

  async approveRedemption(id, operatorId, payload = {}) {
    const order = await this.repository.findRedemptionById(id)
    if (!order) {
      const error = new Error('兑换申请不存在')
      error.status = 404
      throw error
    }
    if (order.status !== 'pending_review') {
      const error = new Error('只有待审核申请可以通过')
      error.status = 400
      throw error
    }

    let voucherCode = this.createVoucherCode()
    for (let i = 0; i < 5; i += 1) {
      const exists = await this.repository.findRedemptionByVoucherCode(voucherCode)
      if (!exists) break
      voucherCode = this.createVoucherCode()
    }

    const expireAt = payload.expireAt ? String(payload.expireAt) : addDays(Number(payload.validDays || 30))
    return this.decorateRedemption(await this.repository.updateRedemption(id, {
      status: 'approved',
      voucherCode,
      voucherStatus: 'issued',
      reviewedBy: operatorId || '',
      reviewedAt: toDateTime(),
      expireAt,
    }))
  }

  async rejectRedemption(id, operatorId, payload = {}) {
    const order = await this.repository.findRedemptionById(id)
    if (!order) {
      const error = new Error('兑换申请不存在')
      error.status = 404
      throw error
    }
    if (!['pending_review', 'approved'].includes(order.status) || order.voucherStatus === 'used') {
      const error = new Error('当前状态不能驳回')
      error.status = 400
      throw error
    }

    const account = await this.repository.getPointAccount(order.userId)
    const refund = Number(order.costPoints || order.reward?.cost || 0)
    if (refund > 0) {
      account.available += refund
      await this.repository.updatePointAccount(order.userId, account)
      await this.repository.addPointTransaction({
        id: createId('pt'),
        userId: order.userId,
        title: `兑换驳回退还${order.reward?.name || '学习权益'}`,
        amount: refund,
        status: 'available',
        createdAt: new Date().toISOString().slice(0, 10),
      })
    }

    return this.decorateRedemption(await this.repository.updateRedemption(id, {
      status: 'rejected',
      voucherStatus: 'void',
      reviewedBy: operatorId || '',
      reviewedAt: toDateTime(),
      rejectedReason: String(payload.reason || '审核未通过').trim(),
    }))
  }

  async verifyVoucher(payload = {}) {
    const id = String(payload.id || '').trim()
    const code = String(payload.code || '').trim().toUpperCase()
    const order = id ? await this.repository.findRedemptionById(id) : await this.repository.findRedemptionByVoucherCode(code)
    if (!order) {
      const error = new Error('券码不存在')
      error.status = 404
      throw error
    }
    if (order.status !== 'approved' || order.voucherStatus !== 'issued') {
      const error = new Error('当前券码不能核销')
      error.status = 400
      throw error
    }
    if (order.expireAt && new Date(order.expireAt.replace(' ', 'T')).getTime() < Date.now()) {
      await this.repository.updateRedemption(order.id, { status: 'expired', voucherStatus: 'expired' })
      const error = new Error('券码已过期')
      error.status = 400
      throw error
    }

    return this.decorateRedemption(await this.repository.updateRedemption(order.id, {
      status: 'used',
      voucherStatus: 'used',
      usedAt: toDateTime(),
      usedBy: String(payload.operatorId || '').trim(),
      verifyNote: String(payload.note || '现场核销').trim(),
    }))
  }
}

module.exports = { PointService }
]x  const { createId } = require('../utils/ids')
const { env } = require('../config/env')
const { createSessionToken } = require('../utils/sessionToken')

let wechatTokenCache = {
  token: '',
  expiresAt: 0,
}

function readPagination(filters = {}) {
  const pageNum = Math.max(1, Number(filters.pageNum || filters.page || 1))
  const pageSize = Math.min(100, Math.max(1, Number(filters.pageSize || filters.limit || 10)))
  return { pageNum, pageSize }
}

function paginate(list = [], filters = {}) {
  const { pageNum, pageSize } = readPagination(filters)
  const start = (pageNum - 1) * pageSize
  return {
    list: list.slice(start, start + pageSize),
    total: list.length,
    pageNum,
    pageSize,
  }
}

class ReferralService {
  constructor(repository, pointService, smsService) {
    this.repository = repository
    this.pointService = pointService
    this.smsService = smsService
  }

  async getCenter(inviterId, filters = {}) {
    if (!inviterId) {
      const error = new Error('缺少推荐人')
      error.status = 400
      throw error
    }
    const records = await this.repository.getReferralsByInviter(inviterId)
    const keyword = String(filters.keyword || '').trim()
    const status = String(filters.status || '').trim()
    const filteredRecords = records.filter((item) => {
      const matchKeyword = !keyword || [item.name, item.phone, item.childGrade, item.channel, item.status].some((value) => String(value || '').includes(keyword))
      const matchStatus = !status || status === 'all' || item.status === status
      return matchKeyword && matchStatus
    })
    return {
      stats: this.getStats(records),
      rules: (await this.repository.getReferralRules()).map(({ title, inviter, invitee }) => ({ title, inviter, invitee })),
      records: String(filters.paged || '') === '1' ? paginate(filteredRecords, filters) : filteredRecords,
      share: await this.createShare('默认', inviterId),
    }
  }

  async createShare(channel, inviterId) {
    if (!inviterId) {
      const error = new Error('缺少推荐人')
      error.status = 400
      throw error
    }
    const inviter = await this.repository.findUserById(inviterId)
    if (!inviter) {
      const error = new Error('推荐人不存在')
      error.status = 404
      throw error
    }
    return {
      title: `${inviter.name}邀请你领取立方和教育试听积分`,
      channel,
      path: `/pages/login/index?inviteCode=${inviter.inviterCode}`,
      posterText: '扫码领取试听积分',
    }
  }

  async createMiniProgramCode(inviterId) {
    if (!inviterId) {
      const error = new Error('缺少推荐人')
      error.status = 400
      throw error
    }
    const inviter = await this.repository.findUserById(inviterId)
    if (!inviter) {
      const error = new Error('推荐人不存在')
      error.status = 404
      throw error
    }
    if (!env.wechat.appId || !env.wechat.appSecret) {
      const error = new Error('微信小程序码尚未配置 AppID 和 AppSecret')
      error.status = 501
      throw error
    }

    const accessToken = await this.getWechatAccessToken()
    const response = await fetch(`https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=${accessToken}`, {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({
        scene: `invite=${inviter.inviterCode}`,
        page: 'pages/login/index',
        check_path: false,
        env_version: env.wechat.envVersion || 'release',
        width: 430,
      }),
    })

    const contentType = response.headers.get('content-type') || ''
    if (contentType.includes('application/json')) {
      const data = await response.json()
      const error = new Error(data.errmsg || '微信小程序码生成失败')
      error.status = 400
      throw error
    }
    if (!response.ok) {
      const error = new Error('微信小程序码生成失败')
      error.status = 400
      throw error
    }

    const buffer = Buffer.from(await response.arrayBuffer())
    return {
      inviterCode: inviter.inviterCode,
      path: `/pages/login/index?inviteCode=${inviter.inviterCode}`,
      image: `data:image/png;base64,${buffer.toString('base64')}`,
    }
  }

  async getWechatAccessToken() {
    if (wechatTokenCache.token && Date.now() < wechatTokenCache.expiresAt) return wechatTokenCache.token
    const params = new URLSearchParams({
      grant_type: 'client_credential',
      appid: env.wechat.appId,
      secret: env.wechat.appSecret,
    })
    const response = await fetch(`https://api.weixin.qq.com/cgi-bin/token?${params.toString()}`)
    const data = await response.json()
    if (!response.ok || data.errcode || !data.access_token) {
      const error = new Error(data.errmsg || '微信 access_token 获取失败')
      error.status = 400
      throw error
    }
    wechatTokenCache = {
      token: data.access_token,
      expiresAt: Date.now() + Math.max(60, Number(data.expires_in || 7200) - 300) * 1000,
    }
    return wechatTokenCache.token
  }

  async bindReferral(payload) {
    const inviterCode = String(payload.inviterCode || '').trim().toUpperCase()
    const parentName = String(payload.parentName || '').trim()
    const phone = String(payload.phone || '').trim()
    const code = String(payload.code || '').trim()
    const childName = String(payload.childName || '').trim()
    const grade = String(payload.grade || '').trim()
    if (!parentName || !/^1\d{10}$/.test(phone)) {
      const error = new Error('家长姓名和正确手机号必填')
      error.status = 400
      throw error
    }
    if (!this.smsService) {
      const error = new Error('短信服务未启用')
      error.status = 501
      throw error
    }
    this.smsService.verifyCode(phone, code, 'referral_register')

    const inviter = await this.repository.findUserByInviteCode(inviterCode)
    if (!inviter) {
      const error = new Error('推荐码无效')
      error.status = 400
      throw error
    }

    const existed = await this.repository.findUserByLoginAccount(phone)
    if (existed) {
      const error = new Error('该手机号已注册，不能重复领取新客奖励')
      error.status = 409
      throw error
    }

    const invitee = await this.repository.addUser({
      id: createId('parent'),
      username: phone,
      passwordHash: null,
      role: 'parent',
      roleName: '家长',
      name: parentName,
      phone,
      status: 'active',
      inviterCode: `LFH-${Math.random().toString(36).slice(2, 7).toUpperCase()}`,
      child: { id: createId('child'), name: childName || '未填写', grade: grade || '待确认', focus: '待测评' },
    })

    const rule = await this.pointService.getReferralRule('registered') || { inviter: 20, invitee: 20 }
    const referral = await this.repository.addReferral({
      id: createId('rel'),
      inviterId: inviter.id,
      inviteeId: invitee.id,
      name: parentName,
      phone,
      childGrade: grade || '待确认',
      status: '已注册',
      reward: rule.inviter,
      channel: 'H5/小程序',
    })

    await this.repository.addLead({
      id: createId('lead'),
      referralId: referral.id,
      inviteeId: invitee.id,
      parent: parentName,
      source: inviter.name,
      stage: '已注册',
      advisor: '未分配',
      risk: '正常',
      channel: referral.channel,
    })

    if (rule.inviter > 0) await this.pointService.grant(inviter.id, `${parentName}完成注册`, rule.inviter)
    if (rule.invitee > 0) await this.pointService.grant(invitee.id, '新家长注册奖励', rule.invitee)

    const safeInvitee = await this.repository.withPermissions(invitee)
    return {
      referral,
      invitee: {
        ...safeInvitee,
        sessionToken: createSessionToken(safeInvitee.id),
      },
    }
  }

  getStats(records) {
    const stageRank = {
      待注册: 0,
      已注册: 1,
      已预约: 2,
      已到课: 3,
      已报名: 4,
    }
    const reached = (status) => records.filter((item) => (stageRank[item.status] || 0) >= stageRank[status]).length
    const purchased = reached('已报名')
    return {
      invited: records.length,
      registered: reached('已注册'),
      booked: reached('已预约'),
      attended: reached('已到课'),
      purchased,
      conversion: records.length ? Math.round((purchased / records.length) * 100) : 0,
    }
  }
}

module.exports = { ReferralService }
Rlxconst crypto = require('crypto')
const { env } = require('../config/env')

const VALID_PURPOSES = ['login', 'bind_wechat', 'update_phone', 'referral_register']

function nowSeconds() {
  return Math.floor(Date.now() / 1000)
}

function toAliyunTimestamp(date = new Date()) {
  return date.toISOString().replace(/\.\d{3}Z$/, 'Z')
}

function percentEncode(value) {
  return encodeURIComponent(String(value))
    .replace(/\+/g, '%20')
    .replace(/\*/g, '%2A')
    .replace(/%7E/g, '~')
}

function hasRealConfig(value) {
  const text = String(value || '').trim()
  return Boolean(text && !text.startsWith('your_') && text !== 'SMS_000000000')
}

class SmsService {
  constructor(options = env.sms) {
    this.options = options
    this.codes = new Map()
  }

  normalizePhone(phone) {
    return String(phone || '').trim()
  }

  assertPhone(phone) {
    if (!/^1\d{10}$/.test(phone)) {
      const error = new Error('手机号格式不正确')
      error.status = 400
      throw error
    }
  }

  normalizePurpose(purpose) {
    const value = String(purpose || 'login').trim()
    if (!VALID_PURPOSES.includes(value)) {
      const error = new Error('验证码用途无效')
      error.status = 400
      throw error
    }
    return value
  }

  cacheKey(phone, purpose) {
    return `${purpose}:${phone}`
  }

  createCode() {
    const length = Math.max(4, Math.min(8, Number(this.options.codeLength || 6)))
    const min = 10 ** (length - 1)
    const max = (10 ** length) - 1
    return String(crypto.randomInt(min, max + 1))
  }

  async sendVerificationCode(payload = {}) {
    const phone = this.normalizePhone(payload.phone)
    const purpose = this.normalizePurpose(payload.purpose)
    this.assertPhone(phone)

    const key = this.cacheKey(phone, purpose)
    const existed = this.codes.get(key)
    const current = nowSeconds()
    const cooldown = Math.max(0, Number(this.options.codeCooldownSeconds || 60))
    if (existed && existed.sentAt + cooldown > current) {
      const error = new Error(`验证码发送过于频繁，请 ${existed.sentAt + cooldown - current} 秒后再试`)
      error.status = 429
      throw error
    }

    const code = this.createCode()
    if (this.shouldUseAliyun()) {
      await this.sendAliyunSms(phone, code)
      this.codes.set(key, {
        code,
        sentAt: current,
        expiresAt: current + Math.max(60, Number(this.options.codeTtlSeconds || 300)),
      })
      return {
        phone,
        purpose,
        expiresIn: Number(this.options.codeTtlSeconds || 300),
        cooldown,
        message: '验证码已发送',
      }
    }

    if (this.options.allowTestCode) {
      return {
        phone,
        purpose,
        expiresIn: Number(this.options.codeTtlSeconds || 300),
        cooldown,
        message: '短信未配置，测试验证码可用',
      }
    }

    const error = new Error('短信服务未配置')
    error.status = 501
    throw error
  }

  verifyCode(phoneValue, codeValue, purposeValue = 'login') {
    const phone = this.normalizePhone(phoneValue)
    const code = String(codeValue || '').trim()
    const purpose = this.normalizePurpose(purposeValue)
    this.assertPhone(phone)

    if (this.options.allowTestCode && code === String(this.options.testCode || '111111')) return true

    const key = this.cacheKey(phone, purpose)
    const record = this.codes.get(key)
    if (!record || record.expiresAt < nowSeconds() || record.code !== code) {
      const error = new Error('验证码错误')
      error.status = 401
      throw error
    }
    this.codes.delete(key)
    return true
  }

  shouldUseAliyun() {
    const aliyun = this.options.aliyun || {}
    return String(this.options.provider || 'aliyun') === 'aliyun'
      && hasRealConfig(aliyun.accessKeyId)
      && hasRealConfig(aliyun.accessKeySecret)
      && hasRealConfig(aliyun.signName)
      && hasRealConfig(aliyun.templateCode)
  }

  async sendAliyunSms(phone, code) {
    const aliyun = this.options.aliyun || {}
    const params = {
      AccessKeyId: aliyun.accessKeyId,
      Action: 'SendSms',
      Format: 'JSON',
      PhoneNumbers: phone,
      RegionId: 'cn-hangzhou',
      SignName: aliyun.signName,
      SignatureMethod: 'HMAC-SHA1',
      SignatureNonce: crypto.randomUUID(),
      SignatureVersion: '1.0',
      TemplateCode: aliyun.templateCode,
      TemplateParam: JSON.stringify({ [aliyun.templateParamKey || 'code']: code }),
      Timestamp: toAliyunTimestamp(),
      Version: '2017-05-25',
    }
    const canonicalQuery = Object.keys(params)
      .sort()
      .map((key) => `${percentEncode(key)}=${percentEncode(params[key])}`)
      .join('&')
    const stringToSign = `GET&%2F&${percentEncode(canonicalQuery)}`
    const signature = crypto
      .createHmac('sha1', `${aliyun.accessKeySecret}&`)
      .update(stringToSign)
      .digest('base64')
    const url = `https://${aliyun.endpoint || 'dysmsapi.aliyuncs.com'}/?Signature=${percentEncode(signature)}&${canonicalQuery}`
    const response = await fetch(url, { method: 'GET' })
    const data = await response.json().catch(() => ({}))
    if (!response.ok || data.Code !== 'OK') {
      const error = new Error(data.Message || '短信发送失败')
      error.status = 502
      error.providerCode = data.Code
      throw error
    }
    return data
  }
}

module.exports = { SmsService }
㣤xt 100644 ids.js 1"s(`gR 100644 password.js ae'
D0100644 sessionToken.js Ơwm&&lPkIuY,f	x ifunction createId(prefix) {
  return `${prefix}_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 7)}`
}

module.exports = { createId }
g3xOconst crypto = require('crypto')

function hashPassword(password) {
  return crypto.createHash('sha256').update(String(password || '')).digest('hex')
}

function verifyPassword(password, passwordHash) {
  return Boolean(passwordHash) && hashPassword(password) === passwordHash
}

module.exports = {
  hashPassword,
  verifyPassword,
}
GuۺNxconst crypto = require('crypto')
const { env } = require('../config/env')

function sign(value) {
  return crypto
    .createHmac('sha256', env.auth.sessionSecret)
    .update(value)
    .digest('base64url')
}

function createSessionToken(accountId) {
  const issuedAt = Math.floor(Date.now() / 1000)
  const payload = Buffer.from(JSON.stringify({
    sub: accountId,
    iat: issuedAt,
    exp: issuedAt + Math.max(1, Number(env.auth.sessionTtlHours || 168)) * 60 * 60,
  })).toString('base64url')
  return `${payload}.${sign(payload)}`
}

function verifySessionToken(token) {
  const [payload, signature, extra] = String(token || '').trim().split('.')
  if (!payload || !signature || extra) return null
  const expected = sign(payload)
  const actualBuffer = Buffer.from(signature)
  const expectedBuffer = Buffer.from(expected)
  if (actualBuffer.length !== expectedBuffer.length || !crypto.timingSafeEqual(actualBuffer, expectedBuffer)) return null

  try {
    const data = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'))
    if (!data.sub || !data.exp || Number(data.exp) <= Math.floor(Date.now() / 1000)) return null
    return data
  } catch (error) {
    return null
  }
}

module.exports = { createSessionToken, verifySessionToken }
u x$ 100644 .gitkeep ⛲CK)wZS]o0x     x=services:
  mysql:
    image: mysql:8.4
    container_name: lifanghe-mysql
    command: --character-set-server=utf8mb4 --collation-server=utf8mb4_0900_ai_ci
    environment:
      MYSQL_ROOT_PASSWORD: root123456
      MYSQL_DATABASE: lifanghe_education
      MYSQL_USER: lifanghe
      MYSQL_PASSWORD: lifanghe123
      TZ: Asia/Shanghai
    ports:
      - "3306:3306"
    volumes:
      - mysql_data:/var/lib/mysql
      - ./backend/sql/init.sql:/docker-entrypoint-initdb.d/01-init.sql:ro
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-ulifanghe", "-plifanghe123"]
      interval: 10s
      timeout: 5s
      retries: 10

  backend:
    image: node:20-alpine
    container_name: lifanghe-backend
    working_dir: /app
    depends_on:
      mysql:
        condition: service_healthy
    environment:
      NODE_ENV: development
      PORT: 3100
      DB_CLIENT: mysql
      MYSQL_HOST: mysql
      MYSQL_PORT: 3306
      MYSQL_USER: lifanghe
      MYSQL_PASSWORD: lifanghe123
      MYSQL_DATABASE: lifanghe_education
      WECHAT_APP_ID: ${WECHAT_APP_ID:-}
      WECHAT_APP_SECRET: ${WECHAT_APP_SECRET:-}
      TZ: Asia/Shanghai
    ports:
      - "3100:3100"
    volumes:
      - ./backend:/app
      - backend_node_modules:/app/node_modules
    command: sh -c "npm install && npm start"

  front-h5:
    image: node:20-alpine
    container_name: lifanghe-front-h5
    working_dir: /app
    depends_on:
      - backend
    environment:
      VITE_API_BASE_URL: http://127.0.0.1:3100/api
      TZ: Asia/Shanghai
    ports:
      - "5177:5177"
    volumes:
      - ./front:/app
      - front_node_modules:/app/node_modules
    command: sh -c "npm install && npm run dev:h5 -- --host 0.0.0.0 --port 5177"

  pc-admin:
    image: node:20-alpine
    container_name: lifanghe-pc-admin
    working_dir: /app
    depends_on:
      - backend
    environment:
      VITE_APP_BASE_API: http://127.0.0.1:3100/api
      TZ: Asia/Shanghai
    ports:
      - "5174:5174"
    volumes:
      - ./pc-admin:/app
      - pc_admin_node_modules:/app/node_modules
    command: sh -c "npm install && npx vite --host 0.0.0.0 --port 5174"

volumes:
  mysql_data:
  backend_node_modules:
  front_node_modules:
  pc_admin_node_modules:
HO[x100644 .env.development W,>Y(cmUf100644 index.html K/ngx100644 package-lock.json +Mb~q!aTB[}4~100644 package.json AhLЫQs100644 shims-uni.d.ts J:|eeoX=)40000 src >6L ];,h{`100644 vite.config.js o{,=+aNe<	Lʡqxb VITE_API_BASE_URL=http://localhost:3100/api
# VITE_API_BASE_URL=https://lfhedu.ruixininfo.com/api
E f*x[<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <script>
      var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
        CSS.supports('top: constant(a)'))
      document.write(
        '<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
        (coverSupport ? ', viewport-fit=cover' : '') + '" />')
    </script>
    <title></title>
    <!--preload-links-->
    <!--app-context-->
  </head>
  <body>
    <div id="app"><!--app-html--></div>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>
0dbxN{
  "name": "uni-preset-vue",
  "version": "0.0.0",
  "scripts": {
    "dev:custom": "uni -p",
    "dev:h5": "uni",
    "dev:h5:ssr": "uni --ssr",
    "dev:mp-alipay": "uni -p mp-alipay",
    "dev:mp-baidu": "uni -p mp-baidu",
    "dev:mp-jd": "uni -p mp-jd",
    "dev:mp-kuaishou": "uni -p mp-kuaishou",
    "dev:mp-lark": "uni -p mp-lark",
    "dev:mp-qq": "uni -p mp-qq",
    "dev:mp-toutiao": "uni -p mp-toutiao",
    "dev:mp-harmony": "uni -p mp-harmony",
    "dev:mp-weixin": "uni -p mp-weixin",
    "dev:mp-xhs": "uni -p mp-xhs",
    "dev:quickapp-webview": "uni -p quickapp-webview",
    "dev:quickapp-webview-huawei": "uni -p quickapp-webview-huawei",
    "dev:quickapp-webview-union": "uni -p quickapp-webview-union",
    "build:custom": "uni build -p",
    "build:h5": "uni build",
    "build:h5:ssr": "uni build --ssr",
    "build:mp-alipay": "uni build -p mp-alipay",
    "build:mp-baidu": "uni build -p mp-baidu",
    "build:mp-jd": "uni build -p mp-jd",
    "build:mp-kuaishou": "uni build -p mp-kuaishou",
    "build:mp-lark": "uni build -p mp-lark",
    "build:mp-qq": "uni build -p mp-qq",
    "build:mp-toutiao": "uni build -p mp-toutiao",
    "build:mp-harmony": "uni build -p mp-harmony",
    "build:mp-weixin": "uni build -p mp-weixin",
    "build:mp-xhs": "uni build -p mp-xhs",
    "build:quickapp-webview": "uni build -p quickapp-webview",
    "build:quickapp-webview-huawei": "uni build -p quickapp-webview-huawei",
    "build:quickapp-webview-union": "uni build -p quickapp-webview-union"
  },
  "dependencies": {
    "@dcloudio/uni-app": "3.0.0-5000720260410001",
    "@dcloudio/uni-app-harmony": "3.0.0-5000720260410001",
    "@dcloudio/uni-app-plus": "3.0.0-5000720260410001",
    "@dcloudio/uni-components": "3.0.0-5000720260410001",
    "@dcloudio/uni-h5": "3.0.0-5000720260410001",
    "@dcloudio/uni-mp-alipay": "3.0.0-5000720260410001",
    "@dcloudio/uni-mp-baidu": "3.0.0-5000720260410001",
    "@dcloudio/uni-mp-harmony": "3.0.0-5000720260410001",
    "@dcloudio/uni-mp-jd": "3.0.0-5000720260410001",
    "@dcloudio/uni-mp-kuaishou": "3.0.0-5000720260410001",
    "@dcloudio/uni-mp-lark": "3.0.0-5000720260410001",
    "@dcloudio/uni-mp-qq": "3.0.0-5000720260410001",
    "@dcloudio/uni-mp-toutiao": "3.0.0-5000720260410001",
    "@dcloudio/uni-mp-weixin": "3.0.0-5000720260410001",
    "@dcloudio/uni-mp-xhs": "3.0.0-5000720260410001",
    "@dcloudio/uni-quickapp-webview": "3.0.0-5000720260410001",
    "uview-plus": "^3.4.41",
    "vue": "^3.4.21",
    "vue-i18n": "^9.1.9"
  },
  "devDependencies": {
    "@dcloudio/types": "^3.4.8",
    "@dcloudio/uni-automator": "3.0.0-5000720260410001",
    "@dcloudio/uni-cli-shared": "3.0.0-5000720260410001",
    "@dcloudio/uni-stacktracey": "3.0.0-5000720260410001",
    "@dcloudio/vite-plugin-uni": "3.0.0-5000720260410001",
    "@vue/runtime-core": "^3.4.21",
    "sass": "^1.89.2",
    "vite": "5.2.8"
  }
}
4x 3/// <reference types='@dcloudio/types' />
import 'vue'

declare module '@vue/runtime-core' {
  type Hooks = App.AppInstance & Page.PageInstance;

  interface ComponentCustomOptions extends Hooks {

  }
}
PEWx1100644 App.vue ,4{$bEٛm40000 components IN0IjтF40000 config V̽̑S\P5JW_100644 main.js ^PD()%bcgm100644 manifest.json sxƖM$ʌW-Q9100644 pages.json *f[p.}40000 pages ~<K'ـpOh40000 services ObyuR^^!NG100644 shime-uni.d.ts ̜RT~$cEH40000 static =؟A(, 
FV?40000 styles )lZB\YQ100644 uni.scss _@)S-NFJ40000 utils b>>= =WWĦ!x<script>
export default {
  onLaunch() {
    console.log('LiFangHe Education app launched')
  },
}
</script>

<style lang="scss">
page {
  min-height: 100%;
  color: #17312d;
  background: #f3f6ed;
  font-family: "Inter", "PingFang SC", "Microsoft YaHei", Arial, sans-serif;
}

view,
text,
button,
input,
textarea {
  box-sizing: border-box;
}

button {
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 0;
  padding: 0;
  line-height: normal;
  background: transparent;
}

button::after {
  border: 0;
}
</style>
̻x+ 100644 AdminTabbar.vue c~:_O>mNW^+*]x	<template>
  <view class="admin-tabbar-shell">
    <view class="admin-tabbar-spacer"></view>
    <view class="admin-tabbar">
      <view
        v-for="item in tabs"
        :key="item.key"
        :class="['admin-tabbar__item', active === item.key && 'admin-tabbar__item--active']"
        @click="go(item)"
      >
        <image class="admin-tabbar__icon" :src="iconSrc(item)" mode="aspectFit"></image>
        <text>{{ item.label }}</text>
      </view>
    </view>
  </view>
</template>

<script>
export default {
  props: {
    active: {
      type: String,
      default: 'home',
    },
  },
  data() {
    return {
      tabs: [
        { key: 'home', label: '首页', icon: 'home', url: '/pages/admin/index' },
        { key: 'leads', label: '线索', icon: 'referral', url: '/pages/admin-leads/index' },
        { key: 'courses', label: '试听', icon: 'courses', url: '/pages/admin-courses/index' },
        { key: 'points', label: '积分', icon: 'points', url: '/pages/admin-points/index' },
        { key: 'profile', label: '我的', icon: 'account', url: '/pages/admin-profile/index' },
      ],
    }
  },
  methods: {
    iconSrc(item) {
      const suffix = this.active === item.key ? '-active' : ''
      return `/static/tabbar/${item.icon}${suffix}.png`
    },
    go(item) {
      if (this.active === item.key) return
      uni.redirectTo({ url: item.url })
    },
  },
}
</script>

<style lang="scss" scoped>
.admin-tabbar-spacer {
  height: calc(146rpx + env(safe-area-inset-bottom));
}

.admin-tabbar {
  position: fixed;
  right: 24rpx;
  bottom: calc(20rpx + env(safe-area-inset-bottom));
  left: 24rpx;
  z-index: 50;
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 8rpx;
  padding: 14rpx;
  border: 1rpx solid rgba(11, 79, 70, 0.1);
  border-radius: 18rpx;
  background: rgba(251, 252, 247, 0.96);
  box-shadow: 0 18rpx 46rpx rgba(11, 47, 42, 0.16);
}

.admin-tabbar__item {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: 86rpx;
  border-radius: 14rpx;
  color: #6f7c78;
  font-size: 20rpx;
  font-weight: 900;
}

.admin-tabbar__icon {
  width: 42rpx;
  height: 42rpx;
}

.admin-tabbar__item--active {
  color: #0b4f46;
  background: #dff8ee;
}

.admin-tabbar__item text {
  margin-top: 6rpx;
}
</style>
#]	x$ 100644 index.js  {f(1oo<OHx export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'https://lfhedu.ruixininfo.com/api'

export const POINT_STATUS = {
  available: '可用',
  pending: '待发放',
  frozen: '冻结',
  revoked: '已撤销'
}
R͵zxZimport {
	createSSRApp
} from "vue";
import App from "./App.vue";
import uviewPlus from "uview-plus";
import "uview-plus/index.scss";
import "./styles/theme.scss";
import UButton from "uview-plus/components/u-button/u-button.vue";
import UDatetimePicker from "uview-plus/components/u-datetime-picker/u-datetime-picker.vue";
import UGap from "uview-plus/components/u-gap/u-gap.vue";
import UIcon from "uview-plus/components/u-icon/u-icon.vue";
import UInput from "uview-plus/components/u-input/u-input.vue";
import ULoadingIcon from "uview-plus/components/u-loading-icon/u-loading-icon.vue";
import UOverlay from "uview-plus/components/u-overlay/u-overlay.vue";
import UPicker from "uview-plus/components/u-picker/u-picker.vue";
import UPopup from "uview-plus/components/u-popup/u-popup.vue";
import USafeBottom from "uview-plus/components/u-safe-bottom/u-safe-bottom.vue";
import UStatusBar from "uview-plus/components/u-status-bar/u-status-bar.vue";
import UTag from "uview-plus/components/u-tag/u-tag.vue";
import UToast from "uview-plus/components/u-toast/u-toast.vue";
import UToolbar from "uview-plus/components/u-toolbar/u-toolbar.vue";
import UTransition from "uview-plus/components/u-transition/u-transition.vue";

export function createApp() {
	const app = createSSRApp(App);
	app.use(uviewPlus);
	app.component("u-button", UButton);
	app.component("up-datetime-picker", UDatetimePicker);
	app.component("u-gap", UGap);
	app.component("u-icon", UIcon);
	app.component("up-icon", UIcon);
	app.component("up-input", UInput);
	app.component("u-loading-icon", ULoadingIcon);
	app.component("u-overlay", UOverlay);
	app.component("u-picker", UPicker);
	app.component("u-popup", UPopup);
	app.component("u-safe-bottom", USafeBottom);
	app.component("u-status-bar", UStatusBar);
	app.component("u-tag", UTag);
	app.component("u-toast", UToast);
	app.component("u-toolbar", UToolbar);
	app.component("u-transition", UTransition);
	return {
		app,
	};
}
wĿx{
  "name": "",
  "appid": "",
  "description": "",
  "versionName": "1.0.0",
  "versionCode": "100",
  "transformPx": false,
  /* 5+App特有相关 */
  "app-plus": {
    "usingComponents": true,
    "nvueStyleCompiler": "uni-app",
    "compilerVersion": 3,
    "splashscreen": {
      "alwaysShowBeforeRender": true,
      "waiting": true,
      "autoclose": true,
      "delay": 0
    },
    /* 模块配置 */
    "modules": {},
    /* 应用发布信息 */
    "distribute": {
      /* android打包配置 */
      "android": {
        "permissions": ["<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>", "<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>", "<uses-permission android:name=\"android.permission.VIBRATE\"/>", "<uses-permission android:name=\"android.permission.READ_LOGS\"/>", "<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>", "<uses-feature android:name=\"android.hardware.camera.autofocus\"/>", "<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>", "<uses-permission android:name=\"android.permission.CAMERA\"/>", "<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>", "<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>", "<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>", "<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>", "<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>", "<uses-feature android:name=\"android.hardware.camera\"/>", "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"]
      },
      /* ios打包配置 */
      "ios": {},
      /* SDK配置 */
      "sdkConfigs": {}
    }
  },
  /* 快应用特有相关 */
  "quickapp": {},
  /* 小程序特有相关 */
  "mp-weixin": {
    "appid": "wx48e101cff8e91db9",
    "setting": {
      "urlCheck": false
    },
    "usingComponents": true
  },
  "mp-alipay": {
    "usingComponents": true
  },
  "mp-baidu": {
    "usingComponents": true
  },
  "mp-toutiao": {
    "usingComponents": true
  },
  "uniStatistics": {
    "enable": false
  },
  "vueVersion": "3"
}
4ϻx{
  "easycom": {
    "^u-(.*)": "uview-plus/components/u-$1/u-$1.vue"
  },
  "pages": [
    {
      "path": "pages/index/index",
      "style": {
        "navigationBarTitleText": "立方和教育",
        "navigationStyle": "custom",
        "enablePullDownRefresh": true
      }
    },
    {
      "path": "pages/referral/index",
      "style": {
        "navigationBarTitleText": "推荐有礼",
        "navigationStyle": "custom",
        "enablePullDownRefresh": true
      }
    },
    {
      "path": "pages/points/index",
      "style": {
        "navigationBarTitleText": "积分中心",
        "navigationStyle": "custom",
        "enablePullDownRefresh": true
      }
    },
    {
      "path": "pages/courses/index",
      "style": {
        "navigationBarTitleText": "课程预约",
        "navigationStyle": "custom",
        "enablePullDownRefresh": true
      }
    },
    {
      "path": "pages/account/index",
      "style": {
        "navigationBarTitleText": "我的账号",
        "navigationStyle": "custom",
        "enablePullDownRefresh": true
      }
    },
    {
      "path": "pages/admin/index",
      "style": {
        "navigationBarTitleText": "增长管理台",
        "navigationStyle": "custom",
        "enablePullDownRefresh": true
      }
    },
    {
      "path": "pages/admin-leads/index",
      "style": {
        "navigationBarTitleText": "线索管理",
        "navigationStyle": "custom",
        "enablePullDownRefresh": true
      }
    },
    {
      "path": "pages/admin-points/index",
      "style": {
        "navigationBarTitleText": "积分管理",
        "navigationStyle": "custom",
        "enablePullDownRefresh": true
      }
    },
    {
      "path": "pages/admin-point-records/index",
      "style": {
        "navigationBarTitleText": "积分流水",
        "navigationStyle": "custom",
        "enablePullDownRefresh": true
      }
    },
    {
      "path": "pages/admin-point-accounts/index",
      "style": {
        "navigationBarTitleText": "积分账户",
        "navigationStyle": "custom",
        "enablePullDownRefresh": true
      }
    },
    {
      "path": "pages/admin-courses/index",
      "style": {
        "navigationBarTitleText": "试听管理",
        "navigationStyle": "custom",
        "enablePullDownRefresh": true
      }
    },
    {
      "path": "pages/admin-course-edit/index",
      "style": {
        "navigationBarTitleText": "维护试听课",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/admin-appointments/index",
      "style": {
        "navigationBarTitleText": "预约到课",
        "navigationStyle": "custom",
        "enablePullDownRefresh": true
      }
    },
    {
      "path": "pages/admin-profile/index",
      "style": {
        "navigationBarTitleText": "管理账号",
        "navigationStyle": "custom",
        "enablePullDownRefresh": true
      }
    },
    {
      "path": "pages/login/index",
      "style": {
        "navigationBarTitleText": "账号登录",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/campaign/index",
      "style": {
        "navigationBarTitleText": "成长邀请",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/courses-list/index",
      "style": {
        "navigationBarTitleText": "可约课程",
        "navigationStyle": "custom",
        "enablePullDownRefresh": true
      }
    },
    {
      "path": "pages/referral-records/index",
      "style": {
        "navigationBarTitleText": "推荐记录",
        "navigationStyle": "custom",
        "enablePullDownRefresh": true
      }
    },
    {
      "path": "pages/point-rewards/index",
      "style": {
        "navigationBarTitleText": "兑换权益",
        "navigationStyle": "custom",
        "enablePullDownRefresh": true
      }
    },
    {
      "path": "pages/point-records/index",
      "style": {
        "navigationBarTitleText": "积分记录",
        "navigationStyle": "custom",
        "enablePullDownRefresh": true
      }
    }
  ],
  "tabBar": {
    "color": "#6f7c78",
    "selectedColor": "#0b4f46",
    "backgroundColor": "#fbfcf7",
    "borderStyle": "white",
    "list": [
      {
        "pagePath": "pages/index/index",
        "text": "首页",
        "iconPath": "static/tabbar/home.png",
        "selectedIconPath": "static/tabbar/home-active.png"
      },
      {
        "pagePath": "pages/referral/index",
        "text": "推荐",
        "iconPath": "static/tabbar/referral.png",
        "selectedIconPath": "static/tabbar/referral-active.png"
      },
      {
        "pagePath": "pages/points/index",
        "text": "积分",
        "iconPath": "static/tabbar/points.png",
        "selectedIconPath": "static/tabbar/points-active.png"
      },
      {
        "pagePath": "pages/courses/index",
        "text": "课程",
        "iconPath": "static/tabbar/courses.png",
        "selectedIconPath": "static/tabbar/courses-active.png"
      },
      {
        "pagePath": "pages/account/index",
        "text": "我的",
        "iconPath": "static/tabbar/account.png",
        "selectedIconPath": "static/tabbar/account-active.png"
      }
    ]
  },
  "globalStyle": {
    "navigationBarTextStyle": "white",
    "navigationBarTitleText": "立方和教育",
    "navigationBarBackgroundColor": "#0b2f2a",
    "backgroundColor": "#f3f6ed"
  }
}
oo 0x 40000 account l3"*a5,u40000 admin-appointments H6ىB'Ғ;640000 admin-course-edit Q-)E0
40000 admin-courses ̍ȻBC99Aup40000 admin-leads Oɖ)Λ40000 admin-point-accounts Y!g9Љ40000 admin-point-records hq`Fݜr40000 admin-points (XkëovHOI40000 admin-profile Uٶ|A,D]40000 admin ۻwknL
ܩE^40000 campaign A}G4GT*40000 courses-list D!{h،_40000 courses @b	_ڿvPu\
40000 index ޢ"drK|40000 login U*_s!4FD,U40000 point-records <zSrKT${w40000 point-rewards ٦ً'\cTL40000 points %Q۲t`Iw40000 referral-records r;\ۗէxxk*40000 referral "8³fXNZDD'x% 100644 index.vue X(@OxdB<template>
  <view class="lf-page account-page">
    <view class="lf-nav">
      <view>
        <view class="lf-nav__title">我的账号</view>
        <view class="lf-nav__sub">维护个人资料和账号安全</view>
      </view>
      <view class="lf-pill">{{ account.roleName || '-' }}</view>
    </view>

    <view class="profile-card lf-card lf-card--ink">
      <view class="profile-card__top">
        <image v-if="account.wechatAvatar" class="profile-card__avatar-img" :src="account.wechatAvatar" mode="aspectFill" />
        <view v-else class="profile-card__avatar">{{ (account.name || '-').slice(0, 1) }}</view>
        <view>
          <view class="profile-card__name">{{ account.name }}</view>
          <view class="profile-card__phone">{{ account.phone || '未绑定手机号' }}</view>
        </view>
      </view>
      <view v-if="isAdmin && (account.permissions || []).length" class="permission-strip">
        <text v-for="permission in account.permissions || []" :key="permission">{{ permission }}</text>
      </view>
    </view>

    <view class="lf-card action-card">
      <view class="lf-row">
        <view class="lf-section-title">个人资料</view>
        <button class="mini-action" @click="toggleEdit">{{ editing ? '收起' : '编辑' }}</button>
      </view>
      <view v-if="!editing" class="profile-list">
        <view class="profile-line">
          <text>姓名</text>
          <text>{{ account.name || '-' }}</text>
        </view>
        <view class="profile-line">
          <text>孩子</text>
          <text>{{ account.child?.name || '-' }}</text>
        </view>
        <view class="profile-line">
          <text>年级</text>
          <text>{{ account.child?.grade || '-' }}</text>
        </view>
        <view class="profile-line">
          <text>关注点</text>
          <text>{{ account.child?.focus || '-' }}</text>
        </view>
        <view v-if="account.role === 'parent'" class="profile-line">
          <text>试听次数</text>
          <text>剩余 {{ account.trialBookingRemaining ?? account.trialBookingLimit ?? 1 }} / {{ account.trialBookingLimit ?? 1 }} 次</text>
        </view>
      </view>
      <view v-else class="edit-form">
        <!-- #ifdef MP-WEIXIN -->
        <button class="avatar-button" open-type="chooseAvatar" @chooseavatar="chooseAvatar">
          <image v-if="form.wechatAvatar" :src="form.wechatAvatar" mode="aspectFill" />
          <text v-else>选头像</text>
        </button>
        <!-- #endif -->
        <view class="form-field">
          <text>姓名</text>
          <input v-model="form.name" maxlength="20" placeholder="请输入姓名" />
        </view>
        <view class="form-field">
          <text>孩子姓名</text>
          <input v-model="form.childName" maxlength="20" placeholder="请输入孩子姓名" />
        </view>
        <view class="form-field">
          <text>孩子年级</text>
          <input v-model="form.childGrade" maxlength="20" placeholder="例如：三年级" />
        </view>
        <view class="form-field">
          <text>学习关注点</text>
          <input v-model="form.childFocus" maxlength="30" placeholder="例如：数学思维 / 阅读表达" />
        </view>
        <button class="save-button" :disabled="saving" @click="saveProfile">{{ saving ? '保存中' : '保存资料' }}</button>
      </view>
    </view>

    <view class="lf-card action-card">
      <view class="lf-section-title">账号安全</view>
      <view class="entry-row" @click="togglePasswordPanel">
        <view>
          <text>{{ account.hasPassword ? '修改密码' : '设置密码' }}</text>
          <text>{{ account.hasPassword ? '用于账号密码登录' : '设置后可使用账号密码登录' }}</text>
        </view>
        <u-icon name="arrow-right" color="#0b4f46" size="22"></u-icon>
      </view>
      <view v-if="passwordPanel" class="security-panel">
        <view v-if="account.hasPassword" class="form-field">
          <text>原密码</text>
          <input v-model="passwordForm.oldPassword" password maxlength="32" placeholder="请输入原密码" />
        </view>
        <view class="form-field">
          <text>新密码</text>
          <input v-model="passwordForm.password" password maxlength="32" placeholder="至少 6 位" />
        </view>
        <view class="form-field">
          <text>确认密码</text>
          <input v-model="passwordForm.confirmPassword" password maxlength="32" placeholder="请再次输入新密码" />
        </view>
        <button class="save-button" :disabled="securitySaving" @click="savePassword">
          {{ securitySaving ? '保存中' : account.hasPassword ? '修改密码' : '设置密码' }}
        </button>
      </view>

      <view class="entry-row" @click="togglePhonePanel">
        <view>
          <text>换绑手机号</text>
          <text>当前：{{ account.phone || '未绑定' }}</text>
        </view>
        <u-icon name="arrow-right" color="#0b4f46" size="22"></u-icon>
      </view>
      <view v-if="phonePanel" class="security-panel">
        <view class="form-field">
          <text>新手机号</text>
          <input v-model="phoneForm.phone" type="number" maxlength="11" placeholder="请输入新手机号" />
        </view>
        <view class="form-field form-field--code">
          <view class="code-input">
            <text>验证码</text>
            <input v-model="phoneForm.code" type="number" maxlength="6" placeholder="请输入验证码" />
          </view>
          <button class="code-button" :disabled="phoneCountdown > 0" @click="requestPhoneCode">
            {{ phoneCountdown > 0 ? `${phoneCountdown}s` : '获取验证码' }}
          </button>
        </view>
        <button class="save-button" :disabled="securitySaving" @click="savePhone">
          {{ securitySaving ? '保存中' : '确认换绑' }}
        </button>
      </view>

      <view class="entry-row entry-row--danger" @click="confirmLogout">
        <view>
          <text>退出登录</text>
          <text>退出后需要重新登录当前账号</text>
        </view>
        <u-icon name="arrow-right" color="#b54732" size="22"></u-icon>
      </view>
    </view>

    <view v-if="isAdmin" class="lf-card action-card">
      <view class="lf-section-title">管理入口</view>
      <view class="entry-row" @click="go('/pages/admin/index')">
        <view>
          <text>移动管理台</text>
          <text>线索推进、积分管理、试听课程管理</text>
        </view>
        <u-icon name="arrow-right" color="#0b4f46" size="22"></u-icon>
      </view>
    </view>

    <u-toast ref="toast"></u-toast>
  </view>
</template>

<script>
import { sendSmsCode, updateBoundPhone, updatePassword, updateProfile, uploadAvatar } from '../../services/educationApi'
import { clearStoredAccount, getStoredAccount, isAdminAccount, refreshStoredAccount, setStoredAccount } from '../../services/accountSession'

function createProfileForm(account = {}) {
  return {
    name: account.name || '',
    childName: account.child?.name || '',
    childGrade: account.child?.grade || '',
    childFocus: account.child?.focus || '',
    wechatAvatar: account.wechatAvatar || '',
  }
}

function createPasswordForm() {
  return {
    oldPassword: '',
    password: '',
    confirmPassword: '',
  }
}

export default {
  data() {
    const account = getStoredAccount() || {}
    return {
      account,
      editing: false,
      saving: false,
      securitySaving: false,
      passwordPanel: false,
      phonePanel: false,
      phoneCountdown: 0,
      phoneTimer: null,
      form: createProfileForm(account),
      passwordForm: createPasswordForm(),
      phoneForm: {
        phone: '',
        code: '',
      },
    }
  },
  computed: {
    isAdmin() {
      return isAdminAccount(this.account)
    },
  },
  async onShow() {
    const account = await refreshStoredAccount()
    if (!account) {
      uni.reLaunch({ url: '/pages/login/index' })
      return
    }
    this.account = account
    this.form = createProfileForm(account)
  },
  async onPullDownRefresh() {
    await this.onShow()
    uni.stopPullDownRefresh()
  },
  onUnload() {
    if (this.phoneTimer) clearInterval(this.phoneTimer)
  },
  methods: {
    toggleEdit() {
      this.editing = !this.editing
      if (this.editing) this.form = createProfileForm(this.account)
    },
    async chooseAvatar(event) {
      const localPath = event.detail.avatarUrl
      if (!localPath) return
      const res = await uploadAvatar(localPath)
      if (res.ok === false) {
        this.$refs.toast.show({ message: res.message || '头像上传失败', type: 'error' })
        return
      }
      this.form.wechatAvatar = res.data?.url || res.url || localPath
      this.$refs.toast.show({ message: '头像已上传', type: 'success' })
    },
    async saveProfile() {
      if (!this.form.name.trim()) {
        this.$refs.toast.show({ message: '请填写姓名', type: 'error' })
        return
      }
      this.saving = true
      try {
        const res = await updateProfile(this.account.id, this.form)
        if (res.ok === false) {
          this.$refs.toast.show({ message: res.message || '保存失败', type: 'error' })
          return
        }
        this.applyAccount(res.data || res)
        this.editing = false
        this.$refs.toast.show({ message: '资料已保存', type: 'success' })
      } finally {
        this.saving = false
      }
    },
    togglePasswordPanel() {
      this.passwordPanel = !this.passwordPanel
      this.passwordForm = createPasswordForm()
      if (this.passwordPanel) this.phonePanel = false
    },
    togglePhonePanel() {
      this.phonePanel = !this.phonePanel
      this.phoneForm = { phone: '', code: '' }
      if (this.phonePanel) this.passwordPanel = false
    },
    async requestPhoneCode() {
      if (!/^1\d{10}$/.test(this.phoneForm.phone)) {
        this.$refs.toast.show({ message: '请先输入正确手机号', type: 'error' })
        return
      }
      const res = await sendSmsCode(this.phoneForm.phone, 'update_phone')
      if (res.ok === false) {
        this.$refs.toast.show({ message: res.message || '验证码发送失败', type: 'error' })
        return
      }
      const data = res.data || res
      this.startPhoneCountdown(data.cooldown || 60)
      this.$refs.toast.show({ message: res.message || data.message || '验证码已发送', type: 'success' })
    },
    startPhoneCountdown(seconds = 60) {
      if (this.phoneTimer) clearInterval(this.phoneTimer)
      this.phoneCountdown = Number(seconds || 60)
      this.phoneTimer = setInterval(() => {
        this.phoneCountdown -= 1
        if (this.phoneCountdown <= 0) {
          clearInterval(this.phoneTimer)
          this.phoneTimer = null
        }
      }, 1000)
    },
    async savePassword() {
      if (this.account.hasPassword && !this.passwordForm.oldPassword) {
        this.$refs.toast.show({ message: '请输入原密码', type: 'error' })
        return
      }
      if (this.passwordForm.password.length < 6) {
        this.$refs.toast.show({ message: '密码至少需要 6 位', type: 'error' })
        return
      }
      if (this.passwordForm.password !== this.passwordForm.confirmPassword) {
        this.$refs.toast.show({ message: '两次输入的密码不一致', type: 'error' })
        return
      }
      this.securitySaving = true
      try {
        const res = await updatePassword(this.account.id, this.passwordForm)
        if (res.ok === false) {
          this.$refs.toast.show({ message: res.message || '密码保存失败', type: 'error' })
          return
        }
        this.applyAccount(res.data || res)
        this.passwordPanel = false
        this.passwordForm = createPasswordForm()
        this.$refs.toast.show({ message: '密码已更新', type: 'success' })
      } finally {
        this.securitySaving = false
      }
    },
    async savePhone() {
      if (!/^1\d{10}$/.test(this.phoneForm.phone)) {
        this.$refs.toast.show({ message: '请输入正确手机号', type: 'error' })
        return
      }
      if (!this.phoneForm.code) {
        this.$refs.toast.show({ message: '请输入验证码', type: 'error' })
        return
      }
      this.securitySaving = true
      try {
        const res = await updateBoundPhone(this.account.id, this.phoneForm)
        if (res.ok === false) {
          this.$refs.toast.show({ message: res.message || '换绑失败', type: 'error' })
          return
        }
        this.applyAccount(res.data || res)
        this.phonePanel = false
        this.phoneForm = { phone: '', code: '' }
        this.$refs.toast.show({ message: '手机号已换绑', type: 'success' })
      } finally {
        this.securitySaving = false
      }
    },
    applyAccount(account) {
      this.account = account
      this.form = createProfileForm(account)
      setStoredAccount(account)
    },
    confirmLogout() {
      uni.showModal({
        title: '确认退出登录',
        content: '退出后需要重新登录当前账号，是否继续？',
        confirmText: '退出',
        confirmColor: '#b54732',
        success: (res) => {
          if (!res.confirm) return
          clearStoredAccount()
          uni.reLaunch({ url: '/pages/login/index' })
        },
      })
    },
    go(url) {
      uni.navigateTo({ url })
    },
  },
}
</script>

<style lang="scss" scoped>
.profile-card {
  margin-top: 18rpx;
  padding: 32rpx;
}

.profile-card__top {
  display: flex;
  align-items: center;
  gap: 22rpx;
}

.profile-card__avatar,
.profile-card__avatar-img {
  width: 92rpx;
  height: 92rpx;
  border-radius: 24rpx;
}

.profile-card__avatar {
  display: grid;
  place-items: center;
  color: #0b2f2a;
  background: #f5c451;
  font-size: 42rpx;
  font-weight: 900;
}

.profile-card__name {
  color: #ffffff;
  font-size: 36rpx;
  font-weight: 900;
}

.profile-card__phone {
  margin-top: 8rpx;
  color: rgba(255, 255, 255, 0.68);
  font-size: 24rpx;
}

.permission-strip {
  display: flex;
  flex-wrap: wrap;
  gap: 12rpx;
  margin-top: 28rpx;
}

.permission-strip text {
  padding: 8rpx 14rpx;
  border-radius: 999rpx;
  color: #dff8ee;
  background: rgba(255, 255, 255, 0.1);
  font-size: 20rpx;
  font-weight: 800;
}

.action-card {
  margin-top: 22rpx;
  padding: 28rpx;
}

.mini-action {
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 96rpx;
  height: 52rpx;
  padding: 0 18rpx;
  border-radius: 999rpx;
  color: #0b4f46;
  background: #dff8ee;
  font-size: 22rpx;
  font-weight: 900;
  line-height: normal;
}

.profile-list {
  margin-top: 18rpx;
}

.profile-line {
  display: flex;
  justify-content: space-between;
  gap: 24rpx;
  padding: 20rpx 0;
  border-bottom: 1rpx solid #edf1e9;
}

.profile-line:last-child {
  border-bottom: 0;
}

.profile-line text:first-child {
  color: #6f7c78;
  font-size: 24rpx;
}

.profile-line text:last-child {
  color: #17312d;
  font-size: 26rpx;
  font-weight: 900;
  text-align: right;
}

.edit-form,
.security-panel {
  margin-top: 20rpx;
  padding: 22rpx;
  border: 1rpx solid #dfe9dc;
  border-radius: 16rpx;
  background: #fbfdf8;
}

.avatar-button {
  display: grid;
  place-items: center;
  width: 112rpx;
  height: 112rpx;
  margin-bottom: 22rpx;
  border-radius: 28rpx;
  color: #0b4f46;
  background: #dff8ee;
  font-size: 22rpx;
  font-weight: 900;
  overflow: hidden;
}

.avatar-button image {
  width: 112rpx;
  height: 112rpx;
}

.form-field {
  margin-top: 18rpx;
}

.form-field:first-child {
  margin-top: 0;
}

.form-field text {
  display: block;
  margin-bottom: 10rpx;
  color: #6f7c78;
  font-size: 22rpx;
  font-weight: 900;
}

.form-field input {
  height: 78rpx;
  padding: 0 22rpx;
  border: 1rpx solid #dfe9dc;
  border-radius: 14rpx;
  color: #17312d;
  background: #f8faf5;
  font-size: 26rpx;
  line-height: 78rpx;
}

.form-field--code {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 190rpx;
  align-items: end;
  gap: 16rpx;
}

.code-button,
.save-button {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 78rpx;
  border-radius: 16rpx;
  font-size: 26rpx;
  font-weight: 900;
  line-height: normal;
}

.code-button {
  color: #0b4f46;
  border: 1rpx solid #b9d8d2;
  background: #dff8ee;
}

.code-button[disabled] {
  color: #7d8b86;
  background: #eef3ec;
}

.save-button {
  margin-top: 24rpx;
  color: #ffffff;
  background: #0b4f46;
}

.entry-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 18rpx;
  padding: 26rpx 0;
  border-bottom: 1rpx solid #edf1e9;
}

.entry-row:last-child {
  border-bottom: 0;
}

.entry-row--danger text:first-child {
  color: #b54732;
}

.entry-row text {
  display: block;
}

.entry-row text:first-child {
  color: #17312d;
  font-size: 28rpx;
  font-weight: 900;
}

.entry-row text:last-child {
  margin-top: 8rpx;
  color: #6f7c78;
  font-size: 22rpx;
  line-height: 1.45;
}
</style>
x% 100644 index.vue D!X6x(<template>
  <view class="lf-page account-list-page">
    <view class="lf-nav">
      <view>
        <view class="lf-nav__title">积分账户</view>
        <view class="lf-nav__sub">搜索家长账户，查看可用、待到账和将过期积分</view>
      </view>
      <view class="lf-pill">{{ total }} 个</view>
    </view>

    <view v-if="!isAdmin" class="lf-card denied-card">
      <view class="denied-card__title">当前账号没有管理权限</view>
      <view class="lf-caption">请切换到管理账号后再查看积分账户。</view>
    </view>

    <template v-else>
      <view class="summary-grid">
        <view v-for="item in metrics" :key="item.label" class="lf-card summary-card">
          <text>{{ item.label }}</text>
          <text>{{ item.value }}</text>
        </view>
      </view>

      <view class="lf-card filter-card">
        <view class="search-box">
          <u-icon name="search" color="#6f7c78" size="22"></u-icon>
          <input v-model="filters.keyword" placeholder="搜索家长、手机号、孩子名" confirm-type="search" @confirm="refresh" />
        </view>
        <view class="status-row">
          <button
            v-for="item in statusOptions"
            :key="item.value"
            :class="['status-chip', filters.pointStatus === item.value && 'status-chip--active']"
            @click="selectStatus(item.value)"
          >
            {{ item.label }}
          </button>
        </view>
        <view class="query-actions">
          <button class="query-actions__ghost" @click="reset">重置</button>
          <button class="query-actions__primary" @click="refresh">查询</button>
        </view>
      </view>

      <view v-for="item in list" :key="item.user.id" class="lf-card account-card">
        <view class="account-card__head">
          <view class="account-card__main">
            <view class="account-card__name">{{ item.user.name }}</view>
            <view class="account-card__meta">{{ item.user.phone || '-' }} · {{ item.user.child?.name || '未填写孩子' }}</view>
          </view>
          <view class="account-card__score">
            <text>{{ item.points.available }}</text>
            <text>可用</text>
          </view>
        </view>
        <view class="points-grid">
          <view>
            <text>{{ item.points.pending }}</text>
            <text>待到账</text>
          </view>
          <view>
            <text>{{ item.points.expiring }}</text>
            <text>将过期</text>
          </view>
          <view>
            <text>{{ item.points.frozen }}</text>
            <text>冻结</text>
          </view>
        </view>
        <view class="account-actions">
          <button @click="adjust(item, 100, '后台补发积分')">补发 100</button>
          <button class="is-warn" @click="adjust(item, -50, '后台冲正积分')">冲正 50</button>
        </view>
      </view>

      <view class="load-state">{{ loadText }}</view>
    </template>

    <AdminTabbar v-if="isAdmin" active="points" />
    <u-toast ref="toast"></u-toast>
  </view>
</template>

<script>
import AdminTabbar from '../../components/AdminTabbar.vue'
import { adjustPoints, getAdminPointAccounts } from '../../services/educationApi'
import { getStoredAccount, isAdminAccount } from '../../services/accountSession'

export default {
  components: { AdminTabbar },
  data() {
    return {
      account: getStoredAccount() || {},
      list: [],
      total: 0,
      overview: { parents: 0, available: 0, pending: 0, expiring: 0, frozen: 0 },
      loading: false,
      finished: false,
      filters: {
        keyword: '',
        pointStatus: 'all',
        pageNum: 1,
        pageSize: 10,
      },
      statusOptions: [
        { label: '全部', value: 'all' },
        { label: '有可用', value: 'available' },
        { label: '待到账', value: 'pending' },
        { label: '将过期', value: 'expiring' },
        { label: '冻结', value: 'frozen' },
      ],
    }
  },
  computed: {
    isAdmin() {
      return isAdminAccount(this.account)
    },
    metrics() {
      return [
        { label: '账户数', value: this.overview.parents || 0 },
        { label: '可用', value: this.overview.available || 0 },
        { label: '待到账', value: this.overview.pending || 0 },
        { label: '将过期', value: this.overview.expiring || 0 },
      ]
    },
    loadText() {
      if (this.loading) return '加载中...'
      if (this.finished) return this.list.length ? '没有更多了' : '暂无积分账户'
      return '上拉加载更多'
    },
  },
  onShow() {
    const account = getStoredAccount()
    if (!account) {
      uni.reLaunch({ url: '/pages/login/index' })
      return
    }
    this.account = account
    if (this.isAdmin && !this.list.length) this.refresh()
  },
  async onPullDownRefresh() {
    if (this.isAdmin) await this.refresh()
    uni.stopPullDownRefresh()
  },
  onReachBottom() {
    this.loadMore()
  },
  methods: {
    selectStatus(status) {
      this.filters.pointStatus = status
      this.refresh()
    },
    reset() {
      this.filters.keyword = ''
      this.filters.pointStatus = 'all'
      this.refresh()
    },
    async refresh() {
      this.filters.pageNum = 1
      this.finished = false
      await this.load(true)
    },
    async loadMore() {
      if (this.loading || this.finished || !this.isAdmin) return
      this.filters.pageNum += 1
      await this.load(false)
    },
    async load(reset) {
      this.loading = true
      try {
        const res = await getAdminPointAccounts(this.filters)
        if (res.ok === false) {
          this.$refs.toast.show({ message: res.message || '积分账户加载失败', type: 'error' })
          return
        }
        const data = res.data || res || {}
        const rows = data.list || []
        this.total = data.total || 0
        this.overview = data.overview || this.overview
        this.list = reset ? rows : this.list.concat(rows)
        this.finished = this.list.length >= this.total
      } finally {
        this.loading = false
      }
    },
    async adjust(item, amount, title) {
      const action = amount > 0 ? '补发' : '冲正'
      const confirmed = await this.confirmAction('确认调整积分', `将为「${item.user.name}」${action} ${Math.abs(amount)} 积分，操作会写入积分流水。`)
      if (!confirmed) return
      const res = await adjustPoints({
        userId: item.user.id,
        amount,
        title,
        status: 'available',
      })
      if (res.ok === false) {
        this.$refs.toast.show({ message: res.message || '积分调整失败', type: 'error' })
        return
      }
      this.$refs.toast.show({ message: '积分已调整', type: 'success' })
      this.refresh()
    },
    confirmAction(title, content) {
      return new Promise((resolve) => {
        uni.showModal({
          title,
          content,
          confirmText: '确认',
          cancelText: '取消',
          confirmColor: '#0b4f46',
          success: (res) => resolve(Boolean(res.confirm)),
          fail: () => resolve(false),
        })
      })
    },
  },
}
</script>

<style lang="scss" scoped>
.account-list-page {
  padding-bottom: 170rpx;
}

.denied-card,
.filter-card,
.account-card {
  margin-top: 22rpx;
  padding: 28rpx;
}

.denied-card__title {
  margin-bottom: 12rpx;
  color: #17312d;
  font-size: 34rpx;
  font-weight: 900;
}

.summary-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 16rpx;
  margin-top: 18rpx;
}

.summary-card {
  min-height: 128rpx;
  padding: 24rpx;
}

.summary-card text:first-child {
  color: #6f7c78;
  font-size: 22rpx;
  font-weight: 800;
}

.summary-card text:last-child {
  display: block;
  margin-top: 12rpx;
  color: #0b4f46;
  font-size: 38rpx;
  font-weight: 900;
}

.search-box {
  display: flex;
  align-items: center;
  gap: 12rpx;
  height: 74rpx;
  padding: 0 22rpx;
  border-radius: 16rpx;
  background: #f3f8ef;
}

.search-box input {
  flex: 1;
  color: #17312d;
  font-size: 26rpx;
}

.status-row {
  display: flex;
  gap: 12rpx;
  margin-top: 18rpx;
  overflow-x: auto;
  white-space: nowrap;
}

.status-chip {
  height: 54rpx;
  padding: 0 20rpx;
  border: 1rpx solid #dce8dc;
  border-radius: 999rpx;
  color: #6f7c78;
  background: #ffffff;
  font-size: 22rpx;
  font-weight: 900;
  line-height: 54rpx;
}

.status-chip--active {
  color: #0b4f46;
  background: #dff8ee;
  border-color: #9ae9dc;
}

.query-actions {
  display: grid;
  grid-template-columns: 0.86fr 1.14fr;
  gap: 14rpx;
  margin-top: 18rpx;
}

.query-actions button {
  height: 68rpx;
  border-radius: 16rpx;
  font-size: 25rpx;
  font-weight: 900;
  line-height: 68rpx;
}

.query-actions__ghost {
  color: #0b4f46;
  background: #eef8f4;
}

.query-actions__primary {
  color: #ffffff;
  background: #0b4f46;
}

.account-card__head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 18rpx;
}

.account-card__main {
  min-width: 0;
}

.account-card__name {
  color: #17312d;
  font-size: 30rpx;
  font-weight: 900;
}

.account-card__meta {
  margin-top: 8rpx;
  color: #6f7c78;
  font-size: 22rpx;
  line-height: 1.45;
}

.account-card__score {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  flex: 0 0 auto;
  min-width: 112rpx;
}

.account-card__score text:first-child {
  color: #0b4f46;
  font-size: 38rpx;
  font-weight: 900;
}

.account-card__score text:last-child {
  margin-top: 4rpx;
  color: #6f7c78;
  font-size: 20rpx;
  font-weight: 800;
}

.points-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 12rpx;
  margin-top: 22rpx;
}

.points-grid view {
  padding: 18rpx 12rpx;
  border-radius: 14rpx;
  background: #f3f8ef;
  text-align: center;
}

.points-grid text {
  display: block;
}

.points-grid text:first-child {
  color: #17312d;
  font-size: 28rpx;
  font-weight: 900;
}

.points-grid text:last-child {
  margin-top: 6rpx;
  color: #6f7c78;
  font-size: 20rpx;
  font-weight: 800;
}

.account-actions {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 14rpx;
  margin-top: 22rpx;
}

.account-actions button {
  height: 66rpx;
  border-radius: 16rpx;
  color: #ffffff;
  background: #0b4f46;
  font-size: 24rpx;
  font-weight: 900;
  line-height: 66rpx;
}

.account-actions .is-warn {
  background: #ff7a59;
}

.load-state {
  padding: 28rpx 0 90rpx;
  color: #6f7c78;
  font-size: 23rpx;
  text-align: center;
}
</style>
R]xQ@#dmin-appointments.S预约到课0预约记录，确认到课后触发积分到账N(条7查看预约记录AG课程、校区sbuttonX)5$id" class="lf-card appointment-card"`
ppointment-card__top"!ppointme3*ppointmea?.name || '-'%ppointme2?.phone || '-' }} · {{ formatTime(item.createdAt)u-tag :text="statusLabel(item.status)" plain shape="circle" color="#0b4f46" borderColor="#b9d8d2"></u-tag>
        </view>

   e
course-panel"a	-course?.name || '-'	
=course?.campus || '-' }} · {{ item.course?.teacher || '-' }}"k	#course?.nextTime || '-'	试听时间*
        <button v-if="item.status === 'booked'" class="attend-button" @click="confirmAttend(item)">确认到课</button>
     @yZcourse(ttendAdminAppointment, getAdminAppointmeUsE已预约', value: 'booked'P到课', value: 'attended'P取消', value: 'cancelledl0预约记录s/Vl	Appointmem预约记录加载"m9statusLabel(status) {
      const map = { booked: '已预约', attended: '已到课', cancelled: '已取消' }
      return map[status] || status || '-'
    },
    formatTime(value) {
      if (!value) return '-'
      return String(value).replace('T', '7 ').slice(0, 16)
    },
    async confirmAttend(item) {82
        '确认到课',
        `确认「${item.user?.name || item.userId}」已完成「${item.course?.name || '试听课'}M到课？确认后会处理对应待到账积分和到课奖励。`,
      8ttendAdminAppointment(item.id)
&2认到课Y已确认到课XOdmin-appointments,5H'ppointment-card {%4button,
.attend-button {
  width: 100%;
  height: 68%!'5!4appointment-card__top#"Appointme#%ppointmeC#Oppointme#ccourse-panel1!-1.1fr 0.9fr~')1course-panel view {
  min-width: 0;
  padding: 18%4}

.course-panelI&course-panel$3!*1.35;
}

.course-panel&!#&=%line-height: 1.4(t`)x% 100644 index.vue x$I\3Y{GUh
x6*<template>
  <view class="lf-page course-edit-page">
    <view class="lf-nav">
      <view>
        <view class="lf-nav__title">{{ isEdit ? '编辑试听课' : '新增试听课' }}</view>
        <view class="lf-nav__sub">维护家长端可预约的课程、席位和到课积分</view>
      </view>
      <button class="nav-back" @click="goBack">返回</button>
    </view>

    <view v-if="!isAdmin" class="lf-card denied-card">
      <view class="denied-card__title">当前账号没有管理权限</view>
      <view class="lf-caption">请切换到管理账号后再维护试听课。</view>
    </view>

    <template v-else>
      <view class="lf-card edit-hero">
        <view class="edit-hero__mark">{{ isEdit ? '改' : '新' }}</view>
        <view>
          <view class="edit-hero__title">{{ form.name || '未命名试听课' }}</view>
          <view class="edit-hero__sub">
            {{ form.campus || '未设置校区' }} · {{ form.teacher || '未设置老师' }} · {{ form.trialMinutes || 0 }} 分钟
          </view>
        </view>
      </view>

      <view class="lf-card form-card">
        <view class="form-section-title">课程信息</view>
        <view class="form-field form-field--wide">
          <text>课程名称</text>
          <input v-model="form.name" placeholder="例如：数学思维体验课" />
        </view>
        <view class="form-grid">
          <view class="form-field">
            <text>校区</text>
            <input v-model="form.campus" placeholder="例如：南山校区" />
          </view>
          <view class="form-field">
            <text>老师</text>
            <input v-model="form.teacher" placeholder="例如：林老师" />
          </view>
        </view>
        <view class="form-field form-field--wide">
          <text>课程标签</text>
          <input v-model="form.tagsText" placeholder="用逗号分隔，例如：试听,小班,可转班" />
        </view>
      </view>

      <view class="lf-card form-card">
        <view class="form-section-title">排期与权益</view>
        <view class="form-field form-field--wide">
          <text>试听时间</text>
          <view class="datetime-trigger" @click="openNextTimePicker">
            <text :class="form.nextTime ? '' : 'is-placeholder'">{{ form.nextTime || '选择试听日期和时间' }}</text>
            <up-icon name="calendar" color="#0b4f46" size="20"></up-icon>
          </view>
        </view>
        <view class="form-grid form-grid--three">
          <view class="form-field">
            <text>席位</text>
            <view class="stepper">
              <button @click="changeNumber('seats', -1)">-</button>
              <input :value="form.seats" type="number" @input="onNumberInput('seats', $event)" />
              <button @click="changeNumber('seats', 1)">+</button>
            </view>
          </view>
          <view class="form-field">
            <text>到课积分</text>
            <input :value="form.pointsBack" type="number" @input="onNumberInput('pointsBack', $event)" />
          </view>
          <view class="form-field">
            <text>时长</text>
            <input :value="form.trialMinutes" type="number" @input="onNumberInput('trialMinutes', $event)" />
          </view>
        </view>
      </view>

      <view class="lf-card form-card">
        <view class="form-section-title">上架状态</view>
        <view class="status-switch">
          <button :class="form.status === 'active' && 'is-active'" @click="form.status = 'active'">上架</button>
          <button :class="form.status === 'paused' && 'is-active'" @click="form.status = 'paused'">暂停</button>
        </view>
      </view>

      <view class="save-panel">
        <button class="save-panel__ghost" @click="goBack">取消</button>
        <button class="save-panel__primary" :disabled="saving" @click="submit">
          {{ saving ? '保存中...' : (isEdit ? '保存修改' : '新增课程') }}
        </button>
      </view>
    </template>

    <AdminTabbar v-if="isAdmin" active="courses" />
    <up-datetime-picker
      v-model="nextTimeValue"
      :show="nextTimePickerVisible"
      mode="datetime"
      title="选择试听时间"
      :min-date="minCourseTime"
      :max-date="maxCourseTime"
      confirm-color="#0b4f46"
      @confirm="confirmNextTime"
      @cancel="closeNextTimePicker"
      @close="closeNextTimePicker"
    ></up-datetime-picker>
    <u-toast ref="toast"></u-toast>
  </view>
</template>

<script>
import AdminTabbar from '../../components/AdminTabbar.vue'
import { createAdminCourse, getAdminCourse, updateAdminCourse } from '../../services/educationApi'
import { getStoredAccount, isAdminAccount } from '../../services/accountSession'

function blankForm() {
  return {
    id: '',
    name: '',
    campus: '',
    teacher: '',
    nextTime: '',
    seats: 4,
    pointsBack: 100,
    trialMinutes: 45,
    status: 'paused',
    tagsText: '试听,小班',
  }
}

function padNumber(value) {
  return String(value).padStart(2, '0')
}

function formatDateTime(value) {
  const date = new Date(Number(value))
  if (Number.isNaN(date.getTime())) return ''
  return `${date.getFullYear()}-${padNumber(date.getMonth() + 1)}-${padNumber(date.getDate())} ${padNumber(date.getHours())}:${padNumber(date.getMinutes())}`
}

function defaultCourseTime() {
  const date = new Date(Date.now() + 24 * 60 * 60 * 1000)
  date.setMinutes(0, 0, 0)
  return date.getTime()
}

function parseCourseTime(value) {
  const match = String(value || '').match(/^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2})$/)
  if (!match) return defaultCourseTime()
  const date = new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3]), Number(match[4]), Number(match[5]))
  return Number.isNaN(date.getTime()) ? defaultCourseTime() : date.getTime()
}

export default {
  components: { AdminTabbar },
  data() {
    return {
      account: getStoredAccount() || {},
      courseId: '',
      form: blankForm(),
      saving: false,
      nextTimePickerVisible: false,
      nextTimeValue: defaultCourseTime(),
      minCourseTime: new Date().setHours(0, 0, 0, 0),
      maxCourseTime: new Date(new Date().getFullYear() + 2, 11, 31, 23, 59).getTime(),
    }
  },
  computed: {
    isAdmin() {
      return isAdminAccount(this.account)
    },
    isEdit() {
      return Boolean(this.courseId)
    },
  },
  onLoad(query = {}) {
    this.courseId = query.id || ''
  },
  onShow() {
    const account = getStoredAccount()
    if (!account) {
      uni.reLaunch({ url: '/pages/login/index' })
      return
    }
    this.account = account
    if (this.isAdmin && this.courseId) this.loadCourse()
  },
  methods: {
    async loadCourse() {
      const res = await getAdminCourse(this.courseId)
      if (res.ok === false) {
        this.$refs.toast.show({ message: res.message || '课程加载失败', type: 'error' })
        return
      }
      const course = res.data || res
      this.form = {
        id: course.id,
        name: course.name || '',
        campus: course.campus || '',
        teacher: course.teacher || '',
        nextTime: course.nextTime || '',
        seats: course.seats ?? 0,
        pointsBack: course.pointsBack ?? 0,
        trialMinutes: course.trialMinutes ?? 45,
        status: course.status || 'paused',
        tagsText: (course.tags || []).join(','),
      }
      this.nextTimeValue = parseCourseTime(course.nextTime)
    },
    buildPayload() {
      return {
        name: String(this.form.name || '').trim(),
        campus: String(this.form.campus || '').trim(),
        teacher: String(this.form.teacher || '').trim(),
        nextTime: String(this.form.nextTime || '').trim(),
        seats: this.toNumber(this.form.seats),
        pointsBack: this.toNumber(this.form.pointsBack),
        trialMinutes: this.toNumber(this.form.trialMinutes),
        status: this.form.status || 'paused',
        tags: String(this.form.tagsText || '')
          .split(/[,，]/)
          .map((item) => item.trim())
          .filter(Boolean),
      }
    },
    async submit() {
      const payload = this.buildPayload()
      if (!payload.name) {
        this.$refs.toast.show({ message: '请填写课程名称', type: 'error' })
        return
      }
      if (!payload.nextTime) {
        this.$refs.toast.show({ message: '请选择试听日期和时间', type: 'error' })
        return
      }
      const confirmed = await this.confirmAction(
        this.isEdit ? '确认保存课程' : '确认新增课程',
        this.isEdit ? `保存「${payload.name}」后，家长端课程信息会同步变化。` : `新增「${payload.name}」后，可在家长端按上架状态展示。`,
      )
      if (!confirmed) return
      this.saving = true
      try {
        const res = this.isEdit ? await updateAdminCourse(this.courseId, payload) : await createAdminCourse(payload)
        if (res.ok === false) {
          this.$refs.toast.show({ message: res.message || '保存失败', type: 'error' })
          return
        }
        this.$refs.toast.show({ message: this.isEdit ? '课程已保存' : '课程已新增', type: 'success' })
        setTimeout(() => this.goBack(), 500)
      } finally {
        this.saving = false
      }
    },
    changeNumber(key, delta) {
      this.form[key] = Math.max(0, this.toNumber(this.form[key]) + delta)
    },
    onNumberInput(key, event) {
      const value = event.detail?.value ?? ''
      this.form[key] = value === '' ? '' : Math.max(0, Number(value))
    },
    openNextTimePicker() {
      this.nextTimeValue = parseCourseTime(this.form.nextTime)
      this.nextTimePickerVisible = true
    },
    confirmNextTime(event) {
      this.nextTimeValue = Number(event.value)
      this.form.nextTime = formatDateTime(event.value)
      this.nextTimePickerVisible = false
    },
    closeNextTimePicker() {
      this.nextTimePickerVisible = false
    },
    toNumber(value) {
      const number = Number(value)
      return Number.isFinite(number) && number >= 0 ? number : 0
    },
    goBack() {
      const pages = getCurrentPages()
      if (pages.length > 1) {
        uni.navigateBack()
        return
      }
      uni.redirectTo({ url: '/pages/admin-courses/index' })
    },
    confirmAction(title, content) {
      return new Promise((resolve) => {
        uni.showModal({
          title,
          content,
          confirmText: '确认',
          cancelText: '取消',
          confirmColor: '#0b4f46',
          success: (res) => resolve(Boolean(res.confirm)),
          fail: () => resolve(false),
        })
      })
    },
  },
}
</script>

<style lang="scss" scoped>
.course-edit-page {
  padding-bottom: 170rpx;
}

.nav-back {
  min-width: 92rpx;
  height: 52rpx;
  padding: 0 18rpx;
  border-radius: 999rpx;
  color: #0b2f2a;
  background: #f5c451;
  font-size: 22rpx;
  font-weight: 900;
  line-height: 52rpx;
}

.denied-card,
.edit-hero,
.form-card {
  margin-top: 22rpx;
  padding: 28rpx;
}

.denied-card__title {
  margin-bottom: 12rpx;
  color: #17312d;
  font-size: 34rpx;
  font-weight: 900;
}

.edit-hero {
  display: flex;
  align-items: center;
  gap: 22rpx;
  color: #ffffff;
  background:
    linear-gradient(135deg, rgba(245, 196, 81, 0.24), transparent 44%),
    #0b2f2a;
}

.edit-hero__mark {
  display: grid;
  place-items: center;
  width: 82rpx;
  height: 82rpx;
  border-radius: 18rpx;
  color: #0b2f2a;
  background: #f5c451;
  font-size: 30rpx;
  font-weight: 900;
}

.edit-hero__title {
  font-size: 34rpx;
  font-weight: 900;
  line-height: 1.3;
}

.edit-hero__sub {
  margin-top: 8rpx;
  color: rgba(255, 255, 255, 0.72);
  font-size: 22rpx;
}

.form-section-title {
  margin-bottom: 22rpx;
  color: #17312d;
  font-size: 30rpx;
  font-weight: 900;
}

.form-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 16rpx;
}

.form-grid--three {
  grid-template-columns: repeat(3, 1fr);
}

.form-field {
  min-width: 0;
  margin-top: 18rpx;
}

.form-field--wide:first-of-type,
.form-section-title + .form-field {
  margin-top: 0;
}

.form-field text {
  display: block;
  margin-bottom: 10rpx;
  color: #6f7c78;
  font-size: 21rpx;
  font-weight: 900;
}

.form-field input {
  width: 100%;
  height: 76rpx;
  box-sizing: border-box;
  padding: 0 20rpx;
  border: 1rpx solid #dfe9dc;
  border-radius: 14rpx;
  color: #17312d;
  background: #f8faf5;
  font-size: 25rpx;
  line-height: 76rpx;
}

.datetime-trigger {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  height: 76rpx;
  box-sizing: border-box;
  padding: 0 20rpx;
  border: 1rpx solid #dfe9dc;
  border-radius: 14rpx;
  color: #17312d;
  background: #f8faf5;
  font-size: 25rpx;
}

.datetime-trigger text {
  min-width: 0;
  margin: 0;
  color: #17312d;
  font-size: 25rpx;
  font-weight: 500;
}

.datetime-trigger text.is-placeholder {
  color: #9aa6a1;
}

.stepper {
  display: grid;
  grid-template-columns: 48rpx minmax(54rpx, 1fr) 48rpx;
  gap: 8rpx;
}

.stepper button {
  height: 76rpx;
  border-radius: 14rpx;
  color: #0b4f46;
  background: #dff8ee;
  font-size: 28rpx;
  font-weight: 900;
  line-height: 76rpx;
}

.stepper input {
  padding: 0 8rpx;
  text-align: center;
}

.status-switch {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 14rpx;
}

.status-switch button {
  height: 78rpx;
  border: 1rpx solid #dfe9dc;
  border-radius: 16rpx;
  color: #6f7c78;
  background: #f8faf5;
  font-size: 25rpx;
  font-weight: 900;
  line-height: 78rpx;
}

.status-switch button.is-active {
  color: #0b4f46;
  border-color: #9ae9dc;
  background: #dff8ee;
}

.save-panel {
  display: grid;
  grid-template-columns: 0.85fr 1.15fr;
  gap: 14rpx;
  margin-top: 24rpx;
}

.save-panel button {
  height: 78rpx;
  border-radius: 18rpx;
  font-size: 26rpx;
  font-weight: 900;
  line-height: 78rpx;
}

.save-panel__ghost {
  color: #0b4f46;
  background: #eef8f4;
}

.save-panel__primary {
  color: #ffffff;
  background: #0b4f46;
}

.save-panel__primary[disabled] {
  opacity: 0.68;
}
</style>
\x% 100644 index.vue ݸARA0x<<template>
  <view class="lf-page admin-courses-page">
    <view class="lf-nav">
      <view>
        <view class="lf-nav__title">试听管理</view>
        <view class="lf-nav__sub">可试听课程、名额与上架状态</view>
      </view>
      <view v-if="isAdmin" class="course-nav-actions">
        <view class="lf-pill">{{ filteredCourses.length }} 门</view>
        <button class="nav-add" @click="openCreate">新增</button>
      </view>
      <view v-else class="lf-pill">{{ filteredCourses.length }} 门</view>
    </view>

    <view v-if="!isAdmin" class="lf-card denied-card">
      <view class="denied-card__title">当前账号没有管理权限</view>
      <view class="lf-caption">请切换到管理账号后维护试听课程。</view>
      <u-button color="#0b4f46" shape="circle" text="切换账号" @click="confirmLogout"></u-button>
    </view>

    <template v-else>
      <view class="lf-card course-filter">
        <view class="course-search">
          <u-icon name="search" color="#6f7c78" size="22"></u-icon>
          <input v-model="filters.keyword" placeholder="搜索课程、老师、校区" confirm-type="search" />
        </view>
        <view class="filter-row">
          <button
            v-for="status in statusOptions"
            :key="status.value"
            :class="['status-chip', filters.status === status.value && 'status-chip--active']"
            @click="filters.status = status.value"
          >
            {{ status.label }}
          </button>
        </view>
        <view class="campus-strip">
          <button
            v-for="campus in campusOptions"
            :key="campus"
            :class="['campus-chip', filters.campus === campus && 'campus-chip--active']"
            @click="filters.campus = campus"
          >
            {{ campus }}
          </button>
        </view>
      </view>

      <view class="lf-card appointments-card">
        <view class="lf-row">
          <view>
            <view class="lf-section-title">预约到课</view>
            <view class="lf-caption">当前页仅展示最新 3 条预约</view>
          </view>
          <button class="ghost-button" @click="go('/pages/admin-appointments/index')">查看更多</button>
        </view>
        <view v-if="!previewAppointments.length" class="empty-mini">暂无预约记录</view>
        <view v-for="item in previewAppointments" :key="item.id" class="appointment-row">
          <view class="appointment-row__main">
            <view class="appointment-row__name">{{ item.user?.name || '-' }}</view>
            <view class="appointment-row__meta">{{ item.course?.name || '-' }} · {{ item.course?.nextTime || '-' }}</view>
            <view class="appointment-row__sub">{{ item.user?.phone || '-' }} · {{ item.course?.campus || '-' }}</view>
          </view>
          <view class="appointment-row__side">
            <u-tag :text="appointmentStatusLabel(item.status)" plain shape="circle" color="#0b4f46" borderColor="#b9d8d2"></u-tag>
            <button v-if="item.status === 'booked'" @click="confirmAppointment(item)">到课</button>
          </view>
        </view>
      </view>

      <view v-if="!filteredCourses.length" class="lf-card empty-card">
        <view class="empty-card__title">暂无匹配课程</view>
        <view class="lf-caption">可以调整筛选条件，或新增一门试听课程。</view>
        <button class="empty-card__button" @click="openCreate">新增试听课</button>
      </view>

      <view v-for="course in filteredCourses" :key="course.id" class="lf-card course-card">
        <view class="course-card__top">
          <view class="course-card__main">
            <view class="course-card__name">{{ course.name }}</view>
            <view class="course-card__meta">{{ course.campus || '未设置校区' }} · {{ course.teacher || '未设置老师' }} · {{ course.trialMinutes || 0 }} 分钟</view>
          </view>
          <view :class="['course-status', course.status === 'paused' && 'course-status--paused']">
            {{ course.status === 'paused' ? '已暂停' : '上架中' }}
          </view>
        </view>

        <view class="course-card__tags">
          <text v-for="tag in course.tags" :key="tag">{{ tag }}</text>
          <text v-if="!course.tags || !course.tags.length">未设置标签</text>
        </view>

        <view class="course-board">
          <view class="course-board__cell">
            <text>{{ course.nextTime || '待排期' }}</text>
            <text>下次试听</text>
          </view>
          <view class="course-board__cell seat-cell">
            <text>剩余席位</text>
            <view class="seat-count">{{ course.seats || 0 }}</view>
          </view>
          <view class="course-board__cell">
            <text>+{{ course.pointsBack || 0 }}</text>
            <text>到课积分</text>
          </view>
        </view>

        <view class="course-actions">
          <button @click="openEdit(course)">编辑</button>
          <button @click="toggleStatus(course)">{{ course.status === 'paused' ? '上架' : '暂停' }}</button>
          <button class="is-danger" @click="removeCourse(course)">删除</button>
        </view>
      </view>
    </template>

    <AdminTabbar v-if="isAdmin" active="courses" />
    <u-toast ref="toast"></u-toast>
  </view>
</template>

<script>
import AdminTabbar from '../../components/AdminTabbar.vue'
import { attendAdminAppointment, deleteAdminCourse, getAdminAppointments, getAdminCourses, updateAdminCourse } from '../../services/educationApi'
import { clearStoredAccount, getStoredAccount, isAdminAccount } from '../../services/accountSession'

export default {
  components: { AdminTabbar },
  data() {
    return {
      account: getStoredAccount() || {},
      courses: [],
      appointments: [],
      filters: {
        keyword: '',
        status: '全部',
        campus: '全部',
      },
      statusOptions: [
        { label: '全部', value: '全部' },
        { label: '上架中', value: 'active' },
        { label: '已暂停', value: 'paused' },
      ],
    }
  },
  computed: {
    isAdmin() {
      return isAdminAccount(this.account)
    },
    campusOptions() {
      return ['全部', ...new Set(this.courses.map((item) => item.campus).filter(Boolean))]
    },
    filteredCourses() {
      const keyword = this.filters.keyword.trim()
      return this.courses.filter((course) => {
        const matchKeyword = !keyword || [course.name, course.teacher, course.campus, ...(course.tags || [])].some((value) => String(value || '').includes(keyword))
        const matchStatus = this.filters.status === '全部' || course.status === this.filters.status
        const matchCampus = this.filters.campus === '全部' || course.campus === this.filters.campus
        return matchKeyword && matchStatus && matchCampus
      })
    },
    previewAppointments() {
      return this.appointments.slice(0, 3)
    },
  },
  onShow() {
    const account = getStoredAccount()
    if (!account) {
      uni.reLaunch({ url: '/pages/login/index' })
      return
    }
    this.account = account
    if (this.isAdmin) this.load()
  },
  async onPullDownRefresh() {
    if (this.isAdmin) await this.load()
    uni.stopPullDownRefresh()
  },
  methods: {
    async load() {
      await Promise.all([this.loadCourses(), this.loadAppointments()])
    },
    async loadCourses() {
      const res = await getAdminCourses()
      const list = res.data || res || this.courses
      this.courses = list.map((item) => ({ ...item, tags: [...(item.tags || [])] }))
    },
    async loadAppointments() {
      const res = await getAdminAppointments({ pageNum: 1, pageSize: 3, status: 'all' })
      const data = res.data || res || {}
      this.appointments = data.list || []
    },
    appointmentStatusLabel(status) {
      const map = { booked: '已预约', attended: '已到课', cancelled: '已取消' }
      return map[status] || status || '-'
    },
    async confirmAppointment(item) {
      const confirmed = await this.confirmAction(
        '确认到课',
        `确认「${item.user?.name || item.userId}」已完成「${item.course?.name || '试听课'}」到课？确认后会处理对应待到账积分和到课奖励。`,
      )
      if (!confirmed) return
      const res = await attendAdminAppointment(item.id)
      if (res.ok === false) {
        this.$refs.toast.show({ message: res.message || '确认到课失败', type: 'error' })
        return
      }
      this.$refs.toast.show({ message: '已确认到课', type: 'success' })
      await this.loadAppointments()
    },
    openCreate() {
      uni.navigateTo({ url: '/pages/admin-course-edit/index' })
    },
    openEdit(course) {
      uni.navigateTo({ url: `/pages/admin-course-edit/index?id=${encodeURIComponent(course.id)}` })
    },
    async toggleStatus(course) {
      const nextStatus = course.status === 'paused' ? 'active' : 'paused'
      const label = nextStatus === 'active' ? '上架' : '暂停'
      const confirmed = await this.confirmAction(`确认${label}课程`, `${label}「${course.name}」后，家长端可预约课程会同步变化。`)
      if (!confirmed) return
      await updateAdminCourse(course.id, { status: nextStatus })
      course.status = nextStatus
      this.$refs.toast.show({ message: `课程已${label}`, type: 'success' })
    },
    async removeCourse(course) {
      const confirmed = await this.confirmAction('确认删除课程', `仅可删除没有预约记录的「${course.name}」。已有预约时请暂停课程。`)
      if (!confirmed) return
      await deleteAdminCourse(course.id)
      this.courses = this.courses.filter((item) => item.id !== course.id)
      this.$refs.toast.show({ message: '课程已删除', type: 'success' })
    },
    confirmLogout() {
      uni.showModal({
        title: '确认退出登录',
        content: '退出后需要重新登录当前账号，是否继续？',
        confirmText: '退出',
        confirmColor: '#b54732',
        success: (res) => {
          if (!res.confirm) return
          clearStoredAccount()
          uni.reLaunch({ url: '/pages/login/index' })
        },
      })
    },
    go(url) {
      uni.navigateTo({ url })
    },
    confirmAction(title, content) {
      return new Promise((resolve) => {
        uni.showModal({
          title,
          content,
          confirmText: '确认',
          cancelText: '取消',
          confirmColor: '#0b4f46',
          success: (res) => resolve(Boolean(res.confirm)),
          fail: () => resolve(false),
        })
      })
    },
  },
}
</script>

<style lang="scss" scoped>
.admin-courses-page {
  padding-bottom: 150rpx;
}

.course-nav-actions {
  display: flex;
  align-items: center;
  gap: 12rpx;
}

.nav-add {
  min-width: 92rpx;
  height: 52rpx;
  padding: 0 18rpx;
  border-radius: 999rpx;
  color: #0b2f2a;
  background: #f5c451;
  font-size: 22rpx;
  font-weight: 900;
  line-height: 52rpx;
}

.denied-card,
.course-filter,
.appointments-card,
.course-card,
.empty-card {
  margin-top: 22rpx;
  padding: 28rpx;
}

.denied-card__title,
.empty-card__title {
  margin-bottom: 12rpx;
  color: #17312d;
  font-size: 34rpx;
  font-weight: 900;
}

.empty-card__button {
  width: 220rpx;
  height: 64rpx;
  margin-top: 22rpx;
  border-radius: 999rpx;
  color: #ffffff;
  background: #0b4f46;
  font-size: 24rpx;
  font-weight: 900;
  line-height: 64rpx;
}

.ghost-button {
  min-width: 132rpx;
  height: 52rpx;
  border: 1rpx solid #b9d8d2;
  border-radius: 999rpx;
  color: #0b4f46;
  background: #ffffff;
  font-size: 22rpx;
  font-weight: 900;
  line-height: 52rpx;
}

.empty-mini {
  padding: 28rpx 0 6rpx;
  color: #6f7c78;
  font-size: 23rpx;
  text-align: center;
}

.appointment-row {
  display: flex;
  justify-content: space-between;
  gap: 18rpx;
  padding: 24rpx 0;
  border-bottom: 1rpx solid #edf1e9;
}

.appointment-row:last-child {
  border-bottom: 0;
}

.appointment-row__main {
  min-width: 0;
}

.appointment-row__name {
  color: #17312d;
  font-size: 28rpx;
  font-weight: 900;
}

.appointment-row__meta,
.appointment-row__sub {
  margin-top: 8rpx;
  color: #6f7c78;
  font-size: 21rpx;
  line-height: 1.45;
}

.appointment-row__side {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 12rpx;
  flex: 0 0 auto;
}

.appointment-row__side button {
  min-width: 86rpx;
  height: 50rpx;
  border-radius: 999rpx;
  color: #ffffff;
  background: #0b4f46;
  font-size: 22rpx;
  font-weight: 900;
  line-height: 50rpx;
}

.course-search {
  display: flex;
  align-items: center;
  gap: 12rpx;
  height: 74rpx;
  padding: 0 22rpx;
  border-radius: 18rpx;
  background: #f3f8ef;
}

.course-search input {
  flex: 1;
  color: #17312d;
  font-size: 26rpx;
}

.filter-row,
.campus-strip {
  display: flex;
  gap: 12rpx;
  margin-top: 18rpx;
  overflow-x: auto;
  white-space: nowrap;
}

.status-chip,
.campus-chip {
  height: 54rpx;
  padding: 0 20rpx;
  border: 1rpx solid #dce8dc;
  border-radius: 999rpx;
  color: #6f7c78;
  background: #ffffff;
  font-size: 22rpx;
  font-weight: 900;
  line-height: 54rpx;
  white-space: nowrap;
}

.status-chip--active,
.campus-chip--active {
  color: #0b4f46;
  background: #dff8ee;
  border-color: #9ae9dc;
}

.course-card__top {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 18rpx;
}

.course-card__main {
  min-width: 0;
}

.course-card__name {
  color: #17312d;
  font-size: 31rpx;
  font-weight: 900;
  line-height: 1.35;
}

.course-card__meta {
  margin-top: 8rpx;
  color: #6f7c78;
  font-size: 22rpx;
  line-height: 1.45;
}

.course-status {
  flex: 0 0 auto;
  height: 44rpx;
  padding: 0 16rpx;
  border-radius: 999rpx;
  color: #0b4f46;
  background: #dff8ee;
  font-size: 20rpx;
  font-weight: 900;
  line-height: 44rpx;
  white-space: nowrap;
}

.course-status--paused {
  color: #9a3d20;
  background: #ffe0d6;
}

.course-card__tags {
  display: flex;
  flex-wrap: wrap;
  gap: 12rpx;
  margin-top: 18rpx;
}

.course-card__tags text {
  padding: 8rpx 14rpx;
  border-radius: 999rpx;
  color: #0b4f46;
  background: #eef8f4;
  font-size: 20rpx;
  font-weight: 800;
}

.course-board {
  display: grid;
  grid-template-columns: 1.18fr 1.18fr 0.86fr;
  gap: 12rpx;
  margin-top: 22rpx;
}

.course-board__cell {
  min-width: 0;
  padding: 18rpx;
  border-radius: 14rpx;
  background: #f3f8ef;
}

.course-board__cell text {
  display: block;
}

.course-board__cell text:first-child {
  color: #0b4f46;
  font-size: 25rpx;
  font-weight: 900;
  line-height: 1.35;
}

.course-board__cell text:last-child {
  margin-top: 8rpx;
  color: #6f7c78;
  font-size: 20rpx;
  font-weight: 800;
}

.seat-cell {
  padding: 14rpx;
}

.seat-cell > text {
  color: #6f7c78 !important;
  font-size: 20rpx !important;
}

.seat-count {
  margin-top: 10rpx;
  color: #0b4f46;
  font-size: 32rpx;
  font-weight: 900;
}

.course-actions {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 12rpx;
  margin-top: 22rpx;
}

.course-actions button {
  height: 66rpx;
  border-radius: 16rpx;
  color: #0b4f46;
  background: #eef8f4;
  font-size: 24rpx;
  font-weight: 900;
  line-height: 66rpx;
  white-space: nowrap;
}

.course-actions .is-primary {
  color: #ffffff;
  background: #0b4f46;
}

.course-actions .is-danger {
  color: #9a3d20;
  background: #ffe0d6;
}

</style>
$x% 100644 index.vue YXڒL"E[Dy+HxZx^(lead.T线索0*筛选来源和阶段，推进推荐转化"_ALeads.length }} 条查看线索filter-consolefilter-console__top	search-box">
  N6家长、来源、渠道j#$&scroll-view scroll-x class="stage-rail(stage in stageOstagep3stage-pill', filters.stage === stage && 'stage-pill(stage = stage"
          Pstage }}</textPstageCount(stage) }}</text>scroll-Bfilter-console__foot">
          <text>{{ filterSummary }}</text>
BactiveFilterCount || filters.keyword" @click="resetFilters">重置@(O[ead-list">
        <view v-for="lead in filteredLeads" :key="lead.id" class="lf-card lead-c lead-card__head	ead-card__name">{{ lead.parentIead-card__meta">{{ lead.channel || lead.source }} · 来源 {{ lead.sourc:F
lead.stageyPlead-card__body	W  <text>来源</text>
              <text>{{ lead.channel || lead.source }}</text>
    1  <button :class="['advance-button', lead.stage === '已报名' && 'advance-button--disabled', nextStage(lead) === '已报名' X&& 'advance-button--purchase']" @click="advance(lead)">
            {{ actionLabel(lead).`Alead2confirmLeadPurchase, getAdminLeads, updateLeadStagSconst stageFlow = ['待跟进', '已注册', '已预约', '已到课', '已报名']"ylead8ge"geOptions: ['全部', ...stageFlowa7activeFilterCount() {
      return [this.filters.stage]% !== '全部').length
    },
    filterSummary() {
      const parts = []
      if (this.filters.stage !== '全部') parts.push(this.filters.stage)
      return parts.length ? `已筛选：${parts.join(' / ')}` : '展示全部线索，可按阶段快速换'
    },
    filteredLeadJleads.filter((lead1Clead.parent, lead.phone, lead.childGrade, lead.source, lead.channelOIge = this.filters.stage === '全部' || lead.stage === this.filters.stage(
ge
      } lLeads($
      this.leads = data.list || data || this.leads
    },
    stageCount(stage) {
      if (stage === '全部') return this.leads.length
      return this.leads.filter((lead) => lead.stage === stage).length
    },
    resetFilters() {
      this.filters.keyword = ''
      this.filters.stage = '全部'
    },
    nextStage(lead) {
      const index = stageFlow.indexOf(lead.stage)
      return stageFlow[Math.min(index + 1, stageFlow.length - 1)] || '已注册'
    },
    actionLabel(lead) {
      if (lead.stage === '已报名') return '已完成报名'
      return this.nextStage(lead) === '已报名' ? '确认报名正价课' : '推进到下一阶段'
    },
    async advance(lead) {
      const next = this.nextStage(lead)
      if (lead.stage === next -*'线索已到最终阶段', type: 'default!"'const isPurchase = next === '已报名'L;isPurchase ? '确认报名正价课' : '确认推进线索',
        isPurchase
          ? `确认「${lead.parent}」已报正价课？确认后会发放报名阶段推荐奖励，并更新推荐漏斗。`
          : `将「${lead.parent}」从「$={lead.stage}」推进到「${next}」，会影响转化统计> =UisPurchase ? await confirmLeadPurchase(lead.id) : await updateLeadStage(lead.id, next Z索更新!7lead.stage = next
 'EisPurchase ? '已确认报名并发放奖励' : `已推进到${next}`1&ylead)'denied-card,
.filter-console {:O+(+Y7filter-console {
  overflow: hidden;
}

.filter-console3(*4rpx;
}

.search-box.flex: 1!1
search-box1Emore {
  position: relative;1+2justify-content: center;
  gap: 6rpx;
  width: 144C1#5~1	3)53,(1P;nfilter-more text {
  line-height: 1;
}

.filter-more__dot {
  position: absolute;
  top: 8rpx;
  right: 12rpx;q75place-items: center;
  min-width: 28rpx;
  height: 285(;ff7a59;
  font-size: 18,+stage-rail {:6stage-pill {
  display: inline-14"5min-width: 158rpx;
  height: 72rpx;
  margin-right: 1O*"2':+
4-L1;
  vertical-align: topP;/stage-pill text:last-child {
  display: inline-1Ojustify-content: center;
  min-width: 34rpx;
  height: 34rpx;
  margin-left: 145(/9
$background: #eef3ea;
  font-size: 18!534rpx;
}

.stage-pill--active {-order-color: #9ae9dc3&}

.stage-pill--active text:last-child;0filter-console__foot.)4(6!2}

.filter-console__foot95_-800;
}

.filter-console__footO02rpx;
  height: 425(ff7a590-1ec5*+filter-sheet {
  padding: 18rpx 28rpx calc(34rpx + env(safe-area-inset-bottom));
  background: #fbfcf7;
}

.filter-sheet__bar {4
  width: 78rpx;
  height: 8rpx;
  margin: 0 auto 225 1dce8dc;
}

.filter-sheet__head.)4 }

.filter-sheet__titl4$+filter-sheet_/6 9% 5}

.filter-sheet__headO0108rpx;
  height: 55);0(+Dfilter-sheet__group {
  margin-top: 28rpx;
}

.filter-sheet__label {1!8+
sheet-chip-6.#6rpx;
}

.sheet-chip {
  height: 562+sheet3%order-color: #9ae9dc3E}

.lead-list {
  margin-top: 20rpx;
}

.lead-card {
  margin-top: 18J+lead-card__head.)	4@	lead-card.(30,+lead4H}

.lead-card__bodyo7.fr;
  gap: 14!2:lead-card__body view {7FOlead-card__body text {
  display: block;
}

.lead-card__body text:first-child {2/5800;
}

.lead-card__body81+,+,advance-button {
  width: 100%;
  height: 74!2245:;08+advance-button--disabled {
  color: #82918b;
  background: #dfe8dc;
}

.advance-button--purchase {
  background: #ff7a59;
}
</style>
x% 100644 index.vue o&{9B`Exq#3x% 100644 index.vue T	-\+Q*He+Ve;xWQ>"point-records.Y流水,家长、事项和状态查询积分变动N(条7流水AG账号或事项s
date-grid"!(date-field">
            <text>开始日	9startDate" placeholder="YYYY-MM-DD" confirm-type="done" /+(date-field">
            <text>结束日	7endDate" placeholder="YYYY-MM-DD" confirm-type="done" /0tx in list" :key="tx.id" class="lf-card tx-card"`
tx-card__top"!tx5'tx-card__title">{{ tx.titl%@tx-card__meta">{{ tx.userName || tx.userId }} · {{ tx.createdAttx-card__amount">
            <text :class="tx.amount < 0 && 'is-minus'">{{ tx.amount > 0 ? '+' : '' }}{{ tx.amount }}</text>
 	{{ statusLabel(tx.status) }}?tx-card__id">{{ tx.userId }}NgetAdminPointTransactionU8status: 'all',
        startDate: '',
        endDate: 'PfGa0流水slPstatus = 'all'
      this.filters.startDate = ''
      this.filters.endDate = ''m!VqTransactionq流水加载"m9PstatusLabel(status) {
      return status === 'pending' ? '待到账' : '可用'6point-records,B	tx-card {%datea%9w'%!date-field text8	bottom: 8&:&	3date-field input {
  height: 68rpx;
  padding: 0 18! 414background: #fbfcf7(!%C'-^!Btx-card__top {
  display: flex"+20rpx;
}

.tx#"tx-card__titlO##29!((1.4;
}

.tx#H}

.tx-card__amount$g28rpx;
}

.tx-card__amount$42!(&Qtx-card__amount text:first-child.is-minus {
  color: #ff7a59;
}

.tx-card__amount&!#&0rpx;
}

.tx-card__id {padding: 12rpx 16%\A"/%,(n\Cx% 100644 index.vue oY2ys4$'O6Tx|QY#dmin-points.Y管理'账户池、补发冲正、流水筛选N)manager.overview.transactions || 0 }} 笔7查看积分管理。</view>
      <u-button color="#0b4f46" shape="circle" text="切换账号" @click="confirmLogout"></u-button(8points-metricslmetric in metrics" :key="metricmetricmetric$metricKexpiry-card"`
lf-row"a	 P*lf-section-title">积分过期提醒</viewA.lf-caption">{{ manager.expiryRule?.description<expiry-badge">{{ manager.expiryRule?.validDays || 180 }} 天-,expiry-note">{{ manager.expiryRule?.reminder}9anager.expirySchedules" :key="item.id" class="expiry-row"a	 P1expiry-row__name">{{ item.userName || item.userId$expiry-row__meta">{{ item.sourcexpiry-row__rightM#
 }} 积分	
expireAt }}0\+lf-row"a	 Plf-section-title">家长KHlf-caption">仅展示前 5 个账户，筛选查询进入完整账户页	%
Kghost-button" @click="go('/pages/admin-point-accounts/index')">查看更多q 8v-if="!previewAccounts.length" class="empty-mini">暂无"}previewAccountsaccount-row"a	 ProwhErow9-' }} · 可用 {{ item.points.available }} · 待到账 	'row__
A</button>
  
Y\H*lf-row"a	 P*lf-section-title">最近积分流水</viewAElf-caption">仅展示最新 5 条，筛选查询进入完整流水页	%
Jghost-button" @click="go('/pages/admin-point-records/index')">查看更多q Hv-if="!previewTransactions.length" class="empty-mini">暂无积分流水}6tx in previewTransactions" :key="tx.id" class="tx-row"a	 Ptx-row__title">{{ tx.titl%?tx-row__meta">{{ tx.userName || tx.userId }} · {{ tx.createdAttx-row__amount">
            <text :class="tx.amount < 0 && 'is-minus'">{{ tx.amount > 0 ? '+' : '' }}{{ tx.amount }}</text>
  	{{ statusLabel(tx.status) }}0\Manager/clearStoredAccount,Jconst emptyManager = {
  overview: {},
  accounts: [],
  transactions: [],
  expiryRule: {},
  expirySchedules: [],
  rewards: [],
  rules: [],
}ymanager: emptyManagerr,const overview = this.manager.overview || {}"可用积分池', value: {@>overview.expir长账户', value: ;]
    },
    previewTransactions() {
      return (this.manager.transactions || []).slice(0, 5)
    },
    previewAccounts() {
6      return (this.manager.accounts || []).slice(0, 5)) this.loadGload"(wasync load() {
%Manager()
      this.manager = res.data || res || this.manager
    },
    statusLabel(status) {
      return status === 'pending' ? '待到账' : '可用'loadLogout() {
title: '确认退出登录',
        content: '退出后需要重新登录当前账号，是否继续？',
        confirmText: '退出',
        confirmColor: '#b54732',
        success: (res) => {
          if (!res.confirm) return
          clearStoredAccount()
    =8  },(  go(url) {
      uni.navigateTo({ url }dmin-points,C50rpx;
}

.denied-card,
.account-card,
.filter-card,
.expiry-card {%points-metric0!.p'dDmetric-card {
  min-height: 138rpx;
  padding: 24rpx;
}

.metric-carq&(
metric-car&=4]$40!(&"expiry-badge {
  min-width: 86rpx; %U 0b2f2a;
  background: #f5c451% '  text-align: center;
}

.expiry-note {padding: 18%d &"  line-height: 1.5;
}

.expiry-row-".  padding: 22rpx 0;
  border-bottom: 1rpx solid #edf1e9;
}

.expiry-row:last-child {
  border-bottom: 0;
}

.expiry-row__name {R# 27!(&
expiry-row#=1rpx;
}

.expiry-row__right$I(min-width: 210rpx;
}

.expiry-row__right$ff7a59;
  font-size: 26!(&expiry-row__right&!#&0rpx;
}

.account-row-"(6rpx;
  padding: 24rpx 0;
  border-bottom: 1rpx solid #edf1e9;
}

.account-row:last-child {
  border-bottom: 0;
}

.account-rowJ#(&account-row#drow__actions$0Ngap: 10rpx;
}

.account-row__actions button {
  min-width: 94rpx;
  height: 46%U '-1!(&account-row__\(.0ghost-button {
  min-width: 132rpx;
  height: 52! b9d8d2B %l
q M)2rpx;
}

.empty-mini {
  padding: 28rpx 0&'(X%. '!	background: #fbfcf75Nchip-row$
wrap: wrap%916rpx;
}

.filter-chip {
  height: 52rpx;
  padding: 0 18! +'	filter %order-color: #9ae9dc }

.tx-row {
  display: flex"2  padding: 24rpx 0;
  border-bottom: 1rpx solid #edf1e9;
}

.tx-row:last-child {
  border-bottom: 0;
}

.tx-row__title {
  coloY#27!(&tx-row#C}

.tx-row__amount$I%min-width: 110rpx;
}

.tx-row__amount$3&Otx-row__amount text:first-child.is-minus {
  color: #ff7a59;
}

.tx-row__amount&!#&0rpx;
}
</style>
Sx% 100644 index.vue Y:)wӛI;Z~Jx.+<template>
  <view class="lf-page admin-profile-page">
    <view class="lf-nav">
      <view>
        <view class="lf-nav__title">管理账号</view>
        <view class="lf-nav__sub">角色权限与账号安全</view>
      </view>
      <view class="lf-pill">{{ account.roleName || '-' }}</view>
    </view>

    <view v-if="!isAdmin" class="lf-card denied-card">
      <view class="denied-card__title">当前账号没有管理权限</view>
      <view class="lf-caption">请切换到管理账号后查看。</view>
      <u-button color="#0b4f46" shape="circle" text="切换账号" @click="confirmLogout"></u-button>
    </view>

    <template v-else>
      <view class="profile-card lf-card lf-card--ink">
        <view class="profile-card__top">
          <view class="profile-card__avatar">{{ (account.name || '-').slice(0, 1) }}</view>
          <view>
            <view class="profile-card__name">{{ account.name }}</view>
            <view class="profile-card__phone">{{ account.phone || '未绑定手机号' }}</view>
          </view>
        </view>
        <view class="permission-strip">
          <text v-for="permission in account.permissions || []" :key="permission">{{ permission }}</text>
        </view>
      </view>

      <view class="lf-card action-card">
        <view class="lf-section-title">账号安全</view>
        <view class="entry-row" @click="togglePasswordPanel">
          <view>
            <text>{{ account.hasPassword ? '修改密码' : '设置密码' }}</text>
            <text>{{ account.hasPassword ? '用于账号密码登录' : '设置后可使用账号密码登录' }}</text>
          </view>
          <u-icon name="arrow-right" color="#0b4f46" size="22"></u-icon>
        </view>
        <view v-if="passwordPanel" class="security-panel">
          <view v-if="account.hasPassword" class="form-field">
            <text>原密码</text>
            <input v-model="passwordForm.oldPassword" password maxlength="32" placeholder="请输入原密码" />
          </view>
          <view class="form-field">
            <text>新密码</text>
            <input v-model="passwordForm.password" password maxlength="32" placeholder="至少 6 位" />
          </view>
          <view class="form-field">
            <text>确认密码</text>
            <input v-model="passwordForm.confirmPassword" password maxlength="32" placeholder="请再次输入新密码" />
          </view>
          <button class="save-button" :disabled="saving" @click="savePassword">
            {{ saving ? '保存中' : account.hasPassword ? '修改密码' : '设置密码' }}
          </button>
        </view>

        <view class="entry-row" @click="togglePhonePanel">
          <view>
            <text>换绑手机号</text>
            <text>当前：{{ account.phone || '未绑定' }}</text>
          </view>
          <u-icon name="arrow-right" color="#0b4f46" size="22"></u-icon>
        </view>
        <view v-if="phonePanel" class="security-panel">
          <view class="form-field">
            <text>新手机号</text>
            <input v-model="phoneForm.phone" type="number" maxlength="11" placeholder="请输入新手机号" />
          </view>
          <view class="form-field form-field--code">
            <view class="code-input">
              <text>验证码</text>
              <input v-model="phoneForm.code" type="number" maxlength="6" placeholder="请输入验证码" />
            </view>
            <button class="code-button" :disabled="phoneCountdown > 0" @click="requestPhoneCode">
              {{ phoneCountdown > 0 ? `${phoneCountdown}s` : '获取验证码' }}
            </button>
          </view>
          <button class="save-button" :disabled="saving" @click="savePhone">
            {{ saving ? '保存中' : '确认换绑' }}
          </button>
        </view>

        <view class="entry-row entry-row--danger" @click="confirmLogout">
          <view>
            <text>退出登录</text>
            <text>退出后需要重新登录当前账号</text>
          </view>
          <u-icon name="arrow-right" color="#b54732" size="22"></u-icon>
        </view>
      </view>
    </template>

    <AdminTabbar v-if="isAdmin" active="profile" />
    <u-toast ref="toast"></u-toast>
  </view>
</template>

<script>
import AdminTabbar from '../../components/AdminTabbar.vue'
import { sendSmsCode, updateBoundPhone, updatePassword } from '../../services/educationApi'
import { clearStoredAccount, getStoredAccount, isAdminAccount, refreshStoredAccount, setStoredAccount } from '../../services/accountSession'

function createPasswordForm() {
  return {
    oldPassword: '',
    password: '',
    confirmPassword: '',
  }
}

export default {
  components: { AdminTabbar },
  data() {
    return {
      account: getStoredAccount() || {},
      saving: false,
      passwordPanel: false,
      phonePanel: false,
      phoneCountdown: 0,
      phoneTimer: null,
      passwordForm: createPasswordForm(),
      phoneForm: {
        phone: '',
        code: '',
      },
    }
  },
  computed: {
    isAdmin() {
      return isAdminAccount(this.account)
    },
  },
  async onShow() {
    const account = await refreshStoredAccount()
    if (!account) {
      uni.reLaunch({ url: '/pages/login/index' })
      return
    }
    this.account = account
  },
  async onPullDownRefresh() {
    await this.onShow()
    uni.stopPullDownRefresh()
  },
  onUnload() {
    if (this.phoneTimer) clearInterval(this.phoneTimer)
  },
  methods: {
    togglePasswordPanel() {
      this.passwordPanel = !this.passwordPanel
      this.passwordForm = createPasswordForm()
      if (this.passwordPanel) this.phonePanel = false
    },
    togglePhonePanel() {
      this.phonePanel = !this.phonePanel
      this.phoneForm = { phone: '', code: '' }
      if (this.phonePanel) this.passwordPanel = false
    },
    async requestPhoneCode() {
      if (!/^1\d{10}$/.test(this.phoneForm.phone)) {
        this.$refs.toast.show({ message: '请先输入正确手机号', type: 'error' })
        return
      }
      const res = await sendSmsCode(this.phoneForm.phone, 'update_phone')
      if (res.ok === false) {
        this.$refs.toast.show({ message: res.message || '验证码发送失败', type: 'error' })
        return
      }
      const data = res.data || res
      this.startPhoneCountdown(data.cooldown || 60)
      this.$refs.toast.show({ message: res.message || data.message || '验证码已发送', type: 'success' })
    },
    startPhoneCountdown(seconds = 60) {
      if (this.phoneTimer) clearInterval(this.phoneTimer)
      this.phoneCountdown = Number(seconds || 60)
      this.phoneTimer = setInterval(() => {
        this.phoneCountdown -= 1
        if (this.phoneCountdown <= 0) {
          clearInterval(this.phoneTimer)
          this.phoneTimer = null
        }
      }, 1000)
    },
    async savePassword() {
      if (this.account.hasPassword && !this.passwordForm.oldPassword) {
        this.$refs.toast.show({ message: '请输入原密码', type: 'error' })
        return
      }
      if (this.passwordForm.password.length < 6) {
        this.$refs.toast.show({ message: '密码至少需要 6 位', type: 'error' })
        return
      }
      if (this.passwordForm.password !== this.passwordForm.confirmPassword) {
        this.$refs.toast.show({ message: '两次输入的密码不一致', type: 'error' })
        return
      }
      this.saving = true
      try {
        const res = await updatePassword(this.account.id, this.passwordForm)
        if (res.ok === false) {
          this.$refs.toast.show({ message: res.message || '密码保存失败', type: 'error' })
          return
        }
        this.applyAccount(res.data || res)
        this.passwordPanel = false
        this.passwordForm = createPasswordForm()
        this.$refs.toast.show({ message: '密码已更新', type: 'success' })
      } finally {
        this.saving = false
      }
    },
    async savePhone() {
      if (!/^1\d{10}$/.test(this.phoneForm.phone)) {
        this.$refs.toast.show({ message: '请输入正确手机号', type: 'error' })
        return
      }
      if (!this.phoneForm.code) {
        this.$refs.toast.show({ message: '请输入验证码', type: 'error' })
        return
      }
      this.saving = true
      try {
        const res = await updateBoundPhone(this.account.id, this.phoneForm)
        if (res.ok === false) {
          this.$refs.toast.show({ message: res.message || '换绑失败', type: 'error' })
          return
        }
        this.applyAccount(res.data || res)
        this.phonePanel = false
        this.phoneForm = { phone: '', code: '' }
        this.$refs.toast.show({ message: '手机号已换绑', type: 'success' })
      } finally {
        this.saving = false
      }
    },
    applyAccount(account) {
      this.account = account
      setStoredAccount(account)
    },
    confirmLogout() {
      uni.showModal({
        title: '确认退出登录',
        content: '退出后需要重新登录当前账号，是否继续？',
        confirmText: '退出',
        confirmColor: '#b54732',
        success: (res) => {
          if (!res.confirm) return
          clearStoredAccount()
          uni.reLaunch({ url: '/pages/login/index' })
        },
      })
    },
  },
}
</script>

<style lang="scss" scoped>
.admin-profile-page {
  padding-bottom: 150rpx;
}

.denied-card,
.action-card {
  margin-top: 22rpx;
  padding: 28rpx;
}

.denied-card__title {
  margin-bottom: 12rpx;
  color: #17312d;
  font-size: 34rpx;
  font-weight: 900;
}

.profile-card {
  margin-top: 18rpx;
  padding: 32rpx;
}

.profile-card__top {
  display: flex;
  align-items: center;
  gap: 22rpx;
}

.profile-card__avatar {
  display: grid;
  place-items: center;
  width: 92rpx;
  height: 92rpx;
  border-radius: 24rpx;
  color: #0b2f2a;
  background: #f5c451;
  font-size: 42rpx;
  font-weight: 900;
}

.profile-card__name {
  color: #ffffff;
  font-size: 36rpx;
  font-weight: 900;
}

.profile-card__phone {
  margin-top: 8rpx;
  color: rgba(255, 255, 255, 0.68);
  font-size: 24rpx;
}

.permission-strip {
  display: flex;
  flex-wrap: wrap;
  gap: 12rpx;
  margin-top: 28rpx;
}

.permission-strip text {
  padding: 8rpx 14rpx;
  border-radius: 999rpx;
  color: #dff8ee;
  background: rgba(255, 255, 255, 0.1);
  font-size: 20rpx;
  font-weight: 800;
}

.entry-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 18rpx;
  padding: 26rpx 0;
  border-bottom: 1rpx solid #edf1e9;
}

.entry-row:last-child {
  border-bottom: 0;
}

.entry-row--danger text:first-child {
  color: #b54732;
}

.entry-row text {
  display: block;
}

.entry-row text:first-child {
  color: #17312d;
  font-size: 28rpx;
  font-weight: 900;
}

.entry-row text:last-child {
  margin-top: 8rpx;
  color: #6f7c78;
  font-size: 22rpx;
  line-height: 1.45;
}

.security-panel {
  margin-top: 20rpx;
  padding: 22rpx;
  border: 1rpx solid #dfe9dc;
  border-radius: 16rpx;
  background: #fbfdf8;
}

.form-field {
  margin-top: 18rpx;
}

.form-field:first-child {
  margin-top: 0;
}

.form-field text {
  display: block;
  margin-bottom: 10rpx;
  color: #6f7c78;
  font-size: 22rpx;
  font-weight: 900;
}

.form-field input {
  height: 78rpx;
  padding: 0 22rpx;
  border: 1rpx solid #dfe9dc;
  border-radius: 14rpx;
  color: #17312d;
  background: #f8faf5;
  font-size: 26rpx;
  line-height: 78rpx;
}

.form-field--code {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 190rpx;
  align-items: end;
  gap: 16rpx;
}

.code-button,
.save-button {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 78rpx;
  border-radius: 16rpx;
  font-size: 26rpx;
  font-weight: 900;
  line-height: normal;
}

.code-button {
  color: #0b4f46;
  border: 1rpx solid #b9d8d2;
  background: #dff8ee;
}

.code-button[disabled] {
  color: #7d8b86;
  background: #eef3ec;
}

.save-button {
  margin-top: 24rpx;
  color: #ffffff;
  background: #0b4f46;
}
</style>
X5x% 100644 index.vue U	.ǪeiVVS+a](x+<template>
  <view class="lf-page admin-page">
    <view class="lf-nav">
      <view>
        <view class="lf-nav__title">增长管理台</view>
        <view class="lf-nav__sub">线索、积分、风控统一看</view>
      </view>
      <view class="lf-pill">{{ account.roleName }}</view>
    </view>

    <view v-if="!isAdmin" class="lf-card denied-card">
      <view class="denied-card__title">当前账号没有管理权限</view>
      <view class="lf-caption">请使用校区管理员、顾问、老师、财务或超级管理员账号登录后访问。</view>
      <u-button color="#0b4f46" shape="circle" text="切换账号" @click="confirmLogout"></u-button>
    </view>

    <template v-else>
      <view class="metrics-grid">
        <view v-for="metric in metrics" :key="metric.label" class="metric-card lf-card">
          <text>{{ metric.label }}</text>
          <text>{{ metric.value }}</text>
        </view>
      </view>

      <view class="lf-card ops-card">
        <view class="lf-row">
          <view class="lf-section-title">转化漏斗</view>
          <text class="lf-caption">按当前账号权限展示</text>
        </view>
        <view v-for="item in admin.funnel" :key="item.label" class="ops-bar">
          <view class="ops-bar__head">
            <text>{{ item.label }}</text>
            <text>{{ item.value }}</text>
          </view>
          <view class="ops-bar__track">
            <view class="ops-bar__fill" :style="{ width: `${Math.max(12, item.value / admin.funnel[0].value * 100)}%` }"></view>
          </view>
        </view>
      </view>

      <view class="lf-card leads-card">
        <view class="lf-row">
          <view>
            <view class="lf-section-title">待处理线索</view>
            <view class="lf-caption">首页仅展示最新 3 条</view>
          </view>
          <button class="ghost-button" @click="goAdminPage('/pages/admin-leads/index')">全部线索</button>
        </view>
        <view v-for="lead in previewLeads" :key="lead.id" class="lead-row">
          <view>
            <view class="lead-row__name">{{ lead.parent }}</view>
            <view class="lead-row__meta">来源 {{ lead.source }}</view>
            <view class="lead-row__risk">{{ lead.channel || lead.source }}</view>
          </view>
          <view class="lead-row__stage">
            <u-tag :text="lead.stage" plain shape="circle" color="#0b4f46" borderColor="#b9d8d2"></u-tag>
            <button :class="[lead.stage === '已报名' && 'is-disabled', nextStage(lead) === '已报名' && 'is-purchase']" @click="advance(lead)">
              {{ actionLabel(lead) }}
            </button>
          </view>
        </view>
      </view>

      <view class="lf-card point-hub">
        <view class="point-hub__main">
          <view class="lf-cube-mark">分</view>
          <view>
            <view class="lf-section-title">积分管理</view>
            <view class="lf-caption">补发、冲正、流水筛选在独立页面处理</view>
          </view>
        </view>
        <view class="point-hub__stats">
          <view>
            <text>{{ admin.overview.pointsCost || 0 }}</text>
            <text>累计发放</text>
          </view>
          <view>
            <text>{{ admin.overview.attended || 0 }}</text>
            <text>到课奖励</text>
          </view>
        </view>
        <button class="primary-row-button" @click="goAdminPage('/pages/admin-points/index')">进入积分管理</button>
      </view>

      <view class="lf-card course-hub">
        <view class="point-hub__main">
          <view class="lf-cube-mark">课</view>
          <view>
            <view class="lf-section-title">试听课程管理</view>
            <view class="lf-caption">维护可预约试听课、席位和到课积分</view>
          </view>
        </view>
        <button class="primary-row-button" @click="goAdminPage('/pages/admin-courses/index')">进入试听管理</button>
      </view>

    </template>

    <AdminTabbar v-if="isAdmin" active="home" />

    <u-toast ref="toast"></u-toast>
  </view>
</template>

<script>
import AdminTabbar from '../../components/AdminTabbar.vue'
import { confirmLeadPurchase, getAdminDashboard, updateLeadStage } from '../../services/educationApi'
import { clearStoredAccount, getStoredAccount, isAdminAccount } from '../../services/accountSession'

const stageFlow = ['待跟进', '已注册', '已预约', '已到课', '已报名']

export default {
  components: { AdminTabbar },
  data() {
    return {
      account: getStoredAccount() || {},
      admin: { overview: { leads: 0, attended: 0, purchased: 0, pointsCost: 0 }, funnel: [], leads: [] },
    }
  },
  computed: {
    isAdmin() {
      return isAdminAccount(this.account)
    },
    metrics() {
      const overview = this.admin.overview
      return [
        { label: '推荐线索', value: overview.leads },
        { label: '确认到课', value: overview.attended },
        { label: '报名成交', value: overview.purchased },
        { label: '已发放积分', value: overview.pointsCost },
      ]
    },
    previewLeads() {
      return (this.admin.leads || []).slice(0, 3)
    },
  },
  onShow() {
    const account = getStoredAccount()
    if (!account) {
      uni.reLaunch({ url: '/pages/login/index' })
      return
    }
    this.account = account
    if (this.isAdmin) this.load()
  },
  async onPullDownRefresh() {
    if (this.isAdmin) await this.load()
    uni.stopPullDownRefresh()
  },
  methods: {
    async load() {
      const res = await getAdminDashboard()
      this.admin = res.data || res || this.admin
    },
    nextStage(lead) {
      const index = stageFlow.indexOf(lead.stage)
      return stageFlow[Math.min(index + 1, stageFlow.length - 1)] || '已注册'
    },
    actionLabel(lead) {
      if (lead.stage === '已报名') return '已报名'
      return this.nextStage(lead) === '已报名' ? '确认报名' : '推进'
    },
    async advance(lead) {
      const next = this.nextStage(lead)
      if (lead.stage === next) {
        this.$refs.toast.show({ message: '线索已到最终阶段', type: 'default' })
        return
      }
      const isPurchase = next === '已报名'
      const confirmed = await this.confirmAction(
        isPurchase ? '确认报名正价课' : '确认推进线索',
        isPurchase
          ? `确认「${lead.parent}」已报名正价课？确认后会发放报名阶段推荐奖励，并更新推荐漏斗。`
          : `将「${lead.parent}」从「${lead.stage}」推进到「${next}」，会影响转化统计。`,
      )
      if (!confirmed) return
      const res = isPurchase ? await confirmLeadPurchase(lead.id) : await updateLeadStage(lead.id, next)
      if (res.ok === false) {
        this.$refs.toast.show({ message: res.message || '线索更新失败', type: 'error' })
        return
      }
      lead.stage = next
      this.$refs.toast.show({ message: isPurchase ? '已确认报名并发放奖励' : `已推进到${next}`, type: 'success' })
    },
    confirmLogout() {
      uni.showModal({
        title: '确认退出登录',
        content: '退出后需要重新登录当前账号，是否继续？',
        confirmText: '退出',
        confirmColor: '#b54732',
        success: (res) => {
          if (!res.confirm) return
          clearStoredAccount()
          uni.reLaunch({ url: '/pages/login/index' })
        },
      })
    },
    go(url) {
      uni.navigateTo({ url })
    },
    goAdminPage(url) {
      uni.redirectTo({ url })
    },
    confirmAction(title, content) {
      return new Promise((resolve) => {
        uni.showModal({
          title,
          content,
          confirmText: '确认',
          cancelText: '取消',
          confirmColor: '#0b4f46',
          success: (res) => resolve(Boolean(res.confirm)),
          fail: () => resolve(false),
        })
      })
    },
  },
}
</script>

<style lang="scss" scoped>
.admin-page {
  padding-bottom: 150rpx;
}

.denied-card,
.ops-card,
.leads-card {
  margin-top: 22rpx;
  padding: 28rpx;
}

.denied-card__title {
  margin-bottom: 12rpx;
  color: #17312d;
  font-size: 34rpx;
  font-weight: 900;
}

.denied-card .u-button {
  margin-top: 28rpx;
}

.metrics-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 16rpx;
  margin-top: 18rpx;
}

.metric-card {
  min-height: 150rpx;
  padding: 24rpx;
}

.metric-card text:first-child {
  color: #6f7c78;
  font-size: 22rpx;
  font-weight: 800;
}

.metric-card text:last-child {
  display: block;
  margin-top: 16rpx;
  color: #0b4f46;
  font-size: 44rpx;
  font-weight: 900;
}

.ops-bar {
  margin-top: 22rpx;
}

.ops-bar__head {
  display: flex;
  justify-content: space-between;
  margin-bottom: 10rpx;
  color: #17312d;
  font-size: 24rpx;
  font-weight: 800;
}

.ops-bar__track {
  height: 16rpx;
  overflow: hidden;
  border-radius: 999rpx;
  background: #e2ebde;
}

.ops-bar__fill {
  height: 100%;
  border-radius: inherit;
  background: linear-gradient(90deg, #17bebb, #ff7a59);
}

.lead-row {
  display: flex;
  justify-content: space-between;
  gap: 18rpx;
  padding: 26rpx 0;
  border-bottom: 1rpx solid #edf1e9;
}

.lead-row:last-child {
  border-bottom: 0;
}

.lead-row__name {
  color: #17312d;
  font-size: 28rpx;
  font-weight: 900;
}

.lead-row__meta {
  margin-top: 8rpx;
  color: #6f7c78;
  font-size: 22rpx;
}

.lead-row__risk {
  display: inline-flex;
  margin-top: 12rpx;
  padding: 6rpx 12rpx;
  border-radius: 999rpx;
  color: #0b4f46;
  background: #dff8ee;
  font-size: 20rpx;
  font-weight: 800;
}

.lead-row__stage {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 12rpx;
}

.lead-row__stage button {
  min-width: 92rpx;
  height: 48rpx;
  border-radius: 999rpx;
  color: #ffffff;
  background: #0b4f46;
  font-size: 22rpx;
  font-weight: 800;
}

.lead-row__stage button.is-disabled {
  color: #82918b;
  background: #dfe8dc;
}

.lead-row__stage button.is-purchase {
  min-width: 132rpx;
  background: #ff7a59;
}

.ghost-button {
  min-width: 136rpx;
  height: 52rpx;
  border: 1rpx solid #b9d8d2;
  border-radius: 999rpx;
  color: #0b4f46;
  background: #ffffff;
  font-size: 22rpx;
  font-weight: 900;
}

.point-hub,
.course-hub {
  margin-top: 22rpx;
  padding: 28rpx;
}

.point-hub__main {
  display: flex;
  align-items: center;
  gap: 20rpx;
}

.point-hub__stats {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 16rpx;
  margin-top: 24rpx;
}

.point-hub__stats view {
  padding: 20rpx;
  border-radius: 14rpx;
  background: #f3f8ef;
}

.point-hub__stats text {
  display: block;
}

.point-hub__stats text:first-child {
  color: #0b4f46;
  font-size: 34rpx;
  font-weight: 900;
}

.point-hub__stats text:last-child {
  margin-top: 6rpx;
  color: #6f7c78;
  font-size: 21rpx;
  font-weight: 800;
}

.primary-row-button {
  width: 100%;
  height: 76rpx;
  margin-top: 24rpx;
  border-radius: 16rpx;
  color: #ffffff;
  background: #0b4f46;
  font-size: 26rpx;
  font-weight: 900;
}

</style>
5åx% 100644 index.vue cY#fꊍhw75rxi/<template>
  <view class="lf-page login-page">
    <view class="lf-nav">
      <view>
        <view class="lf-nav__title">账号登录</view>
        <view class="lf-nav__sub">进入立方和教育推广积分系统</view>
      </view>
      <view class="lf-cube-mark">和</view>
    </view>

    <view class="lf-card login-card">
      <view class="login-card__head">
        <view>
          <view class="lf-section-title">{{ wechatPending ? '绑定手机号' : '选择登录方式' }}</view>
          <view class="login-card__hint">
            {{ wechatPending ? '微信登录后需要绑定手机号才能继续使用' : '管理员和家长会按账号角色进入不同界面' }}
          </view>
        </view>
      </view>

      <view v-if="!wechatPending" class="mode-tabs">
        <view :class="['mode-tab', loginMode === 'phone' && 'mode-tab--active']" @click="loginMode = 'phone'">
          手机号登录
        </view>
        <view :class="['mode-tab', loginMode === 'password' && 'mode-tab--active']" @click="loginMode = 'password'">
          账号密码
        </view>
      </view>

      <template v-if="loginMode === 'password' && !wechatPending">
        <view class="form-field">
          <text>账号</text>
          <input v-model="passwordForm.account" maxlength="40" placeholder="请输入账号 / 手机号" confirm-type="next" />
        </view>
        <view class="form-field">
          <text>密码</text>
          <input v-model="passwordForm.password" password maxlength="32" placeholder="请输入密码" confirm-type="done" @confirm="submit" />
        </view>
      </template>

      <template v-else>
        <view class="form-field">
          <text>手机号</text>
          <input v-model="form.phone" type="number" maxlength="11" placeholder="请输入手机号" confirm-type="next" />
        </view>
        <view class="form-field form-field--code">
          <view class="code-input">
            <text>验证码</text>
            <input v-model="form.code" type="number" maxlength="6" placeholder="请输入验证码" confirm-type="done" @confirm="submit" />
          </view>
          <button class="code-button" :disabled="countdown > 0" @click="requestCode">
            {{ countdown > 0 ? `${countdown}s` : '获取验证码' }}
          </button>
        </view>
        <view v-if="wechatPending" class="form-field">
          <text>姓名</text>
          <input v-model="form.name" maxlength="20" placeholder="请输入家长姓名" confirm-type="done" />
        </view>
        <view class="form-field">
          <text>邀请码（选填）</text>
          <input v-model="form.inviterCode" maxlength="20" placeholder="新用户可填写邀请码领取积分" confirm-type="done" />
        </view>
      </template>

      <button class="login-button" :class="loading && 'login-button--loading'" @click="submit">
        {{ submitText }}
      </button>

      <!-- #ifdef MP-WEIXIN -->
      <view v-if="!wechatPending" class="login-divider"><text>或</text></view>
      <button v-if="!wechatPending" class="wechat-button" :disabled="loading" @click="loginByWechat">
        <u-icon name="weixin-fill" color="#ffffff" size="20"></u-icon>
        <text>微信登录</text>
      </button>
      <!-- #endif -->

      <view class="login-note">验证码将通过短信发送，请在有效期内完成登录或绑定。</view>
    </view>

    <u-toast ref="toast"></u-toast>
  </view>
</template>

<script>
import { bindWechatPhone, loginWithPassword, loginWithPhone, loginWithWechat, sendSmsCode } from '../../services/educationApi'
import { isAdminAccount, setStoredAccount } from '../../services/accountSession'

export default {
  data() {
    return {
      loading: false,
      countdown: 0,
      timer: null,
      wechatPending: false,
      loginMode: 'phone',
      form: {
        phone: '',
        code: '',
        name: '',
        inviterCode: '',
      },
      passwordForm: {
        account: '',
        password: '',
      },
    }
  },
  computed: {
    submitText() {
      if (this.loading) return '处理中'
      if (this.wechatPending) return '绑定并登录'
      return this.loginMode === 'password' ? '账号密码登录' : '手机号登录'
    },
  },
  onLoad(query = {}) {
    const inviterCode = this.resolveInviteCode(query)
    if (inviterCode) this.form.inviterCode = inviterCode
  },
  onUnload() {
    if (this.timer) clearInterval(this.timer)
  },
  methods: {
    async requestCode() {
      if (!/^1\d{10}$/.test(this.form.phone)) {
        this.$refs.toast.show({ message: '请先输入正确手机号', type: 'error' })
        return
      }
      const purpose = this.wechatPending ? 'bind_wechat' : 'login'
      const res = await sendSmsCode(this.form.phone, purpose)
      if (res.ok === false) {
        this.$refs.toast.show({ message: res.message || '验证码发送失败', type: 'error' })
        return
      }
      const data = res.data || res
      this.startCountdown(data.cooldown || 60)
      this.$refs.toast.show({ message: res.message || data.message || '验证码已发送', type: 'success' })
    },
    startCountdown(seconds = 60) {
      if (this.timer) clearInterval(this.timer)
      this.countdown = Number(seconds || 60)
      this.timer = setInterval(() => {
        this.countdown -= 1
        if (this.countdown <= 0) {
          clearInterval(this.timer)
          this.timer = null
        }
      }, 1000)
    },
    async submit() {
      if (this.wechatPending) {
        await this.submitPhoneForm(() => this.bindWechatPhone())
        return
      }
      if (this.loginMode === 'password') {
        await this.loginByPassword()
        return
      }
      await this.submitPhoneForm(() => this.loginByPhone())
    },
    async submitPhoneForm(handler) {
      if (!/^1\d{10}$/.test(this.form.phone)) {
        this.$refs.toast.show({ message: '请输入正确手机号', type: 'error' })
        return
      }
      if (!this.form.code) {
        this.$refs.toast.show({ message: '请输入验证码', type: 'error' })
        return
      }
      await handler()
    },
    async loginByPassword() {
      if (!this.passwordForm.account.trim() || !this.passwordForm.password) {
        this.$refs.toast.show({ message: '请输入账号和密码', type: 'error' })
        return
      }
      this.loading = true
      try {
        const res = await loginWithPassword(this.passwordForm.account, this.passwordForm.password)
        if (res.ok === false) {
          this.$refs.toast.show({ message: res.message || '登录失败', type: 'error' })
          return
        }
        this.enterSystem(res.data || res)
      } finally {
        this.loading = false
      }
    },
    async loginByPhone() {
      this.loading = true
      try {
        const res = await loginWithPhone(this.form.phone, this.form.code, this.form.inviterCode)
        if (res.ok === false) {
          this.$refs.toast.show({ message: res.message || '登录失败', type: 'error' })
          return
        }
        this.enterSystem(res.data || res)
      } finally {
        this.loading = false
      }
    },
    async loginByWechat() {
      this.loading = true
      try {
        const wechatCode = await this.getWechatCode()
        const res = await loginWithWechat(wechatCode)
        if (res.ok === false) {
          this.$refs.toast.show({ message: res.message || '微信登录失败', type: 'error' })
          return
        }
        const data = res.data || res
        if (data.needPhoneBinding) {
          this.wechatPending = true
          this.loginMode = 'phone'
          this.$refs.toast.show({ message: '请绑定手机号后继续', type: 'default' })
          return
        }
        this.enterSystem(data.account)
      } catch (error) {
        this.$refs.toast.show({ message: error.message || '微信登录失败', type: 'error' })
      } finally {
        this.loading = false
      }
    },
    async bindWechatPhone() {
      this.loading = true
      try {
        const wechatCode = await this.getWechatCode()
        const res = await bindWechatPhone({
          wechatCode,
          phone: this.form.phone,
          code: this.form.code,
          name: this.form.name,
          inviterCode: this.form.inviterCode,
        })
        if (res.ok === false) {
          this.$refs.toast.show({ message: res.message || '绑定失败', type: 'error' })
          return
        }
        this.enterSystem(res.data || res)
      } catch (error) {
        this.$refs.toast.show({ message: error.message || '绑定失败', type: 'error' })
      } finally {
        this.loading = false
      }
    },
    getWechatCode() {
      return new Promise((resolve, reject) => {
        uni.login({
          provider: 'weixin',
          success: (result) => {
            if (result.code) resolve(result.code)
            else reject(new Error('未获取到微信登录 code'))
          },
          fail: () => reject(new Error('微信授权失败')),
        })
      })
    },
    enterSystem(account) {
      setStoredAccount(account)
      this.$refs.toast.show({ message: `已登录：${account.name}`, type: 'success' })
      const target = isAdminAccount(account) ? '/pages/admin/index' : '/pages/index/index'
      setTimeout(() => {
        uni.reLaunch({ url: target })
      }, 500)
    },
    resolveInviteCode(query = {}) {
      if (query.inviteCode) return decodeURIComponent(query.inviteCode)
      if (query.code) return decodeURIComponent(query.code)
      if (!query.scene) return ''
      const scene = decodeURIComponent(query.scene)
      const pairs = scene.split('&').map((item) => item.split('='))
      const match = pairs.find(([key]) => ['invite', 'inviteCode', 'code'].includes(key))
      return match?.[1] || ''
    },
  },
}
</script>

<style lang="scss" scoped>
.login-card {
  margin-top: 18rpx;
  padding: 30rpx;
}

.login-card__head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 20rpx;
}

.login-card__hint {
  margin-top: 8rpx;
  color: #6f7c78;
  font-size: 22rpx;
  line-height: 1.45;
}

.mode-tabs {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 8rpx;
  margin-top: 24rpx;
  padding: 8rpx;
  border: 1rpx solid #dfe9dc;
  border-radius: 18rpx;
  background: #f8faf5;
}

.mode-tab {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 64rpx;
  border-radius: 14rpx;
  color: #6f7c78;
  font-size: 24rpx;
  font-weight: 900;
  line-height: normal;
}

.mode-tab--active {
  color: #ffffff;
  background: #0b4f46;
  box-shadow: 0 10rpx 22rpx rgba(11, 79, 70, 0.18);
}

.form-field {
  margin-top: 24rpx;
}

.form-field text {
  display: block;
  margin-bottom: 10rpx;
  color: #6f7c78;
  font-size: 22rpx;
  font-weight: 900;
}

.form-field input {
  height: 84rpx;
  padding: 0 24rpx;
  border: 1rpx solid #dfe9dc;
  border-radius: 16rpx;
  color: #17312d;
  background: #f8faf5;
  font-size: 28rpx;
  line-height: 84rpx;
}

.form-field--code {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 190rpx;
  align-items: end;
  gap: 16rpx;
}

.code-button,
.login-button,
.wechat-button {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 84rpx;
  border-radius: 18rpx;
  font-size: 26rpx;
  font-weight: 900;
  line-height: normal;
}

.code-button {
  color: #0b4f46;
  border: 1rpx solid #b9d8d2;
  background: #dff8ee;
}

.code-button[disabled] {
  color: #7d8b86;
  background: #eef3ec;
}

.login-button {
  margin-top: 30rpx;
  color: #ffffff;
  background: #0b4f46;
}

.login-button--loading {
  opacity: 0.72;
}

.login-divider {
  display: flex;
  align-items: center;
  gap: 18rpx;
  margin: 28rpx 0;
  color: #8a9691;
  font-size: 22rpx;
}

.login-divider::before,
.login-divider::after {
  flex: 1;
  height: 1rpx;
  content: "";
  background: #e2e9df;
}

.wechat-button {
  gap: 10rpx;
  color: #ffffff;
  background: #1aad19;
}

.wechat-button text {
  color: #ffffff;
}

.login-note {
  margin-top: 18rpx;
  color: #6f7c78;
  font-size: 22rpx;
  line-height: 1.5;
}
</style>
wxf^0@campaign-page lf-page--light">
    <view class="campaign-hero">
  <view class="campaign-hero__label">来自 {{ inviterCode }} 的成长邀请</view>
      <view class="campaign-hero__title">取试听积分，给孩子选一节真正适配的课</view>
      <view class="campaign-hero__sub">注册、预约、到课N后双方都获得积分。积分可兑换体验课、测评、课程抵扣。.form-card">
领取新家长积分</view>
Bform.parentName" class="form-input" placeholder="家长姓名" />
Hform.phone" class="form-input" placeholder="手机号" type="number" />
}row">
?class="form-input code-row__input" placeholder="短信验证码6" />
a^B	Aform.childName" class="form-input" placeholder="孩子姓名" />
form.grade" class="form-input" placeholder="孩子年级" />
      <u-button color="#0b4f46" shape="circle" text="立即领取$预约" @click="submit"></u-button Wbenefit-strip">
      <view>
        <text>20</text>
        <text>注册积分</text>
A<view>
        <text>50</text>
        <text>预约积分</text>
H<view>
        <text>100</text>
        <text>到课积分</text>
      aLReferral<8hinviterCode: ''/arentName: '',.childName: '',
        gradeMonLoad(query) {
    this.inviterCode = query?.code || query?.inviteCode || ''3'referral_register'{AF!this.inviterCode.推荐链接无效 gparentName.trim(2填写家长姓名 W!短信:`8Referral({ ...this.form, inviterCode: this.inviterCode }Z取 "`#1if (data.invitee) setStoredAccount(data.invitee)
(注册成功，积分已到账'#f$5uni.navigateTo({ url: '/pages/courses/index' }), 800)m&6campaign-page {
  min-height: 100vh;
  padding: calc(48rpx + var(--status-bar-height)) 28rpx 48rpx;
}

.campaign-hero {
  paddiOng-top: 32rpx;
}

.campaign-hero__label {
  margin-top: 34rpx;
  color: #ff7a59)*Q}

.campaign-hero__title {
  margin-top: 20rpx;
  color: #0b2f2a;
  font-size: 54A*/)1.16;
}

.campaign-hero__sub {
  margin-top: 22rpx;
  color: #56645f;
  font-size: 26rpx;
  line-height: 1.7;
}

.form-card {
  margin-top: 36&/form-input {
  height: 88rpx;
  margin: 22rpx 0*F(*C8rpx;
}

.code-row'-`+&+
-c6rpx;
  margin: 22rpx 0;
}

.code-row__input {
  width: auto;
  min-width: 0;
  margin: 0;
}

.code+X"8rpx;
  margin: 0;
  padding: 0 12*,(0b4f46;
  background: #dff8ee)Ey,,:benefit-strip'43, 1fr);
  gap: 16rpx;
  margin-top: 22rpx;
}

.benefit-strip view {
  min-height: 136rpx;
  padding: 22rpx 12rpx;
  border-radius: 16rpx;
  text-align: center;
  background: #0b2f2a;
}

.benefit-strip text:first-child {
  display: block;
  color: #f5c451;
  font-size: 38A*bbenefit-strip text:last-child {
  color: rgba(255, 255, 255, 0.7);
  font-size: 21rpx;
}
</style>
@Ax% 100644 index.vue X<piQ\=H"x"<template>
  <view class="lf-page list-page">
    <view class="lf-nav">
      <view>
        <view class="lf-nav__title">可约课程</view>
        <view class="lf-nav__sub">搜索课程、老师和校区，滚动加载更多</view>
      </view>
      <view class="lf-pill">{{ total }} 门</view>
    </view>

    <view class="lf-card filter-card">
      <view class="search-box">
        <u-icon name="search" color="#6f7c78" size="22"></u-icon>
        <input v-model="filters.keyword" placeholder="搜索课程、老师、校区" confirm-type="search" @confirm="refresh" />
      </view>
      <button class="query-button" @click="refresh">查询</button>
      <view class="quota-summary">试听额度 {{ trialBookingLimit }} 次 · 已使用 {{ activeAppointments.length }} 次 · 剩余 {{ remainingBookings }} 次</view>
    </view>

    <view v-for="course in list" :key="course.id" class="lf-card course-card">
      <view class="course-card__top">
        <view>
          <view class="course-card__name">{{ course.name }}</view>
          <view class="course-card__meta">{{ course.nextTime }} · {{ course.campus || '-' }} · {{ course.teacher || '-' }}</view>
        </view>
        <view class="course-card__points">+{{ course.pointsBack || 0 }}</view>
      </view>
      <view class="course-card__tags">
        <text v-for="tag in course.tags" :key="tag">{{ tag }}</text>
      </view>
      <u-button
        :color="isCourseUnavailable(course) ? '#b7c6bf' : '#0b4f46'"
        :disabled="isCourseUnavailable(course)"
        shape="circle"
        :text="courseButtonText(course)"
        @click="book(course)"
      ></u-button>
    </view>

    <view class="load-state">{{ loadText }}</view>
    <u-toast ref="toast"></u-toast>
  </view>
</template>

<script>
import { bookCourse, getCoursesPage, getMyAppointments } from '../../services/educationApi'
import { getStoredAccount, refreshStoredAccount } from '../../services/accountSession'

export default {
  data() {
    return {
      account: null,
      list: [],
      appointments: [],
      total: 0,
      loading: false,
      finished: false,
      filters: {
        keyword: '',
        pageNum: 1,
        pageSize: 10,
      },
    }
  },
  computed: {
    loadText() {
      if (this.loading) return '加载中...'
      if (this.finished) return this.list.length ? '没有更多了' : '暂无课程'
      return '上拉加载更多'
    },
    activeAppointments() {
      return this.appointments.filter((item) => item.status !== 'cancelled')
    },
    trialBookingLimit() {
      const limit = Number(this.account?.trialBookingLimit ?? 1)
      return Number.isInteger(limit) && limit >= 0 ? limit : 1
    },
    remainingBookings() {
      return Math.max(0, this.trialBookingLimit - this.activeAppointments.length)
    },
    hasReachedLimit() {
      return this.remainingBookings <= 0
    },
    bookedCourseIds() {
      return this.activeAppointments.map((item) => item.courseId)
    },
  },
  async onShow() {
    await this.refresh()
  },
  async onPullDownRefresh() {
    await this.refresh()
    uni.stopPullDownRefresh()
  },
  onReachBottom() {
    this.loadMore()
  },
  methods: {
    async refresh() {
      const account = await refreshStoredAccount()
      if (account?.id) this.account = account
      this.filters.pageNum = 1
      this.finished = false
      await this.load(true)
      await this.loadAppointments()
    },
    async loadMore() {
      if (this.loading || this.finished) return
      this.filters.pageNum += 1
      await this.load(false)
    },
    async load(reset) {
      this.loading = true
      try {
        const res = await getCoursesPage(this.filters)
        if (res.ok === false) {
          this.$refs.toast.show({ message: res.message || '课程加载失败', type: 'error' })
          return
        }
        const data = res.data || res
        this.total = data.total || 0
        this.list = reset ? (data.list || []) : this.list.concat(data.list || [])
        this.finished = this.list.length >= this.total
      } finally {
        this.loading = false
      }
    },
    async loadAppointments() {
      const user = this.account || getStoredAccount()
      if (!user?.id) {
        this.appointments = []
        return
      }
      const res = await getMyAppointments(user.id)
      if (res.ok !== false) this.appointments = res.data || res || []
    },
    async book(course) {
      if (this.isBookedCourse(course)) {
        this.$refs.toast.show({ message: '该试听课程已经预约', type: 'default' })
        return
      }
      if (this.hasReachedLimit) {
        this.$refs.toast.show({ message: this.trialBookingLimit ? `当前账号最多可预约 ${this.trialBookingLimit} 次试听课` : '当前账号暂不可预约试听课', type: 'default' })
        return
      }
      const user = getStoredAccount()
      if (!user) {
        uni.reLaunch({ url: '/pages/login/index' })
        return
      }
      const confirmed = await this.confirmAction('确认预约试听', `预约「${course.name}」${course.nextTime} 的试听课？`)
      if (!confirmed) return
      const res = await bookCourse({ userId: user.id, childId: user.child?.id || '', courseId: course.id })
      if (res.ok === false) {
        this.$refs.toast.show({ message: res.message || '预约失败', type: 'error' })
        return
      }
      this.$refs.toast.show({ message: '预约成功', type: 'success' })
      await this.refresh()
    },
    isBookedCourse(course) {
      return this.bookedCourseIds.includes(course.id)
    },
    isCourseUnavailable(course) {
      return this.isBookedCourse(course) || this.hasReachedLimit
    },
    courseButtonText(course) {
      if (this.isBookedCourse(course)) return '已预约'
      if (this.hasReachedLimit) return this.trialBookingLimit ? '试听次数已用完' : '暂不可预约'
      return '预约试听'
    },
    confirmAction(title, content) {
      return new Promise((resolve) => {
        uni.showModal({ title, content, confirmText: '确认', cancelText: '取消', confirmColor: '#0b4f46', success: (res) => resolve(Boolean(res.confirm)), fail: () => resolve(false) })
      })
    },
  },
}
</script>

<style lang="scss" scoped>
.filter-card,
.course-card {
  margin-top: 18rpx;
  padding: 28rpx;
}

.search-box {
  display: flex;
  align-items: center;
  gap: 12rpx;
  height: 74rpx;
  padding: 0 22rpx;
  border-radius: 16rpx;
  background: #f3f8ef;
}

.search-box input {
  flex: 1;
  color: #17312d;
  font-size: 26rpx;
}

.query-button {
  width: 100%;
  height: 68rpx;
  margin-top: 18rpx;
  border-radius: 16rpx;
  color: #ffffff;
  background: #0b4f46;
  font-size: 25rpx;
  font-weight: 900;
}

.quota-summary {
  margin-top: 18rpx;
  color: #49635d;
  font-size: 22rpx;
  line-height: 1.5;
  text-align: center;
}

.course-card__top {
  display: flex;
  justify-content: space-between;
  gap: 18rpx;
}

.course-card__name {
  color: #17312d;
  font-size: 31rpx;
  font-weight: 900;
}

.course-card__meta {
  margin-top: 8rpx;
  color: #6f7c78;
  font-size: 22rpx;
  line-height: 1.45;
}

.course-card__points {
  flex: 0 0 auto;
  min-width: 94rpx;
  height: 54rpx;
  border-radius: 14rpx;
  color: #0b2f2a;
  background: #f5c451;
  font-size: 24rpx;
  font-weight: 900;
  line-height: 54rpx;
  text-align: center;
}

.course-card__tags {
  display: flex;
  flex-wrap: wrap;
  gap: 12rpx;
  margin: 18rpx 0;
}

.course-card__tags text {
  padding: 8rpx 14rpx;
  border-radius: 999rpx;
  color: #0b4f46;
  background: #dff8ee;
  font-size: 20rpx;
  font-weight: 800;
}

.load-state {
  padding: 28rpx 0 80rpx;
  color: #6f7c78;
  font-size: 23rpx;
  text-align: center;
}
</style>
x% 100644 index.vue j7M
LfFwx}xI"(\程0 课后触发双方积分到账"_lf-pill">试听%alf-card my-bookingK我的0caption">可预约 {{ trialBookingLimit }} 次，已使用 {{ activeAppointments.length }} 次，剩余 {{ remainingBookings }} 次/pill">{{|appointments[0].status) }}$:	aO	#booking-row">
booking-row__name">{{ 程'booking+
j
U
campusK
teacherq
?booking-row__badge">{{|m }}:
card timelineK本周可约Mlf-caption">首页展示前 3 条 · 剩余 {{ remainingBookings }} 次试听0?xmorencourses-list&preview!%head-pR9time">{{ course.nextTime }} · 余 {{ course.seats }} 席`8 points">+{{ course.pointsBack? RU9  <u-button
          :color="isCourseUnavailable(course) ? '#b7c6bf' : '#0b4f46'"
          :disabled="isCourseUnavailable(course)"
          shape="circle"
          :text="courseButtonText(course)"
          @click="book(course)"
        ></u-button>
:
card confirmK课程兑换说明</viewQconfirm-grid">
:<text>1</text>
          <text>积分兑换课程券</text/=<text>2</text>
          <text>顾问确认适配班型</text/<text>3</text>
后自动核销</text/     </view>
L+{ bookCourse, getCourses, getMyAppointments/getStoredAccount, refreshStoredCV%null1active0%status !== 'cancelled')
    },
    trialBookingLimit() {
      const limit = Number(this.account?.trialBookingLimit ?? 1)
     U return Number.isInteger(limit) && limit >= 0 ? limit : 1
    },
    remainingBookingMath.max(0, this.trialBookingLimit - this.activeAppointments.length)
    },
    hasReachedLimit() {
      return this.remaining'Bookings <= 0
    },
    bookedCourseIdctiveAppointmentq!ourseId)
    },
    previewCourse6courses.slice(0, 3)
    },
  },
  async onShow() {
   0%1[const user = await refreshStoredAccount()
      if (!user?.id) {
        this.account = null
        this.appointments = []
   '9&!account = user
      const [res, appointmentRes] = await Promise.all([getCourses(), getMyAppointments(user.id)])
      if (res. I课程加载!<%
2>if (appointmentR <-appointmentRes.message || '预约记录加载!<appointments = appointmentRes.data || appointmentRes || []
    },
    async book(course) {
      if (this.isBookedCourse(course) --'该试听课程已经预约', type: 'default!"if (this.hasReachedLimit -this.trialBookingLimit ? `当前账号最多可预约 ${this.trialBookingLimit} 次试听课` : '当前账号暂不可预约试听课', type: 'default!"S;预约试听',
        `预约「${course.name}」${course.nextTime} 的试听课，到课后会触发积分到账。`,
      )K *userL  if (!user) {
'<&!bookCourse({
        userId: user.id,
        childId: user.child?.id || '',
        courseId: course.id,
      })
      if (re K预约!Y预约成功，待到课确讓/&  this.load()
    },
    isBookedCourse(course) {
      return this.bookedCourseIds.includes(course.id)
    },
    isCourseUnavailable(course) {
      return this.isBookedCourse(course) || this.hasReachedLimit
    },
    courseButtonText(course) {
      if (this.isBookedCourse(course)) return '已预约'
      if (this.hasReachedLimit) return this.trialBookingLimit ? '试听次<已用完' : '暂不可预约'
      return '预约试听'q9labels = {
       '
        attended: '已到课',
       >,
      }
      return labels[status] || status || '已预约''Atimeline-card,
.my-booking-card,
.confirm-card {
  margin-top: 18J+booking-)	4<&2:O+25!8"booking.,30,+booking-row__meta97 5&booking-row__badge {
  flex: 0 0 auto;6R50+Qcourse-card {
  margin-top: 24rpx;
  padding: 26rpx;
  border: 1rpx solid #e4ecdf:1bfcf7;
}

.more-P0,I4head.)	4520@4~4':%3ime9"/93@40points {
  min-width: 94rpx;
  padding: 12rpx 14512}*text-align: center*',+f6'2	: 22rpx 0`66X5+Z7	nfirm-grin7.s:4!2324rpx;
}

.confirm-grid view {
  min-height: 150rpx7;(eef5e9;
}

.confirm-grid text:first-chiln71place-items: center;
  width: 42rpx;
  height: 42512rpx;;*05+Aconfirm-grid text:last-child {
  display: block;
  margin-top: 16+$5	_-8845;
}
</style>
cxx% 100644 index.vue ITj9V}kV؃x,^<template>
  <view class="lf-page home-page">
    <view class="lf-nav">
      <view>
        <view class="lf-nav__title">立方和教育</view>
        <view class="lf-nav__sub">{{ isAdmin ? '账号权限决定管理入口' : '推荐、到课、积分兑换一条线' }}</view>
      </view>
      <view class="lf-cube-mark">和</view>
    </view>

    <view v-if="isAdmin" class="hero lf-card lf-card--ink">
      <view class="hero__eyebrow">{{ currentAccount.roleName }}</view>
      <view class="hero__title">欢迎回来，{{ currentAccount.name }}</view>
      <view class="hero__sub">今日线索 {{ admin.overview.leads }} · 到课 {{ admin.overview.attended }} · 已发放积分 {{ admin.overview.pointsCost }}</view>
      <view class="admin-grid">
        <view class="admin-grid__cell">
          <text>报名成交</text>
          <text>{{ admin.overview.purchased }}</text>
        </view>
        <view class="admin-grid__cell">
          <text>营收</text>
          <text>¥{{ admin.overview.revenue }}</text>
        </view>
      </view>
      <view class="hero__actions">
        <u-button color="#f5c451" shape="circle" text="移动管理台" @click="go('/pages/admin/index')"></u-button>
      </view>
    </view>

    <view v-else class="hero lf-card lf-card--ink">
      <view class="hero__top">
        <view>
          <view class="hero__eyebrow">成长账本</view>
          <view class="hero__title">{{ profile.name }}</view>
          <view class="hero__sub">{{ profile.child.name }} · {{ profile.child.grade }} · {{ profile.child.focus }}</view>
        </view>
        <view class="hero__points">
          <text>{{ pointAccount.available }}</text>
          <text>可用积分</text>
        </view>
      </view>
      <view class="hero__point-grid">
        <view>
          <text>{{ pointAccount.pending }}</text>
          <text>待到账</text>
        </view>
        <view>
          <text>{{ pointAccount.expiring }}</text>
          <text>将过期</text>
        </view>
        <view>
          <text>{{ pointAccount.frozen }}</text>
          <text>冻结</text>
        </view>
      </view>
      <view class="hero__track">
        <view class="hero__track-node">分享</view>
        <view class="hero__track-line"></view>
        <view class="hero__track-node">预约</view>
        <view class="hero__track-line"></view>
        <view class="hero__track-node">到课</view>
        <view class="hero__track-line"></view>
        <view class="hero__track-node hero__track-node--hot">兑换</view>
      </view>
    </view>

    <view v-if="!isAdmin" class="quick-grid">
      <view class="quick-card lf-card" @click="go('/pages/referral/index')">
        <u-icon name="share-square" color="#0b4f46" size="28"></u-icon>
        <text>生成推荐</text>
        <text>朋友圈 / 微信好友 / 海报</text>
      </view>
      <view class="quick-card lf-card" @click="go('/pages/points/index')">
        <u-icon name="coupon" color="#ff7a59" size="28"></u-icon>
        <text>积分兑换</text>
        <text>{{ pointAccount.pending }} 待到账</text>
      </view>
      <view class="quick-card lf-card" @click="go('/pages/courses/index')">
        <u-icon name="calendar" color="#17bebb" size="28"></u-icon>
        <text>预约试听</text>
        <text>{{ courses[0]?.nextTime || '本周可约' }}</text>
      </view>
    </view>

    <view v-if="!isAdmin" class="lf-card funnel-card">
      <view class="lf-row">
        <view class="lf-section-title">我的推荐漏斗</view>
        <view class="lf-pill">转化 {{ stats.conversion }}%</view>
      </view>
      <view class="funnel-row">
        <view v-for="item in funnel" :key="item.label" class="funnel-step">
          <text>{{ item.value }}</text>
          <text>{{ item.label }}</text>
        </view>
      </view>
    </view>

    <view v-else class="lf-card funnel-card">
      <view class="lf-row">
        <view class="lf-section-title">全校区转化链路</view>
        <view class="lf-pill">按账号权限过滤</view>
      </view>
      <view class="bar-list">
        <view v-for="item in admin.funnel" :key="item.label" class="bar-item">
          <view class="bar-item__head">
            <text>{{ item.label }}</text>
            <text>{{ item.value }}</text>
          </view>
          <view class="bar-item__track">
            <view class="bar-item__fill" :style="{ width: `${Math.max(12, item.value / admin.funnel[0].value * 100)}%` }"></view>
          </view>
        </view>
      </view>
    </view>
  </view>
</template>

<script>
import { getBootstrap } from '../../services/educationApi'
import { getStoredAccount, isAdminAccount, refreshStoredAccount, setStoredAccount } from '../../services/accountSession'

const emptyPoints = { available: 0, pending: 0, frozen: 0, expiring: 0 }
const emptyProfile = { name: '', child: { name: '', grade: '', focus: '' }, points: emptyPoints }
const emptyStats = { invited: 0, registered: 0, booked: 0, attended: 0, purchased: 0, conversion: 0 }
const emptyAdmin = { overview: { leads: 0, attended: 0, purchased: 0, pointsCost: 0, revenue: 0 }, funnel: [], leads: [] }

function toNumber(value) {
  const number = Number(value)
  return Number.isFinite(number) ? number : 0
}

function normalizePoints(points = {}) {
  return {
    available: toNumber(points.available),
    pending: toNumber(points.pending),
    frozen: toNumber(points.frozen),
    expiring: toNumber(points.expiring),
  }
}

function normalizeStats(stats = {}) {
  return {
    invited: toNumber(stats.invited),
    registered: toNumber(stats.registered),
    booked: toNumber(stats.booked),
    attended: toNumber(stats.attended),
    purchased: toNumber(stats.purchased),
    conversion: toNumber(stats.conversion),
  }
}

export default {
  data() {
    return {
      currentAccount: getStoredAccount(),
      profile: emptyProfile,
      stats: emptyStats,
      courses: [],
      admin: emptyAdmin,
    }
  },
  computed: {
    isAdmin() {
      return isAdminAccount(this.currentAccount)
    },
    pointAccount() {
      return normalizePoints(this.profile.points)
    },
    funnel() {
      return [
        { label: '邀请', value: this.stats.invited },
        { label: '注册', value: this.stats.registered },
        { label: '预约', value: this.stats.booked },
        { label: '到课', value: this.stats.attended },
        { label: '报名', value: this.stats.purchased },
      ]
    },
  },
  async onShow() {
    this.currentAccount = await refreshStoredAccount()
    if (!this.currentAccount) {
      uni.reLaunch({ url: '/pages/login/index' })
      return
    }
    if (this.isAdmin) {
      uni.reLaunch({ url: '/pages/admin/index' })
      return
    }
    await this.load()
  },
  async onPullDownRefresh() {
    await this.load()
    uni.stopPullDownRefresh()
  },
  methods: {
    async load() {
      const res = await getBootstrap(this.currentAccount.id)
      if (res.ok === false) return
      const data = res.data || res
      const nextProfile = data.profile || {}
      this.currentAccount = setStoredAccount(nextProfile)
      const nextPoints = normalizePoints(data.points || nextProfile.points || this.profile.points)
      this.profile = {
        ...emptyProfile,
        ...nextProfile,
        child: { ...emptyProfile.child, ...(nextProfile.child || {}) },
        points: nextPoints,
      }
      this.stats = normalizeStats(data.referralStats || data.stats || this.stats)
      this.courses = data.courses || this.courses
      this.admin = data.admin || this.admin
    },
    go(url) {
      uni.navigateTo({ url })
    },
  },
}
</script>

<style lang="scss" scoped>
.home-page {
  padding-bottom: 80rpx;
}

.hero {
  margin-top: 18rpx;
  padding: 32rpx;
}

.hero__top {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 24rpx;
}

.hero__eyebrow {
  color: #9ae9dc;
  font-size: 22rpx;
  font-weight: 800;
}

.hero__title {
  margin-top: 10rpx;
  font-size: 42rpx;
  font-weight: 900;
  line-height: 1.22;
}

.hero__sub {
  margin-top: 12rpx;
  color: rgba(255, 255, 255, 0.72);
  font-size: 24rpx;
  line-height: 1.55;
}

.hero__points {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
}

.hero__points text:first-child {
  color: #f5c451;
  font-size: 52rpx;
  font-weight: 900;
}

.hero__points text:last-child {
  color: rgba(255, 255, 255, 0.68);
  font-size: 22rpx;
}

.hero__point-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 12rpx;
  margin-top: 26rpx;
}

.hero__point-grid view {
  padding: 16rpx 12rpx;
  border: 1rpx solid rgba(255, 255, 255, 0.12);
  border-radius: 14rpx;
  background: rgba(255, 255, 255, 0.08);
  text-align: center;
}

.hero__point-grid text {
  display: block;
}

.hero__point-grid text:first-child {
  color: #ffffff;
  font-size: 28rpx;
  font-weight: 900;
}

.hero__point-grid text:last-child {
  margin-top: 4rpx;
  color: rgba(255, 255, 255, 0.64);
  font-size: 20rpx;
  font-weight: 800;
}

.hero__track {
  display: flex;
  align-items: center;
  margin-top: 34rpx;
}

.hero__track-node {
  min-width: 82rpx;
  padding: 14rpx 12rpx;
  border: 1rpx solid rgba(255, 255, 255, 0.18);
  border-radius: 12rpx;
  color: #ffffff;
  text-align: center;
  font-size: 22rpx;
  font-weight: 800;
  background: rgba(255, 255, 255, 0.08);
}

.hero__track-node--hot {
  color: #0b2f2a;
  background: #f5c451;
}

.hero__track-line {
  flex: 1;
  height: 2rpx;
  background: rgba(255, 255, 255, 0.22);
}

.hero__actions {
  display: block;
  margin-top: 28rpx;
}

.quick-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 16rpx;
  margin-top: 22rpx;
}

.quick-card {
  min-height: 190rpx;
  padding: 22rpx 18rpx;
}

.quick-card text:nth-child(2) {
  display: block;
  margin-top: 18rpx;
  color: #17312d;
  font-size: 26rpx;
  font-weight: 900;
}

.quick-card text:nth-child(3) {
  display: block;
  margin-top: 8rpx;
  color: #6f7c78;
  font-size: 20rpx;
  line-height: 1.4;
}

.funnel-card {
  margin-top: 22rpx;
  padding: 28rpx;
}

.funnel-row {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 10rpx;
  margin-top: 28rpx;
}

.funnel-step {
  min-height: 118rpx;
  padding: 16rpx 8rpx;
  border-radius: 12rpx;
  background: #eef5e9;
  text-align: center;
}

.funnel-step text:first-child {
  display: block;
  color: #0b4f46;
  font-size: 34rpx;
  font-weight: 900;
}

.funnel-step text:last-child {
  color: #6f7c78;
  font-size: 20rpx;
}

.admin-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 16rpx;
  margin-top: 28rpx;
}

.admin-grid__cell {
  padding: 20rpx;
  border-radius: 12rpx;
  background: rgba(255, 255, 255, 0.08);
}

.admin-grid__cell text {
  display: block;
}

.admin-grid__cell text:first-child {
  color: rgba(255, 255, 255, 0.62);
  font-size: 22rpx;
}

.admin-grid__cell text:last-child {
  margin-top: 8rpx;
  font-size: 34rpx;
  font-weight: 900;
}

.bar-list {
  margin-top: 24rpx;
}

.bar-item {
  margin-bottom: 20rpx;
}

.bar-item__head {
  display: flex;
  justify-content: space-between;
  margin-bottom: 10rpx;
  color: #17312d;
  font-size: 24rpx;
  font-weight: 800;
}

.bar-item__track {
  height: 16rpx;
  overflow: hidden;
  border-radius: 999rpx;
  background: #e2ebde;
}

.bar-item__fill {
  height: 100%;
  border-radius: inherit;
  background: linear-gradient(90deg, #17bebb, #ff7a59);
}
</style>
x% 100644 index.vue {9)iIokPzI?x% 100644 index.vue S/<i$XXμӳ\gFx	tQ."records.S;{{ mode === 'redemptions' ? '兑换记录' : '积分流水'!分页查询，滚动加载更多N(条7k
Jmode-row">
        <button :class="mode === 'transactions' && 'is-active'" @click="switchMode('transactions')">积分流水</buubutton :class="mode === 'redemptions' && 'is-active'" @click="switchMode('redemptions')">兑换记录</button>
      </view>
  rB4事项、券码或权益名称4</view>
buttonX'CQid" class="lf-card record-card">
      <template v-if="mode === 'transactions'">
	-record-card__name">{{ item.titl-$record-card__meta">{{ item.createdAtrecord-card__right">
          <strong :class="item.amount < 0 && 'is-minus'">{{ item.amount > 0 ? '+' : '' }}{{ item.amount }}4</strong>
          <text>{{ statusText(item.status)
#"template>
      <template v-else>
	-6record-card__name">{{ item.reward?.name || item.itemId-Arecord-card__meta">{{ item.createdAt }} · {{ item.costPoints }} U    <view v-if="item.voucherCode" class="record-card__meta">券码 {{ item.voucherCodrecord-card__right">
	statusLabel || item.status
#template=q.FV{ POINT_STATUS } from '../../config'
import { getPointRedemptions, getPointTransactionBrefreshStoredC3mode: 'transactions'p(UsEE0记录4Load(query = {}) {
    if (query.mode === 'redemptions') this.mode = 'redemptions'
  },
  async onShow() {
    await refreshStoredAccount()
   %w(witchMode(mode) {
      this.mode = modem!@statusText(status) {
      return POINT_STATUS[status] || statusJk5const user = getStoredAccount()
      if (!user) {
  =8
Qloader = this.mode === 'redemptions' ? getPointRedemptions : getPointTransactionsloader(user.id, i记录加载"m7394(data.list || []) : this.list.concat(data.list || []dy,filter-card,
.record-card {mode-row1!-p'72rpx;
}

.mode-row button,
.query-button {
  height: 64%! (&mode-row button {aA"(mode-row button.is-active,
.query-buttonp"0Vquery-button {
  width: 100%!
record-car""2recordE#-29!(&record#arecord-card__right$I+gap: 10rpx;
}

.record-card__right strong {a!0rpx;
}

.record-card__right strong.is-minus {
  color: #ff7a59;
}

.record-card__right text {
  padding: 8rpx 14rpx;
  border-%U l
 1%!&	( 80&%('fx% 100644 index.vue ޘ+[6&+0rAz r^Nx.)|推荐记录*3按家长、手机号、渠道和状态查询</view#.g家长、手机号、渠道\Cchip-row">
        <button v-for="item in statusOptions" :key="item.value" :class="['filter-chip', filters.status === item.valuce && 'filter-chip--active']" @click="changeStatus(item.value)">
          {{ item.label }}
        [record in list" :key="record)R K	
record.nam@ fmeta">{{ record.phone || '-' }} · {{ record.childGrade || '-' }} · {{ record.channel || '-' }}</view# <text>{{ record#strong>+{{ record.reward }}</strongRngetReferralRecords		  statusOptions: [
        { label: '全部', value: 'all' },
        { label: '已注册', value: '已注册' },
        { label: '已预约', value: '已预约' },
        { label: '已到课', value: '已到课' },
        { label: '已报名', value: '已报名' },
      ]E
推荐
8changeStatus(status#status = status
      this.refresh(/account = getStoredAccount()
      if (!accountG*getReferralRecords(accountAn推荐^?.records || res.records || {}bVE;chip-row {
  display: flex;
  flex-wrap: wrap;
  gap: 12rpxOfilter-chip {
  height: 52rpx;
  padding: 0 18rpx;
  border: 1rpx solid #dce8dc0%
$background: #ffffff;
  font-size: 22?filter-chip--active {
  color: #0b4f46;
  border-color: #9ae9dc[$height: 68rpx'  font-size: 25({30ff7a59;
  font-size: 28rpxt()dyyxn)3$wa&V兑换权益+,索可兑换课程、测评和服务权益>项 权益名称、类型{ock=ockS@ock:ward in list" :key="reward.id" class="lf-card reward-card"O$reward-card__type">{{ reward.type }}$reward-card__name">{{ reward.name }}[reward-card__desc">{{ reward.description || '兑换后由校区审核发放权益券码' }}reward-card__foot">
        <text>{{ reward.cost }} 积分 · 库存 {{ reward.stock }}</text>
        <button :disabled="rewarZd.stock <= 0" @click="redeem(reward)">{{ reward.stock > 0 ? '兑换' : '售罄' }}</buttonaPointRewards, redeemReward$ockS3IockH有库存', value: 'in_stock售罄', value: 'sold_out益A	"	ock(valuen
ockStatus = value
@user!userPointRewards(usero益&X || res8  async redeem(reward) {
      if (reward.stock <= 0) {
)''该权益库存不足', type: 'warning6"
const user!userUconst confirmed = await this.confirmAction('确认兑换权益', `将消耗 ${reward.cost} 积分兑换「${reward.name}」？`l)
      if (!confirmed) return
      const res = await redeemReward({ userId: user.id, itemId: reward.id })
&1兑换,"C.'兑换申请已提交', type: 'success' })
  	
confirmAction(title, content) {
      return new Promise((resolve) => {
        uni.showModal({ title, content, confirmText: '认', cancelText: '取消', confirmColor: '#0b4f46', success: (res) => resolve(Boolean(res.confirm)), fail: () => resolve(false) })
      })Fwa6ward-card__type2#1&ward-card__name {
  margin-top: 10rpx;i!1!ward-card__desc {
  margin-top: 1&5;
}

.reward-card__foo $(?6rpx;
  margin-top: 22rpx;
}

.reward-card__foot text {
  color39ward-card__foot button {
  min-width: 110rpx;
  height: 5`-3Jward-card__foot button[disabled] {
  color: #7a8580;
  background: #dce5ddYt즥x% 100644 index.vue -HYrIrb9b^gx16<template>
  <view class="lf-page points-page">
    <view class="lf-nav">
      <view>
        <view class="lf-nav__title">积分中心</view>
        <view class="lf-nav__sub">积分只能换学习权益，不可提现</view>
      </view>
      <u-icon name="rmb-circle" color="#f5c451" size="34"></u-icon>
    </view>

    <view class="balance-card lf-card lf-card--ink">
      <view class="balance-card__label">可用积分</view>
      <view class="balance-card__amount">{{ account.available }}</view>
      <view class="balance-card__meta">待发放 {{ account.pending }} · 即将过期 {{ account.expiring }}</view>
    </view>

    <view class="lf-card expiry-card">
      <view class="lf-row">
        <view>
          <view class="lf-section-title">积分过期说明</view>
          <view class="lf-caption">{{ expiryRule.description }}</view>
        </view>
        <view class="expiry-card__days">{{ expiryRule.validDays }} 天</view>
      </view>
      <view v-for="item in expirySchedules" :key="item.id" class="expiry-item">
        <view>
          <view class="expiry-item__source">{{ item.source }}</view>
          <view class="expiry-item__time">过期时间 {{ item.expireAt }}</view>
        </view>
        <text>{{ item.points }} 积分</text>
      </view>
    </view>

    <view class="lf-card reward-card">
      <view class="lf-row">
        <view class="lf-section-title">兑换学习权益</view>
        <button class="more-button" @click="go('/pages/point-rewards/index')">查看更多</button>
      </view>
      <view class="reward-grid">
        <view v-for="reward in previewRewards" :key="reward.id" class="reward-item">
          <view class="reward-item__type">{{ reward.type }}</view>
          <view class="reward-item__name">{{ reward.name }}</view>
          <view class="reward-item__desc">{{ reward.description || '兑换后由校区审核发放权益券码' }}</view>
          <view class="reward-item__foot">
            <text>{{ reward.cost }} 积分</text>
            <button :disabled="reward.stock <= 0" @click="redeem(reward)">{{ reward.stock > 0 ? '兑换' : '售罄' }}</button>
          </view>
        </view>
      </view>
    </view>

    <view class="lf-card redemption-card">
      <view class="lf-row">
        <view class="lf-section-title">我的兑换</view>
        <button class="more-button" @click="go('/pages/point-records/index?mode=redemptions')">查看更多</button>
      </view>
      <view v-if="redemptions.length" class="redemption-list">
        <view v-for="item in previewRedemptions" :key="item.id" class="redemption-item">
          <view class="redemption-item__head">
            <view>
              <view class="redemption-item__name">{{ item.reward?.name || item.itemId }}</view>
              <view class="redemption-item__meta">{{ item.createdAt }} · {{ item.costPoints }} 积分</view>
            </view>
            <text :class="['redemption-status', `redemption-status--${item.status}`]">{{ item.statusLabel || redemptionStatusText(item.status) }}</text>
          </view>
          <view v-if="item.voucherCode" class="voucher-box">
            <view>
              <text>权益券码</text>
              <strong>{{ item.voucherCode }}</strong>
            </view>
            <button @click="copyCode(item.voucherCode)">复制</button>
          </view>
          <view v-if="item.expireAt" class="redemption-item__hint">有效期至 {{ item.expireAt }}</view>
          <view v-if="item.rejectedReason" class="redemption-item__hint">驳回原因：{{ item.rejectedReason }}</view>
        </view>
      </view>
      <view v-else class="empty-redemption">暂无兑换申请</view>
    </view>

    <view class="lf-card flow-card">
      <view class="lf-row">
        <view class="lf-section-title">积分流水</view>
        <button class="more-button" @click="go('/pages/point-records/index?mode=transactions')">查看更多</button>
      </view>
      <view v-for="tx in previewTransactions" :key="tx.id" class="tx-row">
        <view>
          <view class="tx-row__title">{{ tx.title }}</view>
          <view class="tx-row__date">{{ tx.createdAt }}</view>
        </view>
        <view class="tx-row__amount">
          <text>{{ tx.amount > 0 ? '+' : '' }}{{ tx.amount }}</text>
          <text>{{ statusText(tx.status) }}</text>
        </view>
      </view>
    </view>

    <u-toast ref="toast"></u-toast>
  </view>
</template>

<script>
import { POINT_STATUS } from '../../config'
import { getPointCenter, redeemReward } from '../../services/educationApi'
import { getStoredAccount, refreshStoredAccount } from '../../services/accountSession'

export default {
  data() {
    return {
      account: { available: 0, pending: 0, frozen: 0, expiring: 0 },
      rewards: [],
      redemptions: [],
      transactions: [],
      expiryRule: { title: '', validDays: 0, description: '', reminder: '' },
      expirySchedules: [],
    }
  },
  computed: {
    previewRewards() {
      return this.rewards.slice(0, 4)
    },
    previewRedemptions() {
      return this.redemptions.slice(0, 3)
    },
    previewTransactions() {
      return this.transactions.slice(0, 3)
    },
  },
  async onShow() {
    await this.load()
  },
  async onPullDownRefresh() {
    await this.load()
    uni.stopPullDownRefresh()
  },
  methods: {
    async load() {
      const user = await refreshStoredAccount()
      if (!user) {
        uni.reLaunch({ url: '/pages/login/index' })
        return
      }
      const res = await getPointCenter(user.id)
      if (res.ok === false) {
        this.$refs.toast.show({ message: res.message || '积分加载失败', type: 'error' })
        return
      }
      const data = res.data || res
      this.account = data.account || this.account
      this.rewards = data.rewards || this.rewards
      this.redemptions = data.redemptions || this.redemptions
      this.transactions = data.transactions || this.transactions
      this.expiryRule = data.expiryRule || this.expiryRule
      this.expirySchedules = data.expirySchedules || this.expirySchedules
    },
    statusText(status) {
      return POINT_STATUS[status] || status
    },
    redemptionStatusText(status) {
      const map = {
        pending_review: '待审核',
        approved: '已发券',
        rejected: '已驳回',
        used: '已核销',
        expired: '已过期',
      }
      return map[status] || status
    },
    copyCode(code) {
      uni.setClipboardData({
        data: code,
        success: () => this.$refs.toast.show({ message: '券码已复制', type: 'success' }),
      })
    },
    go(url) {
      uni.navigateTo({ url })
    },
    async redeem(reward) {
      if (reward.stock <= 0) {
        this.$refs.toast.show({ message: '该权益库存不足', type: 'warning' })
        return
      }
      const confirmed = await this.confirmAction(
        '确认兑换权益',
        `将消耗 ${reward.cost} 积分兑换「${reward.name}」，课程权益提交后需审核。`,
      )
      if (!confirmed) return
      const user = getStoredAccount()
      if (!user) {
        uni.reLaunch({ url: '/pages/login/index' })
        return
      }
      const res = await redeemReward({ userId: user.id, itemId: reward.id })
      if (res.ok === false) {
        this.$refs.toast.show({ message: res.message || '兑换失败', type: 'error' })
        return
      }
      this.$refs.toast.show({ message: '兑换申请已提交', type: 'success' })
      this.load()
    },
    confirmAction(title, content) {
      return new Promise((resolve) => {
        uni.showModal({
          title,
          content,
          confirmText: '确认',
          cancelText: '取消',
          confirmColor: '#0b4f46',
          success: (res) => resolve(Boolean(res.confirm)),
          fail: () => resolve(false),
        })
      })
    },
  },
}
</script>

<style lang="scss" scoped>
.balance-card {
  margin-top: 18rpx;
  padding: 34rpx;
}

.balance-card__label {
  color: #9ae9dc;
  font-size: 22rpx;
  font-weight: 800;
}

.balance-card__amount {
  margin-top: 10rpx;
  color: #f5c451;
  font-size: 72rpx;
  font-weight: 900;
}

.balance-card__meta {
  color: rgba(255, 255, 255, 0.72);
  font-size: 24rpx;
}

.expiry-card,
.reward-card,
.redemption-card,
.flow-card {
  margin-top: 22rpx;
  padding: 28rpx;
}

.expiry-card__days {
  min-width: 86rpx;
  height: 54rpx;
  border-radius: 999rpx;
  color: #0b2f2a;
  background: #f5c451;
  font-size: 22rpx;
  font-weight: 900;
  line-height: 54rpx;
  text-align: center;
}

.expiry-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16rpx;
  margin-top: 18rpx;
  padding: 18rpx;
  border-radius: 14rpx;
  background: #f3f8ef;
}

.expiry-item__source {
  color: #17312d;
  font-size: 24rpx;
  font-weight: 900;
}

.expiry-item__time {
  margin-top: 8rpx;
  color: #6f7c78;
  font-size: 21rpx;
}

.expiry-item > text {
  color: #ff7a59;
  font-size: 24rpx;
  font-weight: 900;
}

.reward-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 18rpx;
  margin-top: 24rpx;
}

.more-button {
  min-width: 132rpx;
  height: 52rpx;
  border: 1rpx solid #b9d8d2;
  border-radius: 999rpx;
  color: #0b4f46;
  background: #ffffff;
  font-size: 22rpx;
  font-weight: 900;
  line-height: 52rpx;
}

.reward-item {
  min-height: 210rpx;
  padding: 22rpx;
  border: 1rpx solid #e4ecdf;
  border-radius: 14rpx;
  background: #fbfcf7;
}

.reward-item__type {
  color: #ff7a59;
  font-size: 20rpx;
  font-weight: 900;
}

.reward-item__name {
  margin-top: 12rpx;
  color: #17312d;
  font-size: 28rpx;
  font-weight: 900;
  line-height: 1.35;
}

.reward-item__desc {
  display: -webkit-box;
  overflow: hidden;
  margin-top: 10rpx;
  color: #6f7c78;
  font-size: 21rpx;
  line-height: 1.45;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
}

.reward-item__foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-top: 24rpx;
}

.reward-item__foot text {
  color: #0b4f46;
  font-size: 22rpx;
  font-weight: 900;
}

.reward-item__foot button {
  min-width: 98rpx;
  height: 48rpx;
  border-radius: 999rpx;
  color: #ffffff;
  background: #0b4f46;
  font-size: 22rpx;
  font-weight: 800;
}

.reward-item__foot button[disabled] {
  color: #7a8580;
  background: #dce5dd;
}

.redemption-list {
  display: grid;
  gap: 18rpx;
  margin-top: 22rpx;
}

.redemption-item {
  padding: 22rpx;
  border: 1rpx solid #e4ecdf;
  border-radius: 14rpx;
  background: #fbfcf7;
}

.redemption-item__head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16rpx;
}

.redemption-item__name {
  color: #17312d;
  font-size: 28rpx;
  font-weight: 900;
  line-height: 1.35;
}

.redemption-item__meta,
.redemption-item__hint {
  margin-top: 8rpx;
  color: #6f7c78;
  font-size: 22rpx;
}

.redemption-status {
  flex-shrink: 0;
  min-width: 104rpx;
  height: 48rpx;
  border-radius: 999rpx;
  color: #0b4f46;
  background: #dff8ee;
  font-size: 22rpx;
  font-weight: 900;
  line-height: 48rpx;
  text-align: center;
}

.redemption-status--pending_review {
  color: #8d5c00;
  background: #fff3cf;
}

.redemption-status--rejected {
  color: #b42318;
  background: #ffe7e4;
}

.redemption-status--used,
.redemption-status--expired {
  color: #56615d;
  background: #edf1e9;
}

.voucher-box {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 18rpx;
  margin-top: 18rpx;
  padding: 18rpx;
  border-radius: 14rpx;
  background: #0b4f46;
}

.voucher-box view {
  display: grid;
  gap: 6rpx;
}

.voucher-box text {
  color: #9ae9dc;
  font-size: 20rpx;
  font-weight: 900;
}

.voucher-box strong {
  color: #f5c451;
  font-size: 28rpx;
  font-weight: 900;
  letter-spacing: 0;
}

.voucher-box button {
  flex-shrink: 0;
  min-width: 92rpx;
  height: 48rpx;
  border-radius: 999rpx;
  color: #0b2f2a;
  background: #f5c451;
  font-size: 22rpx;
  font-weight: 900;
}

.empty-redemption {
  margin-top: 22rpx;
  padding: 28rpx;
  border-radius: 14rpx;
  color: #6f7c78;
  background: #f3f8ef;
  font-size: 24rpx;
  text-align: center;
}

.tx-row {
  display: flex;
  justify-content: space-between;
  padding: 24rpx 0;
  border-bottom: 1rpx solid #edf1e9;
}

.tx-row:last-child {
  border-bottom: 0;
}

.tx-row__title {
  color: #17312d;
  font-size: 28rpx;
  font-weight: 900;
}

.tx-row__date {
  margin-top: 8rpx;
  color: #6f7c78;
  font-size: 22rpx;
}

.tx-row__amount {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
}

.tx-row__amount text:first-child {
  color: #ff7a59;
  font-size: 28rpx;
  font-weight: 900;
}

.tx-row__amount text:last-child {
  margin-top: 8rpx;
  color: #6f7c78;
  font-size: 20rpx;
}
</style>
Tx% 100644 index.vue '3cA}62O\Dv:x% 100644 index.vue Lt|֗3Ef&|ۙɻxK<<template>
  <view class="lf-page referral-page">
    <view class="lf-nav">
      <view>
        <view class="lf-nav__title">推荐有礼</view>
        <view class="lf-nav__sub">好友通过微信卡片进入小程序，新用户填写分享码即可领取积分</view>
      </view>
      <view class="lf-pill">双向积分</view>
    </view>

    <view class="invite-card lf-card lf-card--ink lf-ticket">
      <view class="share-preview">
        <image class="share-preview__image" :src="shareCover" mode="aspectFill" />
        <view class="share-preview__caption">
          <view>
            <view class="share-preview__eyebrow">成长邀请</view>
            <view class="share-preview__title">把好奇心，变成每一次成长</view>
          </view>
          <view class="share-preview__badge">双向积分</view>
        </view>
      </view>
      <view class="invite-card__body">
        <view class="invite-card__label">我的专属分享码</view>
        <view class="invite-card__code">{{ profile.inviterCode || '-' }}</view>
        <view class="invite-card__copy">新家长首次登录或注册时填写该分享码，双方都会获得注册积分。</view>
      </view>
      <view class="invite-card__actions">
        <!-- #ifdef MP-WEIXIN -->
        <button class="share-button share-button--gold" open-type="share" data-channel="微信好友" @click="prepareShare('微信好友')">
          <u-icon name="chat" color="#0b2f2a" size="17"></u-icon>
          <text>发给好友</text>
        </button>
        <!-- #endif -->
        <button class="share-button" @click="copyInviteCode">
          <u-icon name="file-text" color="#ffffff" size="17"></u-icon>
          <text>复制分享码</text>
        </button>
      </view>
    </view>

    <view class="channel-grid">
      <view v-for="channel in channels" :key="channel.name" class="channel-card lf-card" @click="handleChannel(channel)">
        <u-icon :name="channel.icon" :color="channel.color" size="30"></u-icon>
        <text>{{ channel.name }}</text>
        <text>{{ channel.desc }}</text>
      </view>
    </view>

    <view class="lf-card rule-card">
      <view class="lf-row">
        <view class="lf-section-title">积分阶梯</view>
        <text class="lf-caption">推荐者 / 新家长</text>
      </view>
      <view class="rule-list">
        <view v-for="(rule, index) in rules" :key="rule.title" class="rule-item">
          <view class="rule-item__index">{{ index + 1 }}</view>
          <view class="rule-item__body">
            <text>{{ rule.title }}</text>
            <text>推荐者 +{{ rule.inviter }} · 新家长 +{{ rule.invitee }}</text>
          </view>
        </view>
      </view>
    </view>

    <view class="lf-card records-card">
      <view class="lf-row">
        <view class="lf-section-title">推荐进度</view>
        <button class="more-button" @click="go('/pages/referral-records/index')">查看更多</button>
      </view>
      <view v-if="!records.length" class="empty-text">暂无推荐记录</view>
      <view v-for="record in previewRecords" :key="record.id" class="record-row">
        <view>
          <view class="record-row__name">{{ record.name }}</view>
          <view class="record-row__meta">{{ record.childGrade }} · {{ record.channel }}</view>
        </view>
        <view class="record-row__right">
          <u-tag :text="record.status" plain shape="circle" color="#0b4f46" borderColor="#b9d8d2"></u-tag>
          <text>+{{ record.reward }}</text>
        </view>
      </view>
    </view>

    <u-popup :show="qrVisible" mode="center" round="18" @close="qrVisible = false">
      <view class="qr-panel">
        <view class="qr-panel__title">小程序邀请二维码</view>
        <view class="qr-panel__sub">扫码进入小程序登录页，邀请码会自动带入</view>
        <image v-if="qrCode.image" class="qr-panel__image" :src="qrCode.image" mode="aspectFit" @click="previewQrCode" />
        <view v-else class="qr-panel__empty">正在生成二维码</view>
        <view class="qr-panel__code">邀请码 {{ qrCode.inviterCode || profile.inviterCode || '-' }}</view>
        <button class="qr-panel__button" @click="saveQrCode">保存二维码</button>
      </view>
    </u-popup>

    <u-toast ref="toast"></u-toast>
  </view>
</template>

<script>
import { createMiniProgramCode, createShareMaterial, getReferralCenter } from '../../services/educationApi'
import { getStoredAccount, refreshStoredAccount } from '../../services/accountSession'

const emptyStats = { invited: 0, registered: 0, booked: 0, attended: 0, purchased: 0, conversion: 0 }
const shareCover = '/static/share/referral-share-cover.jpg'

export default {
  data() {
    return {
      profile: { id: '', name: '', inviterCode: '' },
      stats: emptyStats,
      rules: [],
      records: [],
      channels: [
        { name: '朋友圈', desc: '使用微信原生分享', icon: 'photo', color: '#ff7a59' },
        { name: '小程序码', desc: '扫码进入登录页', icon: 'scan', color: '#17bebb' },
      ],
      shareCover,
      share: null,
      incomingShareForwarding: false,
      qrVisible: false,
      qrCode: {},
    }
  },
  onLoad(query = {}) {
    if (this.forwardIncomingShare(query)) {
      this.incomingShareForwarding = true
      return
    }
    this.enableShareMenu()
  },
  async onShow() {
    if (this.incomingShareForwarding) return
    this.enableShareMenu()
    await this.load()
  },
  async onPullDownRefresh() {
    await this.load()
    uni.stopPullDownRefresh()
  },
  onShareAppMessage(event) {
    const channel = event?.target?.dataset?.channel || '微信好友'
    return this.buildSharePayload(channel)
  },
  onShareTimeline() {
    const inviteCode = this.profile.inviterCode || ''
    return {
      title: this.share?.title || `${this.profile.name || '立方和教育'}邀请你领取试听积分`,
      query: `inviteCode=${encodeURIComponent(inviteCode)}&source=timeline`,
      imageUrl: shareCover,
    }
  },
  computed: {
    previewRecords() {
      return this.records.slice(0, 3)
    },
  },
  methods: {
    enableShareMenu() {
      if (uni.showShareMenu) {
        uni.showShareMenu({
          withShareTicket: true,
          menus: ['shareAppMessage', 'shareTimeline'],
        })
      }
    },
    forwardIncomingShare(query = {}) {
      const inviteCode = decodeURIComponent(String(query.inviteCode || '')).trim()
      if (!inviteCode) return false
      const account = getStoredAccount()
      if (account?.inviterCode === inviteCode) return false
      uni.reLaunch({
        url: `/pages/login/index?inviteCode=${encodeURIComponent(inviteCode)}`,
      })
      return true
    },
    async load() {
      const account = await refreshStoredAccount()
      if (!account) {
        uni.reLaunch({ url: '/pages/login/index' })
        return
      }
      this.profile = { ...this.profile, ...account }
      const res = await getReferralCenter(account.id)
      if (res.ok === false) {
        this.$refs.toast.show({ message: res.message || '推荐数据加载失败', type: 'error' })
        return
      }
      const data = res.data || res
      this.stats = data.stats || emptyStats
      this.rules = data.rules || []
      this.records = data.records || []
      this.share = data.share || this.share
    },
    go(url) {
      uni.navigateTo({ url })
    },
    buildSharePayload(channel = '微信好友') {
      const inviteCode = encodeURIComponent(this.profile.inviterCode || '')
      const path = this.share?.path || `/pages/login/index?inviteCode=${inviteCode}`
      return {
        title: this.share?.title || `${this.profile.name || '立方和教育'}邀请你领取试听积分`,
        path,
        imageUrl: shareCover,
        channel,
      }
    },
    async prepareShare(channel) {
      const account = getStoredAccount()
      if (!account) return
      const res = await createShareMaterial(channel, account.id)
      if (res.ok !== false) this.share = res.data?.share || res.data || this.share
    },
    async handleChannel(channel) {
      if (channel.name === '朋友圈') {
        await this.shareToTimeline()
        return
      }
      await this.generateQrCode()
    },
    async shareToTimeline() {
      await this.prepareShare('朋友圈')
      this.enableShareMenu()
      uni.showModal({
        title: '分享到朋友圈',
        content: '朋友圈分享卡片已准备好，请点击右上角“…”并选择“分享到朋友圈”。',
        showCancel: false,
        confirmText: '知道了',
      })
    },
    copyInviteCode() {
      const inviteCode = String(this.profile.inviterCode || '').trim()
      if (!inviteCode) {
        this.$refs.toast.show({ message: '分享码尚未生成', type: 'error' })
        return
      }
      uni.setClipboardData({
        data: inviteCode,
        success: () => this.$refs.toast.show({ message: '分享码已复制', type: 'success' }),
        fail: () => this.$refs.toast.show({ message: '复制失败，请稍后重试', type: 'error' }),
      })
    },
    async generateQrCode() {
      const account = getStoredAccount()
      if (!account) {
        uni.reLaunch({ url: '/pages/login/index' })
        return
      }
      this.qrVisible = true
      const res = await createMiniProgramCode(account.id)
      if (res.ok === false) {
        this.qrVisible = false
        this.$refs.toast.show({ message: res.message || '二维码生成失败', type: 'error' })
        return
      }
      this.qrCode = res.data || res
    },
    previewQrCode() {
      if (!this.qrCode.image) return
      uni.previewImage({ urls: [this.qrCode.image] })
    },
    saveQrCode() {
      if (!this.qrCode.image) return
      if (typeof wx === 'undefined' || !wx.getFileSystemManager) {
        this.$refs.toast.show({ message: '请长按二维码保存', type: 'default' })
        return
      }
      const base64 = this.qrCode.image.replace(/^data:image\/\w+;base64,/, '')
      const filePath = `${wx.env.USER_DATA_PATH}/lifanghe-${Date.now()}.png`
      wx.getFileSystemManager().writeFile({
        filePath,
        data: base64,
        encoding: 'base64',
        success: () => {
          wx.saveImageToPhotosAlbum({
            filePath,
            success: () => this.$refs.toast.show({ message: '二维码已保存', type: 'success' }),
            fail: () => this.$refs.toast.show({ message: '保存失败，请检查相册权限', type: 'error' }),
          })
        },
        fail: () => this.$refs.toast.show({ message: '二维码保存失败', type: 'error' }),
      })
    },
  },
}
</script>

<style lang="scss" scoped>
.invite-card {
  margin-top: 18rpx;
  padding: 16rpx 16rpx 30rpx;
}

.share-preview {
  position: relative;
  width: 100%;
  height: 360rpx;
  overflow: hidden;
  border-radius: 14rpx;
  background: #17312d;
}

.share-preview__image {
  display: block;
  width: 100%;
  height: 100%;
}

.share-preview__caption {
  position: absolute;
  right: 0;
  bottom: 0;
  left: 0;
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 18rpx;
  min-height: 104rpx;
  padding: 18rpx 20rpx;
  box-sizing: border-box;
  background: rgba(11, 47, 42, 0.92);
}

.share-preview__eyebrow {
  color: #9ae9dc;
  font-size: 19rpx;
  font-weight: 800;
}

.share-preview__title {
  margin-top: 6rpx;
  color: #ffffff;
  font-size: 27rpx;
  font-weight: 900;
  line-height: 1.25;
}

.share-preview__badge {
  flex: 0 0 auto;
  padding: 8rpx 14rpx;
  border: 1rpx solid rgba(154, 233, 220, 0.52);
  border-radius: 999rpx;
  color: #f5c451;
  font-size: 19rpx;
  font-weight: 900;
}

.invite-card__body {
  padding: 26rpx 18rpx 0;
}

.invite-card__label {
  color: #9ae9dc;
  font-size: 22rpx;
  font-weight: 800;
}

.invite-card__code {
  margin-top: 18rpx;
  color: #f5c451;
  font-size: 58rpx;
  font-weight: 900;
  letter-spacing: 0;
}

.invite-card__copy {
  margin-top: 12rpx;
  color: rgba(255, 255, 255, 0.72);
  font-size: 24rpx;
  line-height: 1.6;
}

.invite-card__actions {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 18rpx;
  margin: 24rpx 18rpx 0;
}

.share-button,
.qr-panel__button {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 72rpx;
  border-radius: 999rpx;
  font-size: 25rpx;
  font-weight: 900;
  line-height: normal;
}

.share-button {
  gap: 10rpx;
  padding: 0 16rpx;
  box-sizing: border-box;
  border: 1rpx solid rgba(255, 255, 255, 0.7);
  color: #ffffff;
  background: transparent;
}

.share-button::after {
  border: 0;
}

.invite-card__actions > .share-button:only-child {
  grid-column: 1 / -1;
}

.share-button--gold {
  color: #0b2f2a;
  border-color: #f5c451;
  background: #f5c451;
}

.channel-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 16rpx;
  margin-top: 22rpx;
}

.channel-card {
  min-height: 178rpx;
  padding: 22rpx 16rpx;
}

.channel-card text:nth-child(2) {
  display: block;
  margin-top: 18rpx;
  font-size: 25rpx;
  font-weight: 900;
}

.channel-card text:nth-child(3) {
  display: block;
  margin-top: 8rpx;
  color: #6f7c78;
  font-size: 20rpx;
  line-height: 1.4;
}

.rule-card,
.records-card {
  margin-top: 22rpx;
  padding: 28rpx;
}

.rule-list {
  margin-top: 24rpx;
}

.rule-item {
  display: flex;
  gap: 20rpx;
  padding: 22rpx 0;
  border-bottom: 1rpx solid #edf1e9;
}

.rule-item:last-child {
  border-bottom: 0;
}

.rule-item__index {
  display: grid;
  place-items: center;
  width: 48rpx;
  height: 48rpx;
  border-radius: 14rpx;
  color: #0b2f2a;
  background: #f5c451;
  font-size: 22rpx;
  font-weight: 900;
}

.rule-item__body text {
  display: block;
}

.rule-item__body text:first-child {
  color: #17312d;
  font-size: 28rpx;
  font-weight: 900;
}

.rule-item__body text:last-child {
  margin-top: 8rpx;
  color: #6f7c78;
  font-size: 22rpx;
}

.empty-text {
  padding: 34rpx 0 10rpx;
  color: #6f7c78;
  font-size: 24rpx;
  text-align: center;
}

.more-button {
  min-width: 132rpx;
  height: 52rpx;
  border: 1rpx solid #b9d8d2;
  border-radius: 999rpx;
  color: #0b4f46;
  background: #ffffff;
  font-size: 22rpx;
  font-weight: 900;
  line-height: 52rpx;
}

.record-row {
  display: flex;
  justify-content: space-between;
  gap: 18rpx;
  padding: 24rpx 0;
  border-bottom: 1rpx solid #edf1e9;
}

.record-row:last-child {
  border-bottom: 0;
}

.record-row__name {
  color: #17312d;
  font-size: 28rpx;
  font-weight: 900;
}

.record-row__meta {
  margin-top: 8rpx;
  color: #6f7c78;
  font-size: 22rpx;
}

.record-row__right {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 10rpx;
}

.record-row__right text {
  color: #ff7a59;
  font-size: 26rpx;
  font-weight: 900;
}

.qr-panel {
  width: 600rpx;
  padding: 34rpx;
  text-align: center;
  background: #ffffff;
}

.qr-panel__title {
  color: #17312d;
  font-size: 32rpx;
  font-weight: 900;
}

.qr-panel__sub {
  margin-top: 8rpx;
  color: #6f7c78;
  font-size: 22rpx;
}

.qr-panel__image,
.qr-panel__empty {
  width: 390rpx;
  height: 390rpx;
  margin: 28rpx auto 0;
  border-radius: 18rpx;
  background: #f8faf5;
}

.qr-panel__empty {
  display: grid;
  place-items: center;
  color: #6f7c78;
  font-size: 24rpx;
}

.qr-panel__code {
  margin-top: 18rpx;
  color: #0b4f46;
  font-size: 24rpx;
  font-weight: 900;
}

.qr-panel__button {
  margin-top: 24rpx;
  color: #ffffff;
  background: #0b4f46;
}
</style>
=fx{ 100644 accountSession.js bfgc_`ܳE9M100644 educationApi.js f{S100644 http.js j(gם_m+IV'0ݳ@ximport { getCurrentAccount } from './educationApi'

const STORAGE_KEY = 'lfh_current_account'

export function getStoredAccount() {
  try {
    return uni.getStorageSync(STORAGE_KEY) || null
  } catch (error) {
    return null
  }
}

export function setStoredAccount(account) {
  const current = getStoredAccount()
  const next = {
    ...account,
    sessionToken: account?.sessionToken || current?.sessionToken || '',
  }
  uni.setStorageSync(STORAGE_KEY, next)
  return next
}

export function clearStoredAccount() {
  uni.removeStorageSync(STORAGE_KEY)
}

export async function refreshStoredAccount() {
  const current = getStoredAccount()
  if (!current?.sessionToken) return current
  const response = await getCurrentAccount()
  if (response?.ok === false) return current
  return setStoredAccount(response?.data || response)
}

export function isAdminAccount(account = getStoredAccount()) {
  if (!account) return false
  return ['super_admin', 'campus_admin', 'advisor', 'teacher', 'finance'].includes(account.role)
}
bxIimport { http } from './http'
import { API_BASE_URL } from '../config'

function toQuery(params = {}) {
  const query = Object.entries(params)
    .filter(([, value]) => value !== undefined && value !== null && value !== '')
    .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
    .join('&')
  return query ? `?${query}` : ''
}

export function getBootstrap(accountId) {
  return http.get(`/bootstrap?accountId=${encodeURIComponent(accountId)}`)
}

export function loginWithPhone(phone, code, inviterCode = '') {
  return http.post('/auth/phone-login', { phone, code, inviterCode })
}

export function sendSmsCode(phone, purpose = 'login') {
  return http.post('/auth/sms-code', { phone, purpose })
}

export function loginWithPassword(account, password) {
  return http.post('/auth/login', { account, password })
}

export function loginWithWechat(code) {
  return http.post('/auth/wechat-login', { code })
}

export function bindWechatPhone(payload) {
  return http.post('/auth/wechat-bind-phone', payload)
}

export function getCurrentAccount() {
  return http.get('/auth/current')
}

export function updateProfile(accountId, payload) {
  return http.patch(`/auth/profile/${encodeURIComponent(accountId)}`, payload)
}

export function updatePassword(accountId, payload) {
  return http.patch(`/auth/password/${encodeURIComponent(accountId)}`, payload)
}

export function updateBoundPhone(accountId, payload) {
  return http.patch(`/auth/phone/${encodeURIComponent(accountId)}`, payload)
}

export function uploadAvatar(filePath) {
  return new Promise((resolve) => {
    const sessionToken = uni.getStorageSync('lfh_current_account')?.sessionToken || ''
    uni.uploadFile({
      url: `${API_BASE_URL}/uploads/avatar`,
      filePath,
      name: 'file',
      header: sessionToken ? { Authorization: `Bearer ${sessionToken}` } : {},
      success: (res) => {
        try {
          resolve(JSON.parse(res.data))
        } catch (error) {
          resolve({ ok: false, message: '头像上传响应格式错误' })
        }
      },
      fail: () => resolve({ ok: false, message: '头像上传失败' }),
    })
  })
}

export function getReferralCenter(inviterId) {
  return http.get(`/referrals${inviterId ? `?inviterId=${encodeURIComponent(inviterId)}` : ''}`)
}

export function getReferralRecords(inviterId, filters = {}) {
  return http.get(`/referrals${toQuery({ ...filters, inviterId, paged: 1 })}`)
}

export function createShareMaterial(channel, inviterId) {
  return http.post('/referrals/share', { channel, inviterId })
}

export function createMiniProgramCode(inviterId) {
  return http.post('/referrals/mini-code', { inviterId })
}

export function bindReferral(payload) {
  return http.post('/referrals/bind', payload)
}

export function getPointCenter(userId) {
  return http.get(`/points/${userId}`)
}

export function getPointRewards(userId, filters = {}) {
  return http.get(`/points/${userId}/rewards${toQuery(filters)}`)
}

export function getPointTransactions(userId, filters = {}) {
  return http.get(`/points/${userId}/transactions${toQuery(filters)}`)
}

export function getPointRedemptions(userId, filters = {}) {
  return http.get(`/points/${userId}/redemptions${toQuery(filters)}`)
}

export function redeemReward(payload) {
  return http.post('/redemptions', payload)
}

export function getCourses() {
  return http.get('/courses')
}

export function getCoursesPage(filters = {}) {
  return http.get(`/courses${toQuery({ ...filters, paged: 1 })}`)
}

export function getMyAppointments(userId) {
  return http.get(`/appointments?userId=${encodeURIComponent(userId)}`)
}

export function getAdminCourses(filters = {}) {
  return http.get(`/admin/courses${toQuery(filters)}`)
}

export function getAdminCourse(id) {
  return http.get(`/admin/courses/${encodeURIComponent(id)}`)
}

export function getAdminAppointments(filters = {}) {
  return http.get(`/admin/appointments${toQuery(filters)}`)
}

export function attendAdminAppointment(id) {
  return http.patch(`/admin/appointments/${encodeURIComponent(id)}/attend`, {})
}

export function createAdminCourse(payload) {
  return http.post('/admin/courses', payload)
}

export function updateAdminCourse(id, payload) {
  return http.patch(`/admin/courses/${id}`, payload)
}

export function deleteAdminCourse(id) {
  return http.delete(`/admin/courses/${id}`)
}

export function bookCourse(payload) {
  return http.post('/appointments', payload)
}

export function getAdminDashboard() {
  return http.get('/admin/dashboard')
}

export function getAdminLeads(filters = {}) {
  return http.get(`/admin/leads${toQuery(filters)}`)
}

export function updateLeadStage(id, stage) {
  return http.patch(`/admin/leads/${id}/stage`, { stage })
}

export function confirmLeadPurchase(id) {
  return http.post(`/admin/leads/${id}/purchase`, {})
}

export function getAdminPointManager(filters = {}) {
  return http.get(`/admin/points${toQuery(filters)}`)
}

export function getAdminPointAccounts(filters = {}) {
  return http.get(`/admin/point-accounts${toQuery(filters)}`)
}

export function getAdminPointTransactions(filters = {}) {
  return http.get(`/admin/point-transactions${toQuery(filters)}`)
}

export function adjustPoints(payload) {
  return http.post('/admin/points/adjust', payload)
}
N=aximport { API_BASE_URL } from '../config'

let requestSequence = 0

function getSessionToken() {
  try {
    return uni.getStorageSync('lfh_current_account')?.sessionToken || ''
  } catch (error) {
    return ''
  }
}

function request(path, options = {}) {
  return new Promise((resolve) => {
    const sessionToken = getSessionToken()
    const requestPath = options.method === 'GET' || !options.method
      ? `${path}${path.includes('?') ? '&' : '?'}_t=${Date.now()}_${requestSequence += 1}`
      : path
    uni.request({
      url: `${API_BASE_URL}${requestPath}`,
      method: options.method || 'GET',
      data: options.data || {},
      enableCache: false,
      header: {
        'content-type': 'application/json',
        'cache-control': 'no-cache',
        ...(sessionToken ? { Authorization: `Bearer ${sessionToken}` } : {}),
        ...(options.header || {}),
      },
      success: (res) => {
        if (res.statusCode >= 200 && res.statusCode < 300) {
          resolve(res.data)
          return
        }
        resolve({ ok: false, message: res.data?.message || '请求失败' })
      },
      fail: () => {
        resolve({ ok: false, message: '网络异常，请确认后端服务已启动' })
      },
    })
  })
}

export const http = {
  get(path) {
    return request(path, { method: 'GET' })
  },
  post(path, data) {
    return request(path, { method: 'POST', data })
  },
  patch(path, data) {
    return request(path, { method: 'PATCH', data })
  },
  delete(path) {
    return request(path, { method: 'DELETE' })
  },
}
eؼx sexport {};

declare module "vue" {
  type Hooks = App.AppInstance & Page.PageInstance;
  interface ComponentCustomOptions extends Hooks {}
}112xe 100644 logo.png w wvjy0Z40000 share PWA<׬v40000 tabbar etu/Њ߶M!U+`1>#*xHPNG

   IHDR   H   H  "  
CiCCPICC profile  xڝSwX>eVBl "#Y a@Ņ
VHUĂ
H(gAZU\8ܧ}zy&j 9R<:OHɽH g  yx~t?o  p.$P&W   "R .T  Sd
   ly|B"  I> ة آ  (G$@ `UR, @".Y2G vX@` B,  8 C L0ҿ_pH ˕͗K3w!lBa)f	"#HL  8?flŢko">! N_puk[ V h]3	Z
zy8@P<
%b0>3o~@z q@qanvRB1n#ǅ)4\,XP"MyRD!ɕ2	w ONl~Xv @~- g42y  @+ ͗  \L  D*AaD@$<B
AT:18\p`	Aa!:b""aH4 Q"rBj]H#-r9\@ 2G1Qu@Ơst4]k=Kut }c1fa\E`X&cX5V5cX7va$^lGXLXC%#W	1'"O%zxb:XF&!!%^'_H$ɒN
!%2IIkHH-S>iL&m O:ňL	$RJ5e?2BQͩ:ZImvP/S4u%͛Cˤ-Кigih/t	݃EЗkwHb(k{/LӗT02goUX**|:V~TUsU?yTU^V}FUP	թU6RwRPQ__cFHTc!2eXBrV,kMb[Lvv/{LSCsfffqƱ9ٜJ!{--?-jf~7zھbrup@,:m:u	6Qu>cy	Gm7046l18c̐ckihhI'&g5x>fob4ek<abi2ۤĤ)͔kfѴt,ܬج9՜kaټEJ6ǖږ|MV>VyVV׬I\,mWlPW:˶vm))Sn1
9a%m;t;|rtuvlp4éĩWggs5KvSmnz˕ҵܭm=}M.]=AXq㝧/^v^Y^O&0m[{`:>=e>>z"=#~~~;yN`k5/>B	Yroc3g,Z0&L~oL̶Gli})*2.QStqt,֬Yg񏩌;jrvgjlRlc웸xEt$	=sl3Ttcܢ˞w<Y5Y|8? BP/OnM򄛅OEQJ<V8;}ChOFu3	OR+y#MVDެq-9Ri+0(Of++ym#slLѣRPL/+x[[xHHZ3f#|PظxY"E#Sw.1]Rdxi}h˲PXRUjyRҥC+W4nZcadUj[V*_pFWN_|ymJHnYJjAІ_mJtzjʹ5a5[̶6z]V&ֿw{;켵+xWkE}nݏb~ݸGwOŞ{{Ejtolܯ	mR6H:p囀oڛwpZ*A'ߦ|{PߙHy+:u-m=茣^G~1cu5W(=䂓dN?=ԙyLk]Q]gCϞ?tL_]p"b%K==G~pH[oeW<tM;js.]yn&%vw
L]zxem``Y	ӇGG#F#dΓ᧲~VysKXϿyr﫩:#y=}ǽ(@PcǧO>|/9%   tEXtSoftware Adobe ImageReadyqe<  *iTXtXML:com.adobe.xmp     <?xpacket begin="﻿" id="W5M0MpCehiHzreSzNTczkc9d"?> <x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core 5.6-c132 79.159284, 2016/04/19-13:13:40        "> <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"> <rdf:Description rdf:about="" xmlns:xmpMM="http://ns.adobe.com/xap/1.0/mm/" xmlns:stRef="http://ns.adobe.com/xap/1.0/sType/ResourceRef#" xmlns:xmp="http://ns.adobe.com/xap/1.0/" xmpMM:DocumentID="xmp.did:FFA427157DC611E8BFA8DC8EBCE450F1" xmpMM:InstanceID="xmp.iid:FFA427147DC611E8BFA8DC8EBCE450F1" xmp:CreatorTool="Adobe Photoshop CC 2015.5 (Macintosh)"> <xmpMM:DerivedFrom stRef:instanceID="xmp.iid:BA8FAB7C7DC511E8BFA8DC8EBCE450F1" stRef:documentID="xmp.did:BA8FAB7D7DC511E8BFA8DC8EBCE450F1"/> </rdf:Description> </rdf:RDF> </x:xmpmeta> <?xpacket end="r"?>Ae  IDATxbd 홖 &B
 8]I;F%HVĂ8̲ª oĸ4ߡ;z& "&0nG6yںh@b!&mkߨ5 %5@:BsT`鈉ؼ4ڨAt,h"r
 D>4jP[9}3>!Q6QC+!DIf4QA*=o bT2.1 ;f!Tu[
euЀWvc(BF٨F4Q:hAGQ6)`h:h8: r@Ahu8D/=:sTE-rsI{Y'^--&
fp6s  cCe* @ H 咽@
tOc;* @d0|Pjmy=l}< LJeW [    IENDB`Ax4 100644 referral-share-cover.jpg N^pKqw>x    JFIF  ` `   C 
	

 C  "            	
    } !1AQa"q2#BR$3br	
%&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz        	
   w !1AQaq"2B	#3Rbr
$4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz   ? aҔ;~T?
P*!F?)zRc4O'8m 0zʁ~To 7 Z>PaR~TQҀ8 QϧO- 0x?*ʙpi ~T UHE0 NSqqOS$`{~*~3Ih/+}?*
s@bLZ~(LRNud{t_= cR~_;4hR.(v):!HL~Tz *~N3L'S4 {~t SI0 (;mzP1 ֤#ߕ?P() =ԘǥH!\SGOʎiBboҤIM z~&8F)
O8_(ʌz@?Oʌ~t8F1@2ԆM4Ni OG4t?*p~iq c~T?Thz~&M  Zv(4jitȦR>qߥ&=R4P
R7f'?\ASNybH:bzJh84h3 M?犗FzJ:*v9P۵'JE4 p<'Q ֤;(gJm!;)⑽GQKۊ`4vS()(<3?҂)Av;0#AZy&h-&=(ʚx@)f'4SiEHC:¹0j\p>Dzpl枣h¼TdU)hNihh1FPqM"G4iҎE GZCHFiPdҁ.9:H)z1RF)~hO" )`ԙ hg4b@j8QDi)1@iޛiP@i}i(iF1L Z(^SI U@8<ў(4}i ))RSޝ(4(4 Qh`")1Iڜiڀ  1E)8| ;I !<Ӈ4)qF9aҐғ)8)
 LR:Rm
CKE %Қi֌"Jiw҂sHc3\RI@xqO֘fMZn)ފ@3m.iqi43L(N04Ru &)iӱ@8)RPi旯Z) )1}`FF)3O4s@8֔cH7<iJ8bhҀm8&(<&(ҝAځL<TzSHE)v1ҎCvbGZ6sO)q@l)i(P?c7 m
u7 ␌R1N@$du
怸i
q@BH}3LcM74cf)qu!њmvNM1R@"/: 3@ B)S6qJҖP6OLpiɠm)s@)08t= %.) x@4m 4҅"
 g3vy)KSwdSqRRR 4 1OqLьӀ8ri x)4 6)Z~ aiL Lg֛@
isF1 qHM!cSCm nzSMImk5[YC%ݏo
>K>?Z6>Ǩrߞ+9T>&kNJny5&=+ؿd<[܅`3f o5MGl$5s |YGJN:gA_)Fjky-Pᢙ
:T&N[q( ڀ
v)fj&'QLCIgC>m&S)!Rc CE8ъ f);Ӎ31O4NJ!m({ySh"xqHG 2ԧ&9/QLS)!=)KQidH ȦRb4 Joae;
nu1Sl* GB))(wqO+Kb#4)!h4Z 1M4iJJ)í 1KAP ҂(F1FH@Ƒ4y""=?SM 4c~Tp)G4 RH0)1KԊC"zRSHZb5;MҜ(cւ)!QN4fߚ)b3֝@ RKi
 RsQvi;Pi)搊 fiM) @@	ҊSH yۚ nzъyƚA8sF(qAL@4@bMHޣ4 sHE*4f9:M 
CKi44N,P<?	jY(רx[v9N4;7AǨ?٬)NW<e-by"e?wtгxUg` D>>IIm?1x\e%ݏ):x 946 ڤS"s^?z`I|K]rӌ{dyG\ \JzoCӥOߙ}BtBzf?W51Fq֜-z_'+lKH
XygޚMlvfE﷟ҤTTyMc~_f*m5X . 5/ծ<K`%m5,Jr ӪGNs`HAUs
iVs
<c]=;5?mnGtN +ROB94y09ꧣC^__>	\KP~~٥Fֻ!mU\oJ_&|Z5/a2r#\ +ĵ:HVc8k{)5"
~8؀QJaҜ:V!1O&V#-478tӇRu4qIրM#ҟI(INӥ &}i3Jz<R0"4֔sޘH<~bSbii:SO0Ȧ!(4{}恷ʱg3{]憧
9ynU2=ktD+YI&KG*V؛Lt*짥U*JbRNA aH@҅ȣ`P"<`Ԋ)*1XTu3TDP!)x@f.i	wÞf>ԇڌQ@Sǭ '=z
R8B*}T zwJm1sMSϵ0Jp 0-8SҚO4 SqR3@5)P:RяJcNZ]H0Fh3i !3֟b!4^iBS!1PM8B9怸M4zP!c;Iڀh@(?yE-!})iPpi/~iCAkBiRSԡp2~m<<kEh*7?ҳ[5E>ÿ1++:;c15nbm/>Q#!s3S[\n{qkrwg<vV@r0
~Y-xw(}>o~hVmΗuf#OgCǨhk)J#5
ЖܹSX+{㸧1mzԔ"#a튤A!CXuү˴̪s܏+|Tcj)<ѱ`N<ÂAa}jPU0*\gSű	bo  9.F8< Oɳ27d$\sM;ܓ~0͓NMp::E'Eo,ԫ(2E²9^3bNiZwB _50&?NGj=GTmZX rZYD`?AOe}ǁ9YH;} _v+-gif[j|f?~т_dCXpa9??7zWx|QՄt۠"crrhbg<u*\|ǌvqp>W|cҗSNh4hf1HJSɠih4  u(HdIL恑lqnj~(4"s:ҕZ oCIۚ~)P{Iњ \=Lҁ 7 rhdCV|*/QʢeZIΰ-܅BhTJ54'n>\$.+.4cҲ.< 8Oۍ%x[?-a^<ڥ",yEQ0CeY,xxou;TE;Xi)^y!ޔqHE !4sM4xF)'ң<#Sz~=h@	 2֞E&(Zm8MbNLgz0i fc¦)sL1M
AH6F ֊%(
`ME?ۚ  u#c Fh֔ ɠ(40w)
!!~hM+ (l^iq@րy	% txKփҘpi?:i1ޚzӺQ hE<b^<G|jw28XƟV( w#Z6*&ͿE;Y[ \ǍwM`[2mT;2n*HӚɨ'/̀2՛yZ '( Xf6Z̑!,eGZ`z٭d))Bߖ-`뎕>1XȷO#H;ՉqL2	\ܥbgc+gS:&Efew0R
QpoOmq ꄺ2_63iCQnÿs)Cê#Aٶ4.) =_3|;-]^"b&Vur9PiT	PvJֹO|CƱCzDb>y.Vt𞎶=Bc_~$Թ5}ّk˹`;ߍp$Pڼ^<kox^Dsx2[j8X	6G;eruߎޢ F$庶\yrn5[bprq+n|!Nr=,U-bٖv??>AT\29bqB{{=BF6w8U觧֭+1TE5Wiy{ (J(#"֜Vi"<P43@Z wjaiIjCN4{P0"iǚLPiR 7 RƚJqPXRcRQ0)v杷ҏ &Z	4zPJEHST=I*>TdmCCғ)O>1DR =U44ɞXq>讑VM*Gpnn-z;SN<CGO$
ƻYsYJ)H9C>Oj̽4`IkG~Z"mcd#x.4vg]	2Wh9ªK@Z$XQ%lKCqyZpiIٽ#JsM]-?43MۓZ7?Ms.Kxd:8ںHxAvi}Yh GV </gДo^޾32qXLL+/_>.ؼ4jͻO>J>xÚ5ޫ gGkh	w@1ɯ=_Yv/`/{?ҾKnVODy
iҾՈ}j\I鷚v0zg3_U~z[i_oWYcmk|j9WT>U-1ܣ 8+"?l5M֣>O\xyOM%ZBr1^FiWa:m7 bx~¢iwϛv`x־q#f(4wM=iޑ##ҌSHhsIR5PG9ELSϽ(֍Zv:t+ҎEiHVSRL◭%(!ӱށE(@  PzCJi;qLBLzR!@4 -(E SN ΔTFz<Si[@
k׿f8ߌF[; $Z._^gF?KopXW/}v&v^:`Shׅ5=  ZM*:&+|U͏^M/'b?Fxs@Cs<d1ï" &XuM|]u3-*9͜S5f6l{biF2+N; |)RU&7PPN?,~2l OQ_78a2;H\tz}k?VEK-OGM@IԘ̊=\=X=jYJ^y=OJ-pWzt޼]}np%XՓ2pA8֧RU[)K:?ǨG_|L߈Oz_KcI6ͰS,x{+r?_M=u'ntj+Mn5rݲ50k^) 9^M lԔ$n0O)8 ~\=z{b`gP (B2AD硪ᙲ
I֭;6dLF!CzryU=Qv`ZP9uOMDEr7
֞"]ћt84>LSqR qHMLzTMڀ)ԫJVۊ_a@f~;ӳ@SNґ@Rh8sCJnhq7☃ A|A-;4hʹm43#u'S_GUr1?G)3
^J^<T`W<d٦Hӥ!h nz=7U)m7!Piv};wQfO5شAUɱO5o?Ae*Y$i?½],x#V/q0X6١lFʲRvCJ*U^=OD<?Iى5) wZ񶆾/𶭤KKqīʟV:7zwSThWjU7Ο^B*ǖ\tσZTfYJ=pGM#oZ~]+a`?iO9
_F5WT~Y,=iR}:u>x~ q Ӌ=/×v_"1 eSǕ,W>\Tg~Gg9Gy^y
̹Z-搏׀dEoCs_dq}+~҈
|m ߩ<7|Uj{l?ݖd.+ WĵKz|9GZ_|2M~pxy{å{ |#wwy6= ,#w%ROS\=h^ rSXx^t#4 zд-CŦ[D {(_w)('&GF.mE-JX43,jNcg1_n?z_4t+DKJ0W -| &#͡fH-hOOC۽xX\''&ê]\z{
N+<N)1&2)ݩx#:193Ҁq֝E(C gs0NRS
cq@QPr8Z i}jB)ڝOt4ݹNPmRpi	)M1zPM48xaJa⟎@IޔSMڃ֐6i3?ҐG4斚E1xfiNR~+J}|_Xs?#^(wq1ȯ ݤop7W3
^  n`+Yc;Eg+ʥHI#E۱~jFeFFzg<һoc4R[nI<:W|IsG8J@`V2S `nI=RXG8 ĺP=	_S_e,AW8|zWː(
_U|C=yn;wlF=LuN?JZVm{Vak}9`o_}ZY#0@rS_j~|_>׋Qꏪfp
s}*8woe6:`QC)}EW{^jV}Кl1-@OBh"n<z&b}8S tǾ*6_2Q:D֏Ǘ3pwo: eJ29\V;W g#>iJ̠gk{S퇟91Z~iھ\R)C54
v2(0Nq01ƛL=)iڀJ6xb"NkiT@aL&'5@֑VNyJhM4 RSLC
LP179z)	NM#(Rӌc?BH4GT rC#gy5}i Za@:Sb/@1QqP0zBR۩})y H:SN8ZP.@PI>=ڝVkYࣹ>kh s$&%? VУ	 pZϯ58%Ag `TLqOKxx{X,e<m%R5fO}["b,ݛk?+cw&''ڻ	m1?|{_Y[ Wi:CB#E5ҳZkxm`)Tc^{IGcUp+73֜k?<h|Ȳ}c4E~b@FޢB;Ú,rM>ĐNfvvRt_.nuϓhrjwe_uڢ~y5 rk&fϛQ Z$t ˩HJn" 3_Hd@Myt
ꚃ.5
\cԚWυ6wmǦz_JX҆E]F8LTz?xdݧ陊< yU׳t'~Y$֘Ϊ@
I'ֿAІiGd|&UV[Z\ZXS(e@}ᵿ} .o lȅz(}s/ ikTֵȕIule ]sۥzv5Тջ  +U^v "qdxĚGG4ihTLwo|/➗v6[YioJ_~(|Y5r9'bgzk.j<wgnc.Xԇ]OgЭ^MR@}=j4Z&1劊~s)s
B=yzRjO2)J qq@NS!HF:P1i(/zMMԄӱHG FM/J\PG@7:fҝ 7RJ aJq( '"Zv) -746恎ҚM8Ґ#/4'>bS֜zSGJO *6HG5"k=7Ο_9/ZW;M*S]*! ֮lOۂ}+oB|x16r_C:oύ6*v3( 
?D,_%hJ˜G=8/zA(Tݏoo^ϓ~d^:OAc{ <cx_
:p{~[
x
_NN^o8g9̲־Pqdf 3kÏc:t},ZKr++G6;)L|GZUCf 	z |
w6YԆb xw^L.3I4;}*	Xl61Ӝcw TۂQp01Q6Bq<Ol"ɤ1@XxJT9?ZL?ME32?Z%H^N՛bW쵩{zg_H^+I's !]x]kDi ]NuhH4ώH.3 sM+Kސ4Z:H84L@:Ra<qAj u;?F;w< O ۚoJ;4R3@
L!:RN8@S;[a4q@8Iցϥ֐@=h4SwsװTnϠTkk64#{(}*&<TiJW8)s@V4A%}MO^6*pA2iP3TП+R"kC]eܠHၯ*sT~uMZɀ~Ucµav \u:V}W~Vݏ^+OvO2ģV֑+mYB\_[khu՟UMV2kt}ju̶On%#z|S {'~ҹ,w6KGu]'WW{zc?!n$O?q\W4ͷ"p#prç*uɞӱW8E=4j峌>aMvGJO[=5{ꗈΑ!?]AtM1܉`⦖Gv-#'578rcZ${X,
\s|Ꚍw48BNخ핸O?68jx]8£Iy08Zr4*]X{2h2\I'ZO/5\Ka$l	^99eIcg}aiybp~Xy~r8sڽO_#!zvi" ǯGnxWEm,t;\CmA%ַ''ny,U%JZr`Xj[/rZ{wc^%_&<"wIvz/_j^'v4}⺲܎֫ l;/ 3i{i9yjҥUIQ1u\qbyÍӸNߏ_U[C	ԒK1G_+SgΨ@UQɮO)u!cijO"cL; 9澣<;AӐ]yq?w1]5󡸍79R0E|#T#S0?V_.V=7I  ȇCY$bH)f||spA{SJsJ(SOji
47=(1@
qR
 :S{ҚCZ!&B
<Ҟ;mM!;PG{P1=1I@ZiN#(!5#9b?Z\g6jLs@e1HE1:sIހx0	OҤ)qjgc@o8Lk2,mSy+|WklX@K4̆_g~?CU cp-Q_4~tBX홫邾Z Tpkkj1jw~%wM3q0JROFe}֦%T8*8<g>ֿ-X$̶ wW!rWw^+M=%i+;9RFww⣑A*cz#9㯭S~~;*ɰ¢E|zC/xmGX˲vRF
z1 J^@ ޹1/G/K<KwkrO{~5 k"i?,IJ_Kj^q^4ݤ}463צy4-' 

xӥ'6^$A⠚O3,yP=w<
5?3; ^,T] tj1QTI 5	i
{z%Y2g{=֝qֵKp=zqX^ mqv=9<Fn0i{Si^iWhҒN /zC@&siyW/?|Nb?XluX;{^W:-F+Ǣ Vύ|o]zY"\Cgmߺ,B; {iUςa$յnwÏAĐ[,I9<RFoM^S5%^SnQfНǇȦ4ZC(4 3ӛ @;P)ؠdgҊq_ʚG4)OӍ1sOښO4qJzfy4SO}$?Ne}'h#L)l.2h4&A+Ls3rx4
jF	5JvsQ˒{S$(d=?JP-勘}GQ]gYJ>/պʮJ|N#;K%`2Z5psO[LNzUiרjZ1 p#xZ½.oZ-˳ 3e54{9re$"#-u<A(\dsjCWv+GZÆKCx!Y`u7TbbE^K\=>sPmZL!hcL隝[{_ھ:%kӞW^]ϴRsE*?̅"5b(W>x2j'j]MNRwgF*i=R0'}?3MӯZ!:CNix~[NgJoCRvpcߊh~ kF=FݙMNFG=HNTf.2WLO׺v\мLjO|%e_t>R|0tW+EZQQV>"m_EFp~U8:j#iu{ysz/,~Ѽ8tKy[z~㹉,O' ~oOzãk~#x2X~fiن2\׬Sp8FKN{-ݽ.1k~0xoIVKygn׋_EMrz9?S¯^;/<<GIF #:`,XB!o%<3\LL:
Ӂ赛5:Q
8lVu:ݜ{BW7im<2ڶsK֌;T&2)Eb=ztSl}"=izuI  fܚ]cq@ҌM&hӀ4DLӁȤeɤR8}iONj6@1sM4 q4RM'4u5/ҘE u ڀM!Sw hSjM̚@iïȠbM; xƑyɜo(W?(M{ dV754Xc\RnΪXj7tb<)ƞ4
rm[^G} ~	Ft	_/ &Ϩ- Wu;;s1zWo֥/y/?U&9sIw(鋱>V W>X[JJYJ*K<gfV&8Mq4s.WpH?Pkug%vz԰ii1m^}j\ 6CX>oKu6%Q:^&0#Er>EQ3Y*ߑ`	2
><?̸ӔImwo=>Ez BG==kjREi+(xo$5ӓe\LѠp<ѻcVy	G5.ON-Ήls}z#"\ϓ.Ko*sCдB{1K?ֿںyBiUr07ӯg*EUQ~(t%%c?E`403}|3u3ڍyZK|NтeKGۂA<Z|gC[7? H^O9 v;{ Sc'שK>n}=jWjْ	ɘ9 g?;
0dge_Nzi7+tUm@Qգ6h/<kUlݶ[vsZRO	7}+/7xٜr[.hMk>l"ni.q@7_΀yw$Vkڌ:~k%	qO`;_U)aumW m};)cQ<k-[qe|Z~Ls)LZ;{Hȸ]?@t_Sޡ[x:^C%<}cOГv{OS:
zNS1LhYcF5$A8}*$?_AB4t@ԛP(J_9Shg֗wLO&P2Rj<Ph@)Eu:S9ϨJ CM5&8@ g*_n3+ʚW_HT՝zb+3#Jz@sQ$P4Z`! ri^øE@X)=P'c_\L%`K}6Qo󯘫e9Ҥ2>\rzү3הū]6`V7;5U1pm/=zYyX7P4?CQm%Oayoqw_ɏ[I	S
և^n@=nsJ|-/WEԣf;I_vȃZsiZUsGwp >ǹrj!Gn	+z~cSu$z,4pU8 ׽)<}*-ͺ/==))q?HW#$O'2z;&sӇG$R(T*jNj9PܑҤJbq52ci}w	np ׻$VNem5uqk7BHשc 꺭]Zf?^;;EAK6|mF u	w0#q h$͏ +_'/oW |4+ُnL% W^ ,: 6N>v֦Byf?fښWR_G#g;3 =$Uj:<WܠYs^k긆R}kpo>/^WUU&唰T86KJ.Jjn_GslqsǞ*~2q]jΙI	- iZΗN ڤ&6B+ UlO*#$Ҏ*:	8tځn:jB}i !>M# ic.xH<AZx^ig֎{P1:cUAk
V7@j7<Sq@#қҜN:I`J,猭~ض^JZ+Ē6Ig_+A{<Oqm p:xyIAiӚ> -cǦriJ=tkIY"r?vy;CgZ.ͥD3 TWGؼ_C`*I'YQj
"v#'GJ҉]O {#(cjѝ<bLg	#h<;v<f[7CGQ-5H]²`8*}U փFx㊌!ᔲ xuҢQOrgF`#E˙N:?Ѱ}Yꓼ0z|~<PWob:U-OATf88t>Ð~$n\Rp8L@:TkK~CZ;i:7чW5{=JO&&h/*zFETg}jh<ew{cT,q4r1_Sn$yƹu'r8 <QJVq+K RGJoI]URrx5ǭFF~gE2`=ZJWpFG0ɵJyVtI&AǥTk#O~9|mACdO8'0Ǹn V>UV/Djܘq5Y%eFՇlW:W+tN#<T9'9n(RQ+}qM9Kx攌Vi:ӈ3Һ?<wQVq\^H?w io|<]7zi?)Kv~U6hHHbGlݷƣIɩl.?'[U`w3v_aǾ/n:rh>E4ߋ?G\cg-ރ^6ͷ98iXoA}<AGNILP+w9&~\cޜ~Pr=dml3:sHFDR9}E5N*Fd%̽=~)wD'#+t3V49NݚJ)	 )駚""qOD̓@zoZb=i ڊCɢ>s*+gҮ?lzQ3q^
Nc֙FI'ґg֜+`mbd`S8SoYY_>ֱ]uψ5)UFz/V495qz|??l?p.!> {O!w_˜^'9HgۏU|5?ҴY	f<b/v|
qIw+&G91}z aMhkzNgw%# 'i(`NJ2խ4bMHGDFysqJ$.p:QIջKXR[sHd✟7^8j@=G=.z#*Fz :{:qި `Py9{#}*$pU9Rs<Rgi^}i ?Z N\}VoNmRcin!*$rO1q_Qy"B  $#R!5tڿrg(QIF2##p|Jف"K\F݅h^kG_yJR3*~z¤QY<O5Y5noGLiy?SXub0Ⴢv*(Mi ]X<
ntۻVUT%d9Nyh*R2HWyeV~UYֈ:SJd {,gMc/+U McY`-vZv+6K-zS82gMcw3XrK
68LGdv\i==*slts[2r$)d4ej<g "Sc֙}i >z1H u#\P9C4҂Ӈ#i<Sϵ&) 
a^x FG4L0
Hzu曷C"q)1<b5qxV_#8c\֟='0*/ӂ*Eb3;
K8|B2=oݴS1CA'vQӧ|fܲx<Z¤[N|OPWP=I)(;v:F>yʷדkzVBy1H7+~Z-7cͬtL'}z4i#Y4S k^hpzSз \fm5HO"3e?P:2lX g |fJT,V9քes ֦p,qRGv3>@'<KImUêcMOEcq?
XK>c %%fT[Z,5$:ݟCAoƵ,5KZ^K{2֗) 朗+S
H`$uOMXutA|}AgwzkCEREݾ+:j
xBm6'DeV_9!ɷz\?]ǜE؇ r|$qyqA1>>='T>`gr;{xv[y8!rēА*؋6ma$\ⶼHH¢cT`ul4I7q
쇡LJFs-NCҺM uBRBL
<L/?)H3zSI^[GƑ(c?z|_+nks4\463_n'.qw>_-#՜F)ԏlS6>CUEǓ\AI=xYMo`|펂-jZ{gP2`czY	]wvw3#eӧ{[VHV۽G cֽ3$ygIֵNw@tQLZ^"qרCR0zwO9?((㯮):SāR$i9?Pd!_5'KȎ$<w$tTefc/'4H
3!ÃMH~ti3@hH)ÃF84)ץ 74F(!cO59`S5 SJVbF(c)ʫ?jlŇSU\f䋆M1PݳLYsgCQj849Cs]O[ڏoЊ~qJY0I6Mg &2N;uƆ"%gI>is(<WMN?y\2y%gCԄCq/_{>y|1oG}in?u~?h	 w,kYyA]r82zD9Pb/Kxuxnu>mdUaaxyьӭkIEq_wm 9^{mh zYAv֭^miHY{p5]FO ~u(\Cޚ7RBMJ6xb3N^ޢ>2ih-:ԙyҚGsRD3ښ3i	4+`sM3LAI}=dd{(885Ⱦ(SӼq%[t9G>2^ʖbia#*\՝F3WNp-$>Gk>(#mOi[u@5/YPe3߈<[Y^'( #KWJ2kg5sɱǸ[?ٮm?QwnyY6R_cMѷ<^ŧ7m,?1XXR#~3\-2#8s ɯM]澸bs#KNG_Sf51JQ/e*.2 ֌/#lk<A>ZiO<P9CJ(`Zap:f:g #I
Hl/<|'֪gɞj'Lt+׵P fC5A+9=*q+{mǠҚacڨ'<~5v9I`+Uv[T$x1GJkM	QLEj;6m׊ i^G\PHRg7 n)H敏֐SwT3Qi
`$jR0)3@.׾/=- 5|=?gc'` k̿e>*GՇ888';wp:bnw_~a9=jENN8u#$nO=<cf	elx/7Qd_Zʗӱ?
t}XmEӮ8evc\ql6slP,zVPg|%*ik[\H"wcE=jךI"A>WCcA}ok-9@|KTD-rA¡Ã^<R/|VQ:Z[OOnzR]\Aon^03?3'~iPw\s^[TͩqU;8kyV*@<:k
R#޼^M~="%r-ҼˆUc4"^w$shTTv=ߝ%̏,3Ųzj?ԗ:߃PznG!ɨ'K,*[	:URb3zS%\#8>M7 dXf6FlJLL?sos+[qX؞+f<+osW?a cr)r3MUidXVUFI56D I^}QU8vyN_1VLcDüC gLMsj	kg>r1Nڼ׭vd2w㢏\}gbO=M0ncXevȠA;T\ne
aFOkm) c 3T8LF2p>%&x#q?hh6M{B)3Ӿ~'|1q ?֮,3p9<Rǩ}w#pAx=I֬'Zz>HOOӧS@<0=s֘$^~W4߯#ڀN>^ǽ(+9a4}恂@kp;Ed\LÝzP͡.GQAiZFs# 9;<LM٦z^ԀB=3L 擽#6)i<F2{@[c*i<, Av=+hZ~sM# 1UOALn}곾	'T54R5`:g`PEyA5AS@pM3h^59<inuAo"_$8_CVIT08zVj{  +?5kU&g;GD(A_;qҌoϠo$q_|Gcj7Ǌ鬴/IaH߼sM4B\Hļc\VD@sqws|oհlyW;YλMJ^K~},Cr<'i߁P+d0Gp}\қnOᥘ? zRQyv=<0{wh8\сh6dM =)Fzȩ M&]:СG\m AӥJqMTn#hG4ivc+ujTB_34Iqv0ޗ-|/$n[WCZ-v00GB+]hQKUy،5Ɠ)jNDPz
FFo0ed^Cڷt`3pjźp(ߍ}5. Nc2FoK\v} w#qǳuL\MR]>O|+`52qKa+a|Uzt6&ut6?kʭXaD^ֆ6b}qA;H95_%0}zqdLm{Rj)p2C;4׏zYʀQM?vz[reTdqN-tsPTdsNdR>jC4s<Pl|^i>THFց\icW-V(G^\Fvfsۍ	8"*ӹ&ڎFK>ҵsҫl1SrU)8<WRmPKlqڰURKb˖KgTX"5Zښɉom)
 Ƭ<DUp
☄+;oPOI@8CM҆4 )6F9""8мBOO?,׀}=qk  y e ?Gs֤'wQg'#i ,Ozi^k7qZ[)n>wF o	a<m>_?J\36<b<,Ccս}ѭV;$5F=1]ç|;2
}xLSvt2$l
ȧO5[Hxu(;WoD;YOֽ[ú~Ĺl,v:9Ƙ,8{:BR0yL~~Tqyzs\RN8"T
[ L=z0P8'o<7zH͞yr DQ5y_8GX_Jf ßEH N)=YB>xN8cLS!#yRZӐO>+p}j[+P}*
}i72~n	nvJ<}܌9Ǒ)<̃qġ" OjFrFqV2ɥd7.ݹ5u'U"dȅ0{*>½>s5vIn5!if;Qdz|=./]b> >=}+3N֯|e =qɴ7 ^Ϙ.w7/=lXYWЏnI,p3
Jq\AϵA,p==DMK( ZvByIӼ4C N?LMi {p~N?~8Q\CA<g%F]^>^:-Y_nJ>{U8#fP2{fjQ±~yw<Ǻدmd6?W o5op1}Ҫ V}׌YĶ+G$nTJ;B]dc94pIJ-3P
T}qsc1ځ
HNGrڐ樑Q>Sx>эR}jP2X{mA!\p7|v5UF:gRwB%!O4sҕQ j2M =E4ԙZ RiCPL`hc($I%V]ʠfȮ3dUX 8ø6Gts+)r3H M0<*{z]xh?Λq]A'4n<`E. ??]++a85ƻ<Siޣ/>C0=|v>#F1
~aLW1xM/ף~Ur:߉5ahϴH~f x JŊ
G9it]csB+>]ZyɿRc_L7bhxxf&#/oj:Z%&P?{+k`j,"_RzH-kKyeyc*f:\^Er#ys\ǍSo}+qQ={+}L΢Ii};}Um{#?5#ڼ~3q'
ٓЏA^]D"rNAbus䪭3bIC6a<:TrO7n8@y=JiI<"Af8Q\E~KXnuk[1ylS!闚/.PI TwjС-&o;vv?ZxhCV/c%}mS?Ώ=!b&C'E+k|6W=ͤ5o({Q	5Zlҕ|F[@[$: G"++|1⫍}{?Ww62,8ܺ_RXSpXX]hk#χA/Sl?ʸk:Som0ֽ`<W|M\wڬZG[<Iw6*`2T~3\
N}3#	58J>^j,}LefnE.Ԁr?I&iUqMV~xȧ+0$tt6y|ˎs L14lg#R6=)rj2>1mV:隁NOz6ڣm .1''<8ʒ{P2nb qPSPI?05U>>N*7\'uR/ȭCTniG55T8A-A$tr2Yp*G'B{\i*ָ*#+togU)@=:\&LP~KrP<dU4ҵ+zMC׾~ϣ?n Eׂπ3F/3 v_G)῝s:{kdp[[37z/=j<gq!Fױ1j9^$cx#^@zWu_ƏcWGib$CceXT:@A5b6]nw};`\zСV.r
p~OpzfGbj}q};2 'f$>ɫptk~>|5c҇č9;%n-~ZX8CۓJ{##M=zؙc{# ^DimP+ǔ$ ֤rZ5'$r0=u
S*f9ҪNFz4K#	;jl=sT.$qZDR*0cT5e7}Omuuf.$OҼI_>ka<yB@ˈۂ2qy:61#%DrXOVڲMJ;u*--G,Ɠ}dFC?j`2K^q=jm<AgmZHbJ/\ ++:nAm}~5O0jއV# u+? 8~6cq9䏧
J<Z<A̚֏/؀ (kS̷9ӦqjaR||=l;Xw'$mF-`=U۱Mx;YTnzN{#uVʨsy(v~n8qsP㞄Ҁ&Vn1+elye{]\c
]ٸ8<t "KF\6㨦;H֤\>]fXQH煳djunLh#qHJG@L[:b)8N+	Uf8~ sk7-#c,!$/={DQWIt	'5k.~Ӄ34gyz\L=sN'׽ ^eq3w4CM&޴Zu'^)C7$tAϽ>r`p?"ES zƐT-c4}́ښbg4iHSb2#1gȩո#	\GJDO3Rgki^(	&Wn3uI)j3LEiOL`GN>|7=C]VF/+)Qa13PxFMs.JC̃J^8`NztUV8HaPzPgk%܋)gv +1D~ak~Ү5-RAݏ`=xZ^xU}{XܲIm	}֓\&G'".kB?hʢ'>mU{_|ob^ƛW Yk4	ˠpڜʹ?*Z-;r8yz-5Aw>Ú>;+Gjm;Ikm4k li~=Wl<	7i=3_?I:em8nk@t-VGOut_rcrXl
we~gV4ƺ6\~$(m̟T?W}?a<iGy+բZ q,vc^gxjEB|U(Vvfy5U8E;{~9[bF&P/ʤk~'^xR֣M<pۍ	k|#oӚ5cX_t7WPMs?*(UI6ՍetI7`<,{,#~MBZ^.oxtODҾ=˾KE{Q9&;}/ ;}wÐkpL2S0K\iysn{6ҹA|GiZ* ?}|M<G?{ٶ\׾/<7q+s@ށr&m$z;h0R;P`k
!5tσRjPvhCemo^7i4M#&.5j\J~uPW/տ˱ۍrhO>&DgN'bqޢۻab'dMs7hR =yXnCu72ژfe`01I͌ P\.E0l栍sO̐8_ 8N O!pvHd'mIC iI3Қr嗎֍FG~sӥ 9Fivy1_\Ӄ <?:gi!P:oN
_q/@9VjxzRt )K*$r'A,yu#[R8"yQp=z5Jdq9#S=Ď*BVl̖>IV* ;cO^,t׷|_+z3~ 73}g
 =WrJXY7$X=T;2W|qA]C3Yz5	R6M%IYL@_T_w&k؃~fT}{?I"0<cһ4̊#Gb-2-8Țo6^!HV{2Iv&5\ʸ渱ZI3F)̰9*rz⼏}i 
`ٯ:>|QXSUcf\y`g&m6GDQP=?*?> weԟ«L^ٚG#IK|<qvn3'C#9 ~bKv9Ip8TEƳdUINjbv5ZV3M	-5B>է':Z&2mJHiӿ&qa^i#M\~ew?i9jហm|,DdCv Xj3?PWD?Tzx2N&V$8yXK.'i#2z6n ΟacZP9o2hc0|99mSIX/~>}I_¥!VW]O؛U7UF15qn:9B3V8GNgoEk-. *n}2,[&d,W}{sKS	n3U֚;ّ[F'qW},uXi-QrL-m`_wXbSP}ǩ߳-d~> z6B%?uqGVr]G$5ΖŊwhO^<e)Ϛdت:s/-;3+bKm"ߍ{;{ 5']e
`EZd*ANa=k>1[ǭir,d,І2?L Up:zvR 0 cי@Ϳ[#)mu?i9dsR`dgQ{+)MĪ'Tj?
9<Kx|DnMh_DsGoS迟u7f,I q8O>.]z/ fs3F hty7Ӫxto'ʼu?¿tWiFjPsڻ!4ilN2FjcuK$SsL'jC&{SJCZCF9<S64Rw4RwHr U]lXؑU\'ҹM.Ҙn{ԱƽDJBdԛ69犁{0Qu4	7i$@T¡wY*U%lq]Rmҩ8S(8ok<.TXfcvS\@u9
mǋSRt}&*w=zW|HT1_z"ٿӧS?>mk!֧ԚN8iHOwް42;h1O.ɥF_[mgg

_/!vcl¡UF0*x{LJGVccb0+mbI۴vjF?#*|,MhҏSc4m*oqr|[u	#b8-w勒rI׀y Qxv[S8Kw.qɥs\+6kۼMÏ:C y}5zE;Is!OͫxVhl7߲J?G܈8k4DeOZkⳓWɢ[n좹8n_5:?/&{G;M,tQx?ʻCoRC2E*,iyx(/!G[q^.yG[_j2Z7sx{Y4<ş4gk)c=ۣ)xNao?μ	CF{e#7f8ob ^%#{ѾW@ּ+"֢F?e7#xoOC헀:jO\3Hrј<_%KU3*upegKsM-bեP$'NzZpOUmWOut#wA裰 Ȫ
\S	AS+~&*N6_]iM'-zмb^>jԁ5Jc@9@C
BQ/sJ#׭0Iw c~񥻞scۿ$w<j>+q\7y%}߂(
qgCxuMjvpù~;&GF(>xM۝^	6> ͷt?5s>x6]RX_d%bօץv?|qkG `'Ė
i3c	ѐFZlcMgwo͝տtub470❉؝_ n4~oSNw7~pHTړ%NR d8!>,yTSA5=z(*OZH[?{@?I{SwrV$ۃӈ
CQ}h1\?Z`*p\PHUʎhFiYK(FWP:+5(T& rlUb݇<ynW4%`ަcںp*nS09魲>\W|ަ}o׏ 9klw85?Sooz8+ݟ }ۗ k<^9?1UD_UKaqq)vn7}}knulv\2'#	 a{OW(1?oQjUǊD3<êi$@a^`ּL&mDX29o5:-3*Xd#Ys>7]ķ-4멛ڄxθup~UfK+f/Ð[+]S\U=:Ia=䵖ŉ7?_Ҽ{Gvǧ(+ו3^-@Ƀ(oJZ؏v۩2qYKLEc' 2 k?<~UKPxA|?7?	Xʲ?@j)$r%o~*	$q?jqQ%`P@9A,7;	9	T._έzd|@:VFRdmCT?kcPlZ19d#`<z:5s_D/a)_-$_Q|*	'_sAYf_^T-ZOGwmCR3:I5Qԏg V4N`'qEX=9=z}+l}Eͥ@Ӝ|er#  ֬&YydO^q"3~GjDA槏搔'p<Hak
NBNF=rG,mwrOUthӖ\ KO`eL,! תqGtts~uuZ98_pdk:[ĺL7SH+&	k>_ [WLz-kFXb@yYMѲx\uqZӫ:ov9khUE?<2)ӯ4Y~Of\Uuhu]>k7	ȿf+ܼYY;Xk"'!L[LUE9<~50||?z &iaJm sA ]n88q,ۿA"#NY@ H,[	3~tb1rS9~U_/1s93OfVPz8+8߈S,dDpkΙMw?f҇+s ӚE%"oƐ=C4CֻO6݁AiݚB.1J	IZ7yv'vwPz!jlwnZ@aSwvbEj*0y>iU擏K*P\`c9اyCɦ&Hܖ#V-) gޣ;n#G0y4(PNsҀ#;	{r:/> aɦ"6 i1zS@#ۊO砦.J`+&㨪FjӾуLEiweu` ]{kͅppEU_DtFgS_?8I.bI7V}OkeAb}uwg-{Z4Pyi~FC^lm.O^٣w;qyρgF?zֹܷ2LL<Rl_םciׄ)ҕ:PGWiohv	y<
+(m漷m`ER#+jNI>kO\/vB>]gIU;;5 etx˭vȲ$we9$/6R/Rtwg^?W.#c?LW2wd}|ؿ"a$<KGM5;lB~H]
%ӕ(,KFgz4>"ѯۥ2}c_.qassax
Ybp}Gǭ ġ;lۏ.0q沴kKf'{d9w3Rk'0J^ի>f;Iu:hFjΟmypFI)}=3@O"XgP`G5\8l`\Y;y5J95cpT{e&;Kg>Ѿ<joO5*.qP"mbH I}-?6v^7C}Jr"=׵y
/uf	I_dpJݯ kainySҎ=5%$$6>JK
R(f搜z6E	P}E49@(b>oC@f/ĘqQҴ푿\ 9VR_(Us9D>3jvxybq/~M:x\<e9?V A*o!u3rx# M-qX5'eO⛞Ҳ!Ц0]+qku]'xֹ: %{{ȯ	ɤ u" Sok(//湬UhPF Wdkk{)}mʞhU fkWQHYf0W9/M5 y:?O	Qlϙ\U*&V:F߯eݏ
u,w1LԎ1~rGQ4pz0)pMd8=*A&P)w HoUZǁ`7W|>hЯ#F_0<h;wqP\=e-&S3#۽r⨺d&{c5)*YH1yc j) ^Z;kt.!6VRV)ЕT^/sJrH M<.gްﱭi^M)#Y>qEbaFr}\ˇ,銝cޟ3J2tXlKsj%y
۞EQ7'.ϕyr|A^%\h1? ګ^Ȓqs.{B( 
ڇōpf4ӆ$l48+7$ 	Jyc~3f$<^VU*ɔQN U$CeY$1$ˊ,zX|SC3qH+ΫNҩw[@4&w2jyH}{vc涌[vF3:lܮA[5v^[ۡۂǽSUo1SS~L}0nPkU5o
p7}&a/S-$Bp;'֔1$l9s;U[>ZDt5Bl4Aw6-j7
_%^k>w2ZL)PO^ MӚ޳VI(q<Qըf$9LF$m q?NʭZ*ۇThHeʖ#I'1Pn>9\NݻY7?ҬbUtՑrC\yʴmN0N[	jcYLSBExKU-?S?N	j@ap0wolæk#ФO\^pBZ{0nVEl3kIzs	'*)󞃯T8O_zH+3<r|d\+Iu_.жߏ,¹8RKhyHԳ¾hM%PcaVi&R		$3[RkiIip9
z0bV9ѝ\-z1R"&:f溎[2PI<p	hiQc@<siO5L	KqEBN:Q@{y~F?*H(T* @BƂG^OBQ S~aH[bE°qF4$Uyn:
K&01ʅ',y('9'=2v[:cC@fl:3)8lSe9鎔)#qʃچp@ސRŸ
v>U\yȠEV_F/VV3t5qL{:+ı~Y-APmQ+R58ZlN6@7<\ψ<%k4\c!u2,sB69ϧY1&ǝVQ.YqW
BjM7ZDC6Ἰcn#ʂj|/7;#/103k7{t7ap?Ė摧)q%܋RO6x$.5A+;\imjR8r[.hHVD6Qz&?\KJy)ssqkj,i Efz9|>ҳjZ,  '*~>d`kC)+? L|̀~uj 2	$}OW#Ue Woj|7TSqQeς<EnC\L$ߢJSs 䌃j7nst%}SZÈqq޿Q{t<M}mۈ?
qҽYm*[׊kD=cנ㚮wyois>RT* '8 d|1`|uaV[9~)7 D;s'o\
NrL |޵ ?jNcu^zN'V
7@vG|.zywilVkd ֙<tCZAm"(訣 ~#7]Oz1䊈ʹ)V194E$
{	QK<P p *<oc8ը{Ii\dkxJas?>TZF7g )掣Vssgs8 N'#gRIQq*yBR*`|
*n 8<3t"mށ/ZanyM#!= 皉8N/*H`͊vO8BBi$|/xJMG	aZ-.rԮt??ZS\g(OG x`Ho縵L Z~<{o*_`;)%z\	ksa|b_ ޗ5)࿑=CL**+c~ռ0Yow 8T?s
JScPpv.>&UQ+9Rѻc,
#ŀ^k
pN^hd -Trf%tuѭW5RZꎳ¿ucWS'$? ?
ڥ͞}®FI7ؐj]/\'v,֚+3 a1oZ?q[Iڞ5_ލ2G^{ӌ9z78|GnMgr`0
a3(V O>Kx%f}:NO-!vq[=ǥ1wbUտ#( nU;}qh*p:Mx)wF_H# цfk2:tx#[a L,}iJ6tU)	b~n1~:PρQ6Hf>HͲsA)Қ$9cUOʼlkXRvHSQճd6Y:e2W5>--a=ưobK1Ix7SFaj|ϺyF=IbBբ>u'?keP-L&;'tUV쫑_8g_EAgGV CNJ>|r:`9yU#23'4D8<uovG{AWc7AJ1zyW!P!s+>BNF J bA;aίC,Hˎ9 V&-|cZ[
R{4D}@_aay@>9U_Q?ub	^%Ƨ9O=)Xfvȧ9VQchwtⳠ@Hf5d1VG#۱?I"ǒ%!I^1+d0~z
;#:^){vCnaڹ+Q檤N3UN3zSsv?L&i	?)
IvS]am7Oֵ#ۘu&GMBSסzOqKmr2HUh:%@gWU#N){O&h/ywcgZ5kSD]5r{ ONk3qjIu8+5%K}aE+^X=>WfVYNHЎƾ2?
G+IFڎ>[pۇGǟB+yhb5] [;c?{t |(l溏=< ZkE֛-&?`cǡ5ʁ__J:SFrTVcg($).xLI gGJGZ x ӀQgPh,}vQ <ϺzEiO-XܧBewۨr!jJiknYc,IS>ZhyC2n?7O6E7 `mf@0>v;TqPpqi(ȵkأ/^7:ddySsN$cIWؐ>4}8 Jt5ycujdUexm6TFFi%*p9<k`v`m{Af;A<ĉ=ZGˀyO_!JL/J E`>1Lr*=|1d[ހ)ds[Eg,r;; Mi{x^n-rq݇x}Ok:毨u};\O;1fcھ_8
Wm/F<SyVm(#l%G&9x;qU7mm}wvBŻ$ c8ܤٵZeT0Ue]L~cOq@Yt->5z(goĞv.faȍ{)JJqr<dczu:f5[9*C:^KyqLTqߞj*(6vpry Tm"KJ@=1O_jٹ->	qPgbn|up;ׇ爮K@Lq te	f-V^Nj^׮<EvݱX, OrbRPHLlm8_ҥ
T$ynNI%ܞD 5;֠Ykw^]Ο`O|Ǧwf[VPwv8Qtce09.ߋi3	iv7aEo 㢡'I>q5G4 4'#4D}sA(=}J8ҚN^$y8<;~=*hnc_2.3T=m~ NyPuWO
'SZX`6;0!(aUg & ?oRPbTIzԹ搷 Z 瓑PmwOj2}֥c&H	#(Dj@ܼ!Yfϭ <'?
Vn2:kMr	xi	i=.9AWvUWדKtA(裟Ι-
B1G)guSc8$)R)lI)5Tށb1\t<՗|zmԿ)'׿j4VMeM.X&[樹x- ^K+pw:iHf<- /sb?oU@O55g5	
9pJQd|\w1iM}u(0Fd
~0V9uǵ6M{l4ft{U\@,y +PWd-4,/#?+_{SUq[gM1$𭄎p;x͞ƭ9Qc3m-#O]~ځ <#_*|?"JQL<!\Z󧱨&RY?Q-y
*v5I[>nOj
 I0W
DLa]oʙ5{jvg?oǬi{ZS@yay<Q }#)+Ѧ*S=_T3iwt[sHk|_GI^*1e~1ڢg+wa{N۟J9H]03޿d-6Z֬5(^[85'{\xFMn|*g9ڤ k8Dqg Ck?vu9E[-gߥy/cOZ?LzW^bGs)C}WSO+||7h?6ιC}jasR(ӄxRl$w!wo'Gl ׁ W?]Iu }ErhhVmn}9o*0>\^Axχ(̱p>~#S(.xdl}{WRI:ЫaZU# ܊ӂS0YxnU*sUݬlosf7 5mGw+X8kv;!|=?:C:3 v~yX7"&>r(Lq?+I.JOï3Heebnx9wp~
s
#r2Yi4c|RJ  r#^F ۣX=+پ-ϷE;TW$R~ǿkO:3߄&f;^Sc{/KkɿFeM56$ĸ@e+'QЮ,UE1)"PkckTnvRny#zDUVѤBNJ),Sg 14'PkU9}&py*5Y1k};npaRGMݾO)ƛ=yIEB3]C}ǷUL$֪d8ŜgOrF=V8 h
0=A0-Id{xrB~!S3ҽ$8ά,@F[	>zRsRǖn|?i&ayX\=AdU,zuu 	>/ Cfpd yrǨ5ώ>k~]C@߯i)mcZ=~ep?z?<NA QIޕ'89ju1uR?Wل,j˧0jMsA>B.	lTD˅梎=ۀ
 \-1vYxav>2rn܊8ǩ#@֌BR{"%i f5NT^95c*&.sxRrס-7	; "sVBY"&gj(r],Np7<I9j;`HwGzNs]'ʼPz]xU7
+D#ڧdd?fNFb6,WG^=)QY<v13֋DlpI<\kƕܦg*OpZo
?C8''?Uau6fed¹E =*Ag=\M͸bnUnؼ]-܃]cԷGˈo޽kum}i)ڸxٔ%w+NI'??J$DG4m J0+,_(I=F8i?x<qX/WR)~_c׿z\Jz_a1t>k)W;[qIirk1'A˿ࠚH=K:75M}讝nO`FHiqTγEs;8ף|Ǧ[(p?O&q4󁁏;Oe"Y` ')9Mݽr|ɖ:WuoϩƷzMc(y/T?K}~R7z&u]c\!Io#xë*EvvkY¤mx
~皡<q0bweJsNs֔YyUo9s?5qʲO `<So^Y.d81Nx#  {  k_G $y?Ou}sTهOMKmٴB
 6 `zzHJWNzFpn<5%JG)9;rj"~njܨ玵Ԅ@㝼2NMLi@K'S@=ep<v{?\6V1|
&*7=k˾xAODE5a.A0?vz!_on=4>'jiTnޚ͜Rɉ@s@V)lw^8Q4UĜq¿6  SV
'y&>ҘC!'QZ2) Y_C>9--['ɛࣲq@zP=;Tiۀh	sڠX^V͏D^?݆(>"O5[7] NK8IfN~S2G2;g4sPh tIB\xgG'nj3=s@ʯffCL[jjPTO!-2z2c8& PkEżH5T\ֺmүƬ9Ld,clyBã :@$2Ȭ˦=\_3дʣ5C\]j6>RV?}VlTΪ3S\7WQl[w+\ing-
s"Op4ĿF4@ ?:Z;j{炴echUT]t[A+szPVGwbE0J*n8HNF[֣cϵ σ!li5$E$wQ1 T3^G`4 UIc㽿!JXEF\/Sl, )fWJz䌟րeN6a;N |H##L}{2=?>l?SMx%Lf,=%ր%nQ1Sc h|67isL$t#L$
 BTq> yv!\N|0}_-wFh8?:.' bֱh2%d~_^<74D3sk) J1Wލ*aqsOQ}?|-񧃉 ºjL\Ʈ~F=+)u ?Z<M>0|MM"f"GZcC?-|T?]Ik'po5O cjlCY8A*  Wk 75\RG{)H8+OFGtyǇ>+ĶP G^pjvͧG4XQ^G?;lX!^"ppv{גY\}WLg4;ic*#Y><_	Ƃ8|SgCwl	S~Nkڭ֟sdIc2tgOGN*u.b}};}E_Ĉ\VE. bGZ_k/{67F2Bz8?jng# E`F$\E废uc:BS?R>kFT3J̩3zWv/&Y@>׫|omΊEHA=oA^[
򬚕 Zﶍԏ.{phvzJKx)-X2>Ⲧtk?BێV9+bIhn46i|n#O'`x;ydXysVMg%R/O?KUS.⫎qҡ{ U]i'Qvr܎=Ak=bNsiTb=8<p? ZLJ˴rirB0Z6.8jd#8稭3nķv{T b2s)K`ey\ԉm|͕F} :{Z=1Rg,eӡ1&m#؊pH;3--4Miw h᾵P[_ZyBB~uG dq_w69+Otl5K}BpGӏQ0Gc^;gC^<LnSGyGݗ?<6qJ>_}qj?,\x2c~oMл*r#tu*Â<=}GzROjgQ[bsXcKtL\3TEB@?ҹD!\\'j)b3ҡ䐣MMx;]KvBdq#Q''&Nw;j	&|[fBA$O$6viBYvqL@EVb׭li,Ȗ3y!a0NUc~kd<XG|*FzlQ >FqO'T'` (`O|Ztq||`Lgzf%!&@˻NG=+.dh=ܩ{xpF݀qO ]Sgktb0z}}jhf~?s˞a:!*vJ&V鬞de ; qǩ\,` H`tk݌pxV<߼.Rqӵbfg%yk,_4 R{;͕\ںV1Ď ºBYc88+>k1ke"daS ץh:p3/(N{ך[L 1=IDl=?ZrWkz?ȍ=(!yx ~͗;IT AWTlc?!Kt(#LE HOWu9py/ɿCKS&gI1>^
xbW%y[PCݿ OW' !C5=8~~Gu:|rq,䓂G~QM2;v{u}j<v,I*f-G,p ' k[͊b?:L~#5s}mw D+Pa j~0vMqBF=N$I#}=~j0Y]lcRk= HXoyXȎ$S=?XgEbz9_yRSt՗	ݙr66A$
{}0Rkjdy9jR=k!ӂ'\tSΚ	9)"E_~>6Αi[75lls_X~~⋤Бȷ h6y{di
! $<sjѥӵUcg7'>NMnIz :4<sҡ{PEoB^uޑY]w}Px> ɢYbdw+0UV_<([8l{Վ4f*%p?7˺a[Tsh=)>X+U+sޤ=8[A@x:"SqQ@żMhn=^#ʷc$C4mwV2	{M{資&&I?Z #dzUΥo!-;g"RĚ1|AH^~hg8.W2u$
p"0%bmaNA9䚯! k%1}cz[9HhȤ,8g>R88R]#:Z@UrqY/Vv\}+Lxe"qӠ\P}^54E]^leE,OOfR
Dpsi?/:s m)<u׸xCM-aBq S$ˍ~w<
!E<WQ֠n:>4}I.n*S$vc{ԌmvvڹIx9u'jx sRl!儷^=^cR(bXSdH#S֑͎y+	CI3}e/ϐħ.&>kPQQO r# ~fȂ5Ó=i$l
pf#ƓY?dǜ~UaXڣoS@$0?:IcI$P 9Yd-? "oI;c>E3)SE4b(Ue-dHLzo~TVD%_OLtOGYƾ_%Y&6?ڀxi4#qH39R7=i<  ԣ4ӜԘ=⑆+6hN?bkǊԯ=juf @Lzp{Sωi?Cf-X6X)#iRN	esKc<7t53E/,7H1|W"&PxR]\A}ߗqy(yV Sn,NPH5K_۴= ]d>G5Kdaz~#dIӭ;Nt;hWX\gMQ5S	G0t}1̬sӬR	yd}OۦWWhH4ۘn$2g}y(ΟďfxUW8/2zٙRqv]|uKs6tmUHkmc0c [=7ʎp1eGO=S>Y?<OE Q6v=20@!VnW C=N2CȬ.r9*d0%42Ʒo>(F虴Bn  Jr8H$543u+d۪ 1|GLsH"/qsi9PŵʟRn,%!WS\zG-[>rA@Gןzx܄֧:)9 R`ן\T8秭s3`@x$m,rqxu${V<Rlp+@R,p"X,]s"񶗢m
Ҽ^>Jid*r' 
ޝOSQ;|Km/0?E}.b︒D2\^ʨ(8Wk60?)Ҿ ZMvQF> 1ixF*[UpS4wa
#<J́}V_j)^ R:ݼ?{<7u[W1X|NfTO$0X1ev VX㙞XyLmG-`Ia00F}!C>#ikXL](]֏g:j;F! ,H1O^1ycd
Ī?׭@g5L2g?*75 5F=p#p=*ԀZmGqߏdI/Q9䑜z<.mpYy<zz	7#ΜcyӽdZJϗ ylh*$	}Gk渑+P~+ssd629Űֽ	,U*\4ˍ*%PO^8yIzNղXtB8￠{=~!ռGqq/vV&X,E>5s_|1VHLwUd<c>Vދk5h'Y׭[1T0ZsHgx}ca-"0p2O4RL[Ar=O	3@m}@Gza2}R7g6˰<sg/u{uIu,8 {{W{T6G[5PʰwuS9UdO'&Cy`GC֤2Ȭy1!ATIm *EKpNF{s!O=E0n8,;W*zJmA }kV5TIe] 1fVFc;U	QX,}:V6 ER(dq&W4+sQ,S}C8tMD41裖y?|:xC%yJO*ܟ⸐u _NFP<'ʿNO	Xw!O6Rç@="1zSs=靡h##"OO֔(uJq<v3mAO`:U8T^JƤOOC'PV9a
yG
1ƿz>u`q$l%F4:aFsS߭V4^̥ZDǞIoVzh>Ks^ǂ83˝ :ץ [o@-3z":_5On:2%'O:Ss@
Tdd֞IO4 l?*Fldus<g 7s lSA=qMc}HT7j_ʡ?Inʷ0&cy;i"66z{Ԭ#Rqס W'd&-!w_;EջaZZ[K! '38n(cNT vcFt-⍦k&IIOSYWG9sߞ<`VlGɫe!\u) &e#U?z*3c󯧴+_&F:
 fڃjs]4O_?Aƾdcյ~u,cc6Q1b5TD}i@\̐D˝2}O6닙5{zs6$)?X-ޤbHx7dM.f/-ay6D]
?Q["?3A)nP@#,yvs$~Yϩ)O=C6%*O@1YԶplI0Ϣ	 Ql?w)q|EՏLw 	p08ڛ
VT(9RzJ>OT}iN;Q`#E<eꧨ~rsQJ))TRE17M ^OF  C4lβB@ jTger0.jN wFAW"& s@," ՜jhxdP1@8(̯?VisIC  h<~iwtTH(3N @tN8hbN$Fq)M  M;fQS( u=(.ofv{b薾3#ޢ#ȧϨ$wSĨKF>B	?'q ^Tܗs1\/Ŕ3kv. GgV*PPS7UG!6id%0<ά^jPƽhMM]=RX!OQSn/t+Я&;l yz)8@zU] 󬷫oKҴ 3[dP\I<_%w_= Wgv DZuyJjnU 0>g!{?Ҳ*3έCۧC_4}16 vnSSEr#9 bv*On0>qaوϖxHHLw%`s^U)ZW8*ccN\\)y!ڭ`e߂9k ZOi?ur-<3}7Ś=݋9Ftap~*5_ևMM:G~Ҥg9jֲ^d.O#XwFCV7lMs=];t=T7f@[ /=gNj+dfVʰ<vҶ>o[[$Tҗ&>h;N>\}i63OjʺD~n,vW3á@m7wrBrI\s^(MJ`N$}ӕ]akC54K6I>\[v0{wC1u$I$'='J.r{d
2&Qe!UÚY19ewvp-t"+Y\g|xE_UJO8(VA ʪ+	$G$2"SZ#А1 0\xV+v8e}wSI8RM+!'d,9ڬ¬J],WTdgrG<lvT}*`NI$Jʡ@qRomzE;ԑPG,=AsRmp3{  Rՙ}r>^N1\c׽E$P?CR*ҺclyzzҀ1Bm#ȡ_H<Kr>2!fL/z fǆ?bKc D#?F }V!UUwo]*b]0?7Sp.h?0axatm?OxV$m.IMHlXl(=]Y#Qt>9(#1s{S$`cw͌2;zZULF8 s8tU$88IFo} z$iF\9m+nUT񚱂r3-ecڸ,T1LYB}ꜳ3F_Ora*@Σd\F@暰-z*YcdN:Wy_x$5ezaʿ&i1iE8!PuI~CWYv2ğv1ZxmIp~"rZ{It0=zҘ=8Kz(-,|˩V}7u?5} A裠ȩ1a2FqS.49yoƐx|(,APfuv&!}Jz,򹧸'$֚N{HgޠCPI?O٠#~3W1  0 ^?¬r̾cWnO%4A<z=;FiUQ{U-]8*[B1OO;]mk
wc V	z ;~jvK4j ~ ۫ZbFUFY0謻}{2O] ܀mCGOW-57LcБSqt>uk/ }IOd$@)<sU[YRF-#nw
.2f\Ң[ugu.?22yCz 7
qҩ99rIҩMY2HĠ.0=ϰ/A͸j:Բ	ݜn[js5ߛɔ|Jdf ]̟º;ɽ7}+mbtm
ڑD?^^+֟gqWAۚ-׊PB#'֣$g<+;Wd~|ZL2_d! iH$PT} c )}ig12hko!V7pPC/
IRS,p*?䷨!U&?690_ҁ&xbO)<qKϯ6 M~ o%쮂[YwN
g5!`IS{vQ3ꋴʡ؉n[=HϱKHʪ% ztuvHT-yݎۉڀ2Tx-d}	"aTnSj8"j I)$l_hp	
7 s֮9b_G=fkT\}a{u54I:bJ`V\x / EoSc6:sMmַL1/#!si 9R:)1x:SzPxH x?Ҥ8縥1 Q=iܭԜGmDRҕ:yjX8- ғcJ#2L7-K&}~jOY9Hf6:V!@j?SǂxaZ!d.{:P|_+;W(͵-{~_5Gq¯q>?ZNAEH\rFvkJ>oPk~]x:FY\QOzFN
b8 ?X*MzƴjZgrp70'SJ+@ ׵x+\i=?rIwd0!G2(}od{9%Fva~qUϖkG0ؚxsA5k_]^JwIq3IyAK ǥrohY#>|^k<JQE q{
Ŗmg+j<U?חj7τ*Z
q}pӭZmCKn.A2t#cȭ$c#ex־O<~5׃5	w1ofX|7}MXmn9=kL~XD=y0/Y'#ܧr;9 ?ZUajHn#vFxygJi6yXai6ds7'y)]	ʣ$Igι^Akԣ4rOc;zPUNgJwi^~#O/zKFwqBfO~jNlc/5CJhMu
zK?
9#l4޼c~Č* #NÊ=8E3mA.xd2 c'q@,ڇ >DlGSDk309H&[
5 dqj0q*#֐m3M#PFv !Ic8=qHYH6HTU~lM(?[TLѻmws:L> 4# cbNH,O|#ʿ9.ҪeWRڳBYN)<+8*$t ]tҤ]}jFC,j
Uލ4pNacRJr94[B[Y9ZDO1+˴"ɬȘK:+2vxK8+6y2^}g<Kqor	opzq|Nq/eҟ$tSZrq2zS(ELg)oUy`瑐Q_nsl Wȥ7m-R;2@(1 F̪ܠ-7{S\j$P[nH uOj=
sZEI"pO	w򍇠ˍYb0Olq5H}ćٸR$
J
>f<!EsbW<.4?q۽mă#n yoWSx]tX L#b߂WVCvtTQ?@)7mؑ:ǩ@*H
iZM!1ݙ#\j614U;)?
 QUa 5"K{PjSYAE3})XNߟWcK*GMs/<*Ӹzc=ꎡ4oo5dB$ZkXc'9)¤BIQM1{|Đ0pwH$2Ϗr߇e)b\ /l_
2c'qR/(S$'ZBxPzr;g4ݥy!ߥQ]XھLM!A3ҬM4pȑ~cSkזI6!,	Ѣ%X-C, {aO6s\\9[8#PU K(FYؙ$oõ)lZ{Q@/n*y} RY8?} GOzcm_O܂9 Snc\{7%\O|3S_H^J m oZ@-!#YQ?Ҫo3`s
3@OJ κ+v{Qd޲XZ[M=2#pOYӾzYFF?FCM
L%(°XֵH7Ğ?GU ^=dcR+Ӿut/ڨTSOS5vQlqWZq9󘍏PTG@sCX\:=*F@ae\~"5y)%~1G"f8g[sJ |W)q捃aҬrm F?Ҥ<Wj u ԌЍãQJrҪܻZc'&8=JN#t=ې4y|W'֥3~|zS?)Ph$\4fab?CXss}7dFXS@HvFM ⋤GmM9+'prIuOu5$VL7l&Oa~9
nNRjrLcB}gq >іU]͟-O@WoE,0%U.s<R\NK4*^qF/n\ʇGOJD֖5c'w7!=)ݚNXO"`?AiQKq( <}S֜NvqH Z/OƔӱ)כaŦ`WahXץ(ZO7krVC  CzзsJ_+{U)51*kϞV_
ڔ_kzh?iYMx! Aj݆0H55o_`=9"LtW- 1<REzԗ-4xQ6*]U9lVQҭ "i%43++R嶛 OBvdo_ӯzL1ߍcR+G5VtefstJIq{Z&^Ev2N[dqF0VyZFؒqVK\V)TLz׆u7X$\X\$>d~55^[[^Y[xXu_^V+ Zm[V%bX<-3*3?*%J Ok{Sהya :&s5UGos|%<[?9P+֮NRۃ^I?6*V:E#Б41RyiI㿧5P8wQ$RrGDI'})`POLSp)Y.] ^Z	+\L m~,ňQ YR;4T[TOd$mcC_,1[TG(.8(0zF`H_n7n鶖 {Թ#6pi^ z@ǸW^@g߁H69&9;`> 	mA;xޣUU2}cޮÎ>X2Cq
]>@09<)xu8 nnauYcDvnCƄҘXFUUg#0ɹBF C|=9HMџ|$fRW@_JF8u'!23.8lu5SpTdDOV<?W,IXxuJ??7	}E䣎I\dm >V)~z-=fnNz|aKCok~OS=>gA!Is(*'S-خJ֗kR͙T+|Ehx1ׁ/iޥ""hmUɊVDAdĖ<ו-_qrJQ}%ex_>6kv$<
gRI'X\מr׸˷u]\}n{ƾ>hp+}a't]N>BsYq]~+jp>HW|5_SD77Rqi4(,lUrK#᳜Nl厄^3O24m@o?}A>ƽdO.⹆TWD` ?ĤviTBw),mzc
ʍp3S?ߓ۞{j?p'}CX6v/3TeTsC9>SlA') {5in'"1?	=ߕk̎>iߞ{c'?5/fkl~߉,j̢Y>Gh{twdR֓#a:֔M<YWXYp OjbOn)?0sZ vsϮ?3Mx(瞸?J`Gn>3@''B})=}: u'3j	`ESb?h}zt
2e!3y[U҃
Aa㶂&14븖mX`b=JfwqZ\G|q*}j- UFgJ֨ 0cc	]N /D&|n$0e? )i#QWQqd+=;Nsjf y?~v	1ƸxP--mözE?\>ۥ#9 Z]XVr* aL<Nx$P-#O6m{8?v>=ULn$[@b$+cͺzԽFS򤱶,INz|\#>pVLn*HVTmL0A'Y2YSAkǲ~燬k|N:uhiEoʾ֔BSIOΟc6:WQBOZ@!1OlRVX2M4Y)iȓƲDۑg=Udl>b8oΐJhBDs A'ѪEdO2qJG'=(*ϲU0z+?tL$P#A<c:p|n􀕛 v<ְ2ײ\~0<>5YpAF?УqY	7nW,mq3L&R.- YNTL9Vi! U h>¤k?4~?Օ¢hmQ%m7I죰G<{PWN/j}38Tv(dc)IGRivЈbc#,#E?f%b0Q߽H9#?P!:8ϥ&y֝}
PЧ4\HL@<Ν9Cߥ<7Q jwB?t$v0&wH	gz9[$ @xVpHS6¾yOZ#ؿS Z=R7cZCZH,qܟA#p;'V:__XQc B5$;&ϋuQsS$kn	\Pjyqɕ1S'NXVP}iձӭN]}{Ɏfϻj>zriyC#qE1񩱷tV6^޼}*9&|zҪg3}11-Yn_:4|澃aeE!ƾgDljs^G7?UP@`HU<sU<|Xtm/\V	`'ҿ4Gޞ$d͟41zߌ׮8%2p2)<56ݞOƛ</Z
xgR&`]US?tm??ƷxuW2"cO SzLz~<F>#7<el$2[GBWN=2kUuNN_iJhӌ#>FIv8CV'T(/>b\Gfvjx?7QQN+97(.qW}<bI&56gp.(m*osU#So\+"?ҐDF;)HfI!GA yEzd` b#?B11,r,}j<c?jjy2ǝ2
7@:#c ;v;y=IH=ED񌁸>P!\|oEn&p+cStm6YݕAH : =kw?/KK;
Ml չW;;J`+)ySZǈ$UH,.R6NW. <63sm$eb=cQ^_ws%4ӹiplϧSZrϱOYJ.ppr<A;TME<AۄV|:1  feLГ^|a]|Iǯ}*&`սTMʚycmt!*zz_g;]B,LWA#<g5~:XwZ,9?w*>bIZ'v/0ɽAIh)+6T{ylsVɨ]NݹpؕsC9+o<7dc$lCt AȻ[% Xvya"*JfC*Q#{aJ᳴gG~FCYcһ/_Q-6;&ݏOǽprIn5QOrsLI226Uz6Ty?__:-c2;r|ﴜb}+|&lObՋ	2p}}z kxiFA"gpB))Cv}s?/N6^]q.GNסGlҞlfƇ叏ǽU<z U>m	x#9 1H_~c{8P|Ґ9<w2ns>lA~'Up: 2OW#SGX۾"Fv?*z
v:gS IX lekxk*Cۛ8k'9gW%<pzAsPXI9' z d 18!nuR~soVMVNg0BsL,I=i$UK7 >^Ѥ9g
&X*\⁒'3׎"4g'n{5㞔qsMO| Ն})3HF5FFgo~ҠwH
Z[ޅKg**}~b?"qVd;F}:~53ڱ0fJ=~rs}+6:i0(?J~I5bGUlnjɲ$+X>3?t;˅ YG`~$]p^#)4r\qqr?6)Xɚ#cV3(yc^
mAX>ZĠtG4p9ٽjW8?JjFS1֐ʏ!MdUPyԌi<MG4TLցns\)D4hDnVE:6*&ib?{UR\sR
L#ޝ *V88oHC#m6XccޢbiJK 1 /ٛRBP<_#}rw1G?3EFګIwR*3DA)[&y&ؾjX[u
,K" Ѩ6]Lq+},}!o迩$w'ԞiTPGu94!0( Sj2 _Z`)O9S8qր ֐zAv27ր Ҟ=MG\Tܞ3S sNIG7o4|U7~qW.!rR$?E\ߪi6@ CuYÏWYlꦬK3l μM-ͨz)	ֽn^5QӟW̿ZZSNUzI+?Z0Bq6xP?<.0`|Th91A-'@@\ڥM>79H q֝Lf.C=+GN1NgEShOa^=ǣv"Uqs0Az;՘֒se<p>m?0}w='F}&nF:UowVc&@ck|;!Gex*j*61?6?ڽ#e芤9aWpU:E<M>zno.$SnGfjs\Gm
p]?s+u 7,Sk	:9?J:ا/CϭA^lLcBln!񾫯[>ſ'SV$rs14~x\~ >{[EQҥ((tTn sR.HR`O51z=*r(A kgؼ6rq?:T
2ip>bG|b~\CJG5lˆcQyy @ .8  ^(_vÌ廓8*w#{˹`l1XX;GLu1۹3Uj}OcڧI*5wm\Ã `zR0pC.OoJ[v9_֞nr 㡠2adB7SY>\d{ƘiW)f:VvѤ dؙ\dqOHbKv'Y~\x_.<]2+jwx,F|"Ӣ>_Ԯ幼K$F=ُRj<4,J]w3ŏ$R*Lp,9<2mL| TӃҴk'˞9Ҙ\:? ?ZW;rOvˇ7|Ur Vq)m𨜩e
~\dAsV\4`?`?~vQ1pzn;2r=(v)=N8ݘdbLn'w0bwF;Zάz /򇿸>0S
RiGT^xӤH uڨ/O3Xq޶3!Rk+q٣7SdSztDB1{פx#ƚ[d'&9Y\C쯅~-񕥼ﴴ?k!Oնξ%tNɯ6 OOn"+pmD_LpaOO'iZu<-ڂr>N1LBd "^z9Z"<d8{裒[ 
=Pg 9ץ !`ߥTǘ6ȳ5 VeQ4L
ppiUB"ƃ
Gp) 33?n.[`^*?ң2.L|">ޔ0DW /
8_`:RG9=j"H#?)~*`ܿ޹m=pO#C8@Up*2;*ݩ^ VXo{aҫbi. Qr5gI@ ;W<Uk-gVrpo {ڣ|-GaTa~@Mey5CiW欷юWOB ]ɸvkLdGUKx̲TSYnO,/G	72a~ZQ}6<jǏLuS66rNIVx@i05W'
??MB*Ǟj.͹@f5;N=XϷZ̹HRI/Cgܿ^{ԲNN8 ɢF٧DyY2\8"I\=t6̶P~g? /SHoop:WaCòh[*s~EsMݢ{!#*2!qJN}+܏J=&<@	f%mIz	җ&d'F^cV>op~6s^Ў:b8JOڣ4kqӷz[W#Vl3>*[E@||8 <ny qސׁCrsIpin	~(ZrI1nEf9=9-1Ҁ!=Ҕ'o(,g? a<1{b~4 m&0?isSq}R`g'i4oo֔p{u4ph;)#c 3 rzԀqЏHqRO>w)p+JҟĆF>2A@㋏/jL2"['ro\;/Gn'=g忙`\>~¼s*2N  ?|ICZ!Kbbk֊/#k%}~ce'BG|c$<ֻpvpdH9) SJ:gyQS lTh2(3l3R 	qS*f\zR 4z	IR)GHm?5/N=OCpqH.	ަC=:}i`ԴRd̊=}1ZV1m;6B`-_z}+/>c:4.Z6^NVg/.\ZAyz*Q8k+UUFOtF
*VBxIZT?kIeUi45V08g`r+`Ol~t_zy:֊s﬷PCBv#$晼$A4r]بf t⹌K+p9Q,[-Qn7|L$枳U ց#sT~I 	1ȻQ#ڜ;W;d"ak`q"X =iJdylX`c8+>l1Y66Ac(\ul[(@o=T*۲yA\vŭa\¬ZI A;|o9c
6epz܃J/_+[ˍCh/VᑉXwO\ms9ŻjdrTnd4瑌9!CdZe%ŴrI3sQ}9e\x$ZV/fJz
c^qJhU{nSJc1 HځXGO-AltFig`?9_+*+:sƘϘPe8U|2\bg0 U`	03?TA& xB`DqZB
PFx~v#h3cEhe)~[sW㧵3Qγ	!<gЊ
HǒLU9N^ tbH^'4/AG}p>Y/+ &H>;EyoOvs̮=_)oCJqG	Ka5 
>TC,ԒjԘPr|<^Yz?J\4Z6ynӽ4, ,~Qy!RpJ`9NO/Joש~֓}ߚ @sif9N)Ͻ 4lўzf;9T t w7 }(~:(<=.:iZ (! BƸ?ZAR9LfPh\K¤b(3<TLoNZFP2-ǝs qY̤+yqgb AMFU|"#3r@<cju.L'RjgmH	8x'I%(3ġ<:=GCYbEfOvJPb j ԟ,dǤc@A<%sY:ɷP4jqǀ?:3gw6EKN	1MtcC%i֖v#`!;8+*N5vS+,I'=IɾnC)$Մ\hV?Jɔ*޳ЖvԄ}%<ZĸW|; ڶ <6
0n_IƣҴvEGк:cҚtWQuFuk%	߇Z["n A'3Ҙސ8׵!L$5,gUvtFU-g8 Ղq֣olRޕց##U9Ρe }ٳ>U}=NtH7TnqI@L47 dZ_yZ}1׸H[H٤c0	٫O'<vlR`9h;qqIԚFGzU@	~$Oˆ>ZiIgĦC5<8Gl )'n|7Cq<rFx`dqހ?/CQ9aRDai @:09ќ<LE9GLBz{y+am5Ѯz5iZen.ZvQQy#"请Z2>I),đDi֥6w$g泵ySO*Y5	 _pϹ5_I~պOhǞQ ?zzkԠ;Ա"Ԫ9>:t gzsQ N晓cL#ݪ02yZ?Ͻ36H$S9iQG'rA_jEߎҟ3qN8 |''';ӱӿ֐-qOd#cdu&gzjqC>mn}ƐZ:mnc̑x,IzWqtURG|T0bOw~H1 CG#r?Zr*P2~n4?@u'3>h* <\zlL+jS#i<PUϹ<ҨFlNىUUq ^ϕO  OӮiadsݱ0gi><39?79:YGS@JGi.wl,<~qb_Tr~sOZlcf0;O >I.\ʑ[c2HϠCUU,y]
7!aޝ$/ +K`^Ml /qd N%O5du(W#k>$aKa	9ǧ񍲢VOP8O۽  ۸2 'ZI鎵+(ڋbC?j ̑ٱOoJ	8=Z_߭x޷્6B.zn#ھ]ɷ;p8=SIGU@W4g(T|&S|Mf=Q,vx$GZw"6Ъ3`zW>$LVAZ&ey#vpmaB'T GZ.,rWo#8ǥgg(>CS2RPFzgF*0@=xϥZ==85^K	y#sqL$DYW`rWS3DgI<mI1sG 1ȿPHRx\ƒݭZ6*O;ӣNE]ԆfN^?*HKtRΤt^7EѢ2v.O}N<3:U[#{sN>__<WxP6?gb87Rc tp<??V/Wcmnkf~QqϿm40${R BrOJzq1.v[\7Q?ҭ8N¢l~{ xc(䟥$1>s,lg?7G	T GҦ+@:@b]!(YI9;*lvtejU>Ѕ8=ڔSU.6a@H}x< a.#w lgQMjzCz|'J 8 1Hc-~tuu#ڌqf#'>Tm7wjWR'
NqϽFܓǽJAe?Ji@MF{<rj}z1ߚCxݟ\Lw7~Qu֐?
#z?Ƥf	 G:+&J*޸?˭2gTU@ S<rGQHG^fh\wk6qJo"ʏXvbFO8_J)HKpܜtj'5<?JsY2SY>&IҮn8kiFp>:ci_7,' d-;qwu ߐ#?
3\E(pt`Wt,Q'v'j<֡cVAGM$,q,p HV^)n!6upr/ 
CGS	 q)=j9޴#}{qQVĳ<nS+BpNSUn;H}LRXґai#a,K"'(aCP1vזG͝:d2;x:4}ր>\qƃjƠ.x)ڞǏN9Ciqo- N@>B8G\/J lr}sFx ץq}3ҍ=3 Ufh]~B7?Zxv«
15^Q^X~3<Qן֐\hb1I0~{ T`",?D=^ԧB=>W8Lq R/zS4 uvig1M= i^;Fx+`-&?r\kأS#xz57AG%ۑ❣Q\ߺ%fu3 C'ձ5AS]X! *W&L,`O\{c;W}l=.1y^Zִ|IOjk٧~@~ur#.iҦQLUS1lz2 sMU1Ri11֞PO?=S'ph rqR(瞣0(\6($q Jc>}j ?ltTr=sQFrq5~izWuuobh4X͘A >c굅ZGn^ϒCÏ|DkoXbqy1oo,}+O|9E}2%]wr$'>B[һ-#Q4Ҽ+6BbU^.9=O~x[1@Ie8|ZWH}U{s_fT;;9kX:%*x!N3M{G&^H\) m_9|!F_I]*~8t)*78$ W{xH&VNM{'؟8dRg<H>(\џ`(I)XeFΝtA;CHp: נ jx?x:H;ژ2折Nwh`r`W<c6CsXrj?3bg F'=w$cM#r@xRX#sy 63=y }3P#yk~OKdܽA\cegܪrdӜ늭/ʟ($tϿJ0&gbOCƊW61߯PzP",U?7?/'= <ԑ3r1>PW96ݘO` פ,[<	S0?
Kl33r~A̅$)'Sa#RZF#g<Ɛ	U*r8?8Nwrc3e8'UfQBH%NN~ ؃ܠ`NXdUyIh"3ܒ.+m =3I Mr%gxӂ xϵv!1ϾpLK
IS9 ?^=CW< ALJ9}kѤo9[9fU`o+{U`KݿZ#7$hٲ2s5d=Ac^}vd|5iFnA5ic{;n2[YprF*:׏OW{f
2y<U\G5-nz+U,G]'$)gRWqʝM}U?}傕 B
ȱ2H* StqƣϽ{Ox}"M/AQd;!l9(gy/^hs' dB.v??~g3JH JOoҦeicqAC6z3fN[AW	OG",b(nXciHww#H!-!go¥A}>w 1??Z{/;Gqǽ Wod +yls 6R2Hgf69 Џ^"9}!`'fYWnX5>r چd8D/Wj0oJ˖CI}hgn:
iV' >R.L0/0:*V=iD"2]̝ZbyH6ۃNc
¦ FxH'gwZ ǜj~d?ɠb<qҨ\HICF.RqdQӿS4[lY3§}ATU9ڠ{TO`?ZGaݬn1VjYX1Pu8$wUݲh`ҪJsN"t "y&x$+c=	7$ucqz3FGz˹׊2'؆HdTŎ}+&X|/6_LKiC ]#Ӭ'3 dևKoC}t]jדdsCJ¦+HoHӧĠ *C](<qP7'@@%rz=i9q)j+6/#9?*{Ԅ5`*Fj0Kl߼3ǩyKޜWpn}e9dFq܌q(=0?!yP
 <i/y9ڀƫMxuB6N0}79s >]8l-0E ,ǠҀ%osA#]o!a1T75(lxn48t98)^ ^V#d$Kw
q#l#60ny?uARvc<@ 
<pj3I SC`ws0$%WM&1YN'ێC
Qsc
NG͕u#cv (GZx\jljv3Ӕ=(cޅF0s@k"mqp̊|iٸ`?׳|G/f8xwkǜ£$G ^x餴#pv
u j%ODsr ڜ[? + iM[;妟m}SPHsGiV\l6ڪp0*^?ϟdzTF9*e^3l>Z(<(>jt36*?*z''I"()ʸ9?Z@wyzp=:S> ~ 
Mi&%9$J}?{:ƭXQވa*kF%v͏/kwY{?}GNXJ#Pp :*6$]I!_TT/#\-a(oZ׌lMxNR0ArXCƧ^ȞWrI$$}.=b;{"ҔZG>>*rj-?$-X(}	1kɥfI<CI-wP&MȧPp(#J
45DW2LqޮI<U(گH?ЎB-W825$>Tmx`ƞNҼvsNRK.~cӞdJ&s8R{rhǵQLGe034w Dls5:rxW\{g[=$1bgɧ^> bԌ?5J9U}p}=#Svr8>GP2g|cw6658eLp~N&|c~K3*ȧGrʿ@{!yl#d0:GʿwsG݁3 *G$nVVap\Jʣr|ydY2y© (9餕t
7\y{P"I	OnUyF|4@1fGRٹoǓќcېsSӏHC|Ḱpd7?QbC%܎9>֜DA-HלpxGPr;O4f?-H:O#֦YH܎O'qU%mr~뎃H&MӈUs;@X s,OC)To\+67vy<S%sC q5g# =kQ/?7$ΫH?x۰뜎ؓ
6dy9zzpUiUz8YW1Ȳ9z?(©14r~PCP:RD
n\ ݤ{g緽;o\dLW'3φVW7yzX_ndD7|k?w&lLh#m9q ~_0ľiء"lEQp  =3SlQB(T0=>y}:Rk-Nl8)捫 Z0On9׷῕Jj[#OlCuM9NG Zo<TM#Lc1ϥNOpZP8x^)J`S}h6'<v	뚑ہ@ӥr*F{@(2 =: cM@©TPNp:OsANk|P]FNyTzRgnsҚFAeme`J<֘`{[?l,WTL2ryiXb1
0S:S'ihfH%d}G#~$	<"T~ ʜs"x.(rK`短gMtI<ܜESI{;cq,#ۭ8
Q!Q e-=Y9rȿ?_B,r瞽Ve''j
sVZLѓ/&b;V9. soȁ#uld$ѷzFIujŖO
win}~5GT
1Ҽ|JEħ7䊣_JyQ.;
҂BZi*uޙZyOQS߿EZcC*˱w(!I Uc4!$o.qja8=~Rɮf{XqG=ZaҚ9ǿzE9}{5&89 X㎙N=MV6H#fC 0|N9'?,yU,}6>WaOus 5 &aml~iw,{Xʒ~BrzJ 'w֟ fG;Hz~1M*϶	cr
FM|MH YV~k[Yܐ 7*| /hJYYF9)u;F9}Y; _Ƙ
cۚv>aMHɩ7nF:s@pu?39p3,7JRI}HEM^{p܌/ǟҺhx w-l c\d+rڏMVHs/  _+ڇVOi
[=W]?:j52\ ~' +⿌ڸ>)7dB?榺0͋,- y 
jRƿ@X_^Z3MU TQOT5RiަCޡ`t$x>ԙGc1ӮkO^NoiH,"Ac;J	ODiJMB
葷O^U4yrDHz~'_UM¶vG }{95O>oiK啇40^ԯҭFt粊n-⧧¶ 3̋'UB֤}Wut[7c>ޟNoU'̒?$?\׵Ǹ+XXB<Ih%U;n~0;kWFcrGa;~uwƾ+wL~\H B=߅}	j}sEItbG̈́q!YTwc+K;7iBnYHaE}xo?)d#E`;{vQ_N_
=Y0U1*D||/jRxQ-H2[x︁|d^[,k#)_qX#=Ͻx7=vSSgJwO,Fp=ių>0;}jElެzM(x?$vFBA\u gb	"~Z)Wd}7mrȫ錓5v	߀I <}sֹn>cifP11ۚԎ R[;*o fis
sL{"0q98âl@={ՅE=p;Ic,lɆ- פ[a8F3}雖wTcsM<>S:hf7bbp	'?AIGnŕX<sv:ad#m p}VsOst0R>PW8r1| #'H<viTyj^Q;˷5qax  > 2Gb81nݬ܁7pF?>?̏_/r9{
B#/#`dw =DQRs1iǷo׵-;`TQew)CH^OJb$
V?]?s|?q(?S@?JjŲF-.珯=?ƋmXbrqms~"cs;.R)b׬ t~Dhw`&?y.?k7EUX.Њ?@^V+0v*-WMDK6F y[}W
hIuxAUI>Xl׷[Ookӟ75NET-kFsQH*G9Ҩ6{~<8Pq#5UmInScgf_Mc< euTnWC :θy~D1_󭇁@|6b 2= ŽK6ϷoNߍzP~^m%eֳ(ؑh	}],$ּF%ѵ8vWZ<Q=4$:{dt"rWga8'%F~%7;eS8#jK2)^K94zRke#Do༁ rWq;42 {CҼJ餕ĞPeQ%7֬ibO4IF+fiҨ e+)v&o,Mc#fG|>V95m @l> * ]HW-IdI ǀ?;7u:ݔW]3t)>EƏmQ?	_x^/& y1_)d-O}HU'Yk[S!ima[}\Q"1An:bG)"Z}ֺLÚAQ=}Wxc>Ì_$[|B<j1#k[|nt]I)uM%NOiG[IeOkfb FLr[,m	}7y4ݽ+l;>|V ~ Cg'w>lj?1[*ԥ5:p1׷ji\JoR w(Ij+*y?:;At5c#qҠLcz}{ԇM Dtq֢9>n3ޚWϯ5X"əU|p\tǡH'8]149$z&+)O*>i^\{U)7p=?Z3y(tٲ1>L<S	WxHXҨk+ic4X@jF^k$=>vsGţ<~ԺBZ{&zp O:jXDTP1]xWm5yF={T`T/ӊЂ&<zIFܜ8/
9aMe^S㚍{Hs%i漏_r
]UI_Ҟ89<zPdey2⥦3֐zނpqҠ&udhLb2x⚧)2=A<,D\d8S<ʼ}sRG R&o)P> MF\?1NK*+i`S\HLwJ<1 }3F  =x}
izmvBJ+,Hf?iGGc6[hbݤ4r㡤(PebyӿH	W8Laܔq4kTq$,I@,< }N& x$ " 3KePViR>OӚHc~T{рOLBr}?*Q At;֟*'OmzE3L4i B*һR5ZIՉaWq/<j}NE* Is:yr{~wv]9Ք2ܿ5- I}<sin$i\?5	@d;6?!zl񲼒8럭L)>(]4c>`
09dS2d8JzҜ[i?AOJiKl
I ;LcO/tpƣc}c ~}F ;=kG(cs:3 ,GBF~}?,ퟳ ?^ἏpX{.6IR(yˎ?zו-][xc<}]cX-FW__HY5SV}}YA5[zWxşcCkh}~\_a&DI}O.ӱW$?iSo7}="NOkO Ęf4c yKCh~?"mVI-&L?=9o҂o5idWf,pdS8_F+)]LF!@#}x^Uu8URsԞߐZZ$6p##?yi%yf~iNf}KINue@So梼cz&jw	~M' ]}M{*qckkVS}YŜwϯZӷ5Aӊ6+cH'ǯz@8W8vJfC ujp\t9M?Sc~Ҋ{c ??ZІ乏q $up~RT:zzw2s秽`Sxж̖qʞW` YruN+U?.=2@('8'sg$.M"ы*l̮	-8Ǯjx[9~جQvAO o帒Bx`	OR3E]D'jv|$810',+d`7gy> ʤ+IUN>9Lu6`?T2aڬT'<ڠ,HIH>^1(Rd{ןMVQl;dRw2Qi2I$8'dtN7y=,bRِǴq@ zk.y'<}3]wڅ-3y׮-:vW*0sg67zY[q3.<
'Ed5Fl_}!g@5ml<JΌR7s};4$0;r{`wOE;&t-_KEݖ1#%<ƣ,cn9xyDw's >,d.7* ^W{naV79!hiz ws?!.9S~AC`2}ǥ!x80rggFWQ)~EE>~AwE?Wwc-9*n>AcǑZ֕c=j^2Kݩ/ӅO %./ǚ@Xuk;  smoOQ}
Yg_rɼg;K,MFqѸQϨxzkX2cz~xqMeV6~}|>}PFjd\~iTm6`Q<?ZrCp&9?=O|M -<Os"wHYK;`Ժ֭?|?Jjg'd?oҾx~4mfjJ VM4bt=hZU JXHb'qiN׾x*7Uڱ~![m"Ml|vYT{ƹMG\j)|,e-5Ԙ982{E}Hմ5EoD[=bԭyqn' ϑ2$Y;>GWYp{j[t+o_Q0  :zkW{V'R $~;1w%>k{66'|v;PL2HXiM. %	tir2+myn6*72. )JV&vKލ$/l[3	?Ȭ#2sTF(rq]坮<UIRbqZHs dٕ֗$9Q$&Lz95Ě^߰Q_.O{;?&E*-żVA84Þ/tji ]b hTvC~gJ#/vO#>hGfͤ8Z5qћJ]~>6WQڳ="E?NJl<E%?uD[lW2l	ʓI$#]T_ng,4:@U>@ĝ{k?^':~:StF@5>3[]0Sš[j{栔*ߋ
˧buWamuoGXOgD?
?qqHnIJonjt'=jD$ïBf<dۊΔveO4]W\dƹk&R#K6|Nb5mo>n?H ujҭUI'T <ībWE="}5 t8VGנrA㊅7QQZ{gniCQ)Hڣ3!<sG
{犌~ǭ1ۃ|ҟ^ ^>qnx F	\YDx_8H V^CͺL c 7'50O'p=mr#S%jr;>p=q0 Xw]bgc#Mt>*54?<?/jL$ڢo;u7Y18)q_zyJh:4y Gd@4v"֛=FA&; }:}ir3tq89@C;u瑠[޸E'h`׊ ~nej-GNJ+
fprƣ*"z?hs3`jl~PkshS>fz"ߕ|ǭz% iL{`i6PځW95V.jFjd\Hf*lr>".Z@:{ԃdJ14}4=OE;ac<Kv_HyT;1tv<sbh|CBf\8?|)+nn<ks,~iWGWe~<v	?i4;oZZڷK 15wbܞON+wlq>%Z}+
困s_#Ygd1IU㺮6xf"?t{8L/<4̂}Nsn;^>ozje|-8k<:XV|9{I]E	$=<޿C?i~<7|26n{<}T"Ih񸦮ۼv\1pƻb@0@p? fkzZ.%Ð%#+Fx-dmƀO׌xk7R3&~U@>WfNKyq$9wf?x\_=N0fp~Jˉp:ߓ$ mIm%vL| ={}kT;sU=3Nc.Q5fu;v1FxUX c@9M%\(=b&rxu N8Ld烀=4 ~=gւCO |Ҋ %݇ sIo; 26梔C?SpfIӨ\8ү[]򌏟#~uc,@ݼ9~]jlQ1-A<gd:Ub+\w;a#V"#sr楢.m+9Zx٣c =y5ϭmd{Њ"vwc翥HѾ ԕz3o,20:{ ReHII<a 1p:rh
:?{O\~d[I8ǿ#+CFѯ5ŒDF'^
9hZ2dϦR-_ßIQ-/v3ʏ{]ؑclbDQŜŉ yLeم*qtF*;{%$VwR[ \} h~ l71O%P(gމHNk</8ڨjEl`!tll7*yx#އ/Y.#/:?y?F6Ɇ{dzn+.t$ۮi
i;	v lq#j$SBVH_9#+_%VJ嚳?^chc*%t >qB~ck#)1sߵ0iF3 Xu$f#ЭmP:ugn;ڤ,!?,B8rEWӬf[{Xx&$!M<[{HtO Z//.mω,#Em tx4']'+KO]_bkZa^y!|DOTC=k#5;8hvVɑeBx=k$mیc##L=(ho1O2Wt_|Ϳo\״Жq  ?Zkl9EeȯKki,⺵R2Ee<Oo|j-fBY?vSn=z}$|lԝ-}ّWWޙ\iVh.#1j^?ZR< h8)$*{-z۵Ռh]hݹeKW)8nK+ c⋉cg_|c|s5 fWɣ*is	̌?ҧ-tBH5v!SVu;0{I?L]ŏjI}?so(k5z`=y^lb{VRӖ1׎3EA WB, U YQ$K=_L+%EWy+/<O7X8}aG8\($&][Qyߍ)[隤c@ab>H+hx`]Ysy2xZoe	#(G9? uqsjd8θ;O~	bbpVokе4ݦLⲼ/	yS5Jܖُ#dPA_k5g6$뉠ڏ7#&9CNfhTӳH ejkGћ|l c,[ tҭG@)+CFusZٶ2xj^wML~_c"ʃa}=v?E'~cٔKFB~beOWLW-gs dP찷 mC8GjLėJm20MprnG[Vr\B#>Sn?Z{Gep7E0?6?ZXFl?up&? G_5Z7 $7_eoeqǥ&]ˏ9fɥw"׫O}QB{h}6H#dV|O^=Z6eNDgkA#f<1V~a^<*rx1?$yM4ͧ&f]j%X
oz](QvP0+4/I>'Z-jfoO>l~!t<UqҴDU{"\
k}KMds[	5;:vR\TX-Dd8=~b#ALcB;Tx銘FF82&=)3}*S8WҐ0Ȧ{3D4L	<v Ҝ0pnR4?tp??IƎA@	]O	<`ǸCTw(3 %+jK!Mc <~SdAƇ#JͻjUB  R O\c M$n}U^ LPNMHT1')z1L{j1gu>)1OZ@.A?(+= 99 Jtc
8sڼG7PMĖ'e׶\\˜%/) ?Ҿy;I݉.'՘r袷c ]E]NY͸eR}=2en=qX~1O<	-\mNsj~+
#NɳjOVPT7jFTjkJ5'wrE*s&" Q%O#kdtE csѾ:ڑG]ǞzߨO{sX558{ۜqyWVeiiTogj8I| 8w#]~ňKxcXE
E`ұ|Ob<5ԃ3*ο[<ҀdpDIIҼ{X֤n$w,϶=|a~ȩ<;#I#5j' Z^2=x|FڌYپmտxϠzl;>U>*N軲5׮Y~AqON.-Ec\@$d]'}2|_iqxR?H9;;sՅ}e:jPc)V>[$d"UenSaSrޕq}1NJY'\|Ft&с t쏓V\ϊAif ?y2{zzT͸'q>~ҚX6R#ֳ͡&Oǌͺ??eΧzn;\RdI]V.,y$SW-מsAWL`,Ezf UE\G NG$b#X#A#Go^oǸǭ4:bպ4R0cI$өfǏzpUmǎqǿ)F
9$W\ƥU
;#=U˶z =Ry}ǁӜuih;'m݃#?zh'?J@oAp[۹F #8׎¯.WwÂqHF
(2q
$D,Csjl;,7:[nCGnQ#mtH7>c9p;|	aixTcӾi9Qwl,>2kXk!,btH4v௸J2ަ΍UG\[>A~ev;?avlmtv8_BGQ]v7*{ $7`G>r`r=)U< A"g8I?0cQ;=wON ?!f19 vǥs>Kي٧&z 	 ^ss?Z4ǖ;pxF_Wg?& VmngtR9D_QTx#VЬvӡ+LuĀInՠL)e05M3RT?Ff ez~ʰo˝Ut
jrO׻6{u%k')1{|v{_Jk'>aww1= zFJ8srKEaSۢ /S[Wu?y|TWv^WSj0I/MR叠{q劲??WUwlG<QyRG>AF[tz8$)ߎ ֦4 0d_ jmM%DˈsЬ9oį^it|E7 HC{
\ 3{~+1Wi^>RP0Yj~]\Xycҳzz_~I%ΥeoX
d4c,c B\|ghryRIB㺷q}yU[|5J\mjxh[? *֊nyd>]<v6`[-n*N95U9Z˦)U}0K!?:lqN摫%0CSǪjyѹ,~\TYڝ_( IF'@b4[Jw#`ci^H<~VM5y㧧zwߑi^;,/7jG;=>4jCjxl8^%&Tdhr0ǽfӖ3};G !]ԜmYW;Mto.?{lП~$zY}EN 'T|0JXZom+g۶7>QdUޱW	gQx7xzVKI:Sz^9УGE⪎#S"0,,c -[%ZIF&;kPe[ÑW?[k^Aj 9L8+hhUu=&6	W;OeQ=<K[O)1}&w"@p#[xI8 [qI]h:ݥ kTQi#Xuʥ	 M /ğCF%^jH5v&!J7~5sq h\q dje\uC}EdK^}	`;JZt2[TsɿT8\jGcxHdwY/c^rN[$8ww/?V/57|&0'(9l;ז~ҷ}Ƨg鶖,Afs!V\+0_X{zhƬG=Zzd|*>xX-a/7|*@p?ZZC#Zxj^<ͻ~,O|eF6wiiemphG*UvKO=NkWbaм+h/ZukG܏S)Sn!|AC5k	jFyXh^wqgw%˭XYx.ʸ&t0185UiZw3ѥMI^6_#飦xN}W0qwR+MX~ի[i`fxV@?"je:s>V-:㰑5~>1MRy ?ۿk~#ą"Wo-ĮZڳk`?mR^ $~_Ooxo~' ]kr1[@Դlv-4k	kQcn#"UO6BGDđFֺcZٝ%Jڍl~0xjj%ś[~.u.,kY&|t{{6[{  fˠGҶXsg!)dïM<Ҿ]ִΟ\ۑҺ+/**.B5*ߝtGd[;{M+ Ϛ?ㅛau}*kc[puo/FfRWLkҞ1tёN1Mއ)ʇ9 l~eI"A~qI@9ړS@A~3QQ0 :@ 4ALc'QH9 [&wXdHy(ă٦m}ci  <z⛷ &)= 8W3SrH*d$ @ 0p c),R$>?5/6ij~<wfeSgeht,O^KI ٟmU!dok$_W?Vk^юkIn\ x v?֞7-&xc#S'$KSJϞdc:ަ߽3J9[^5t0JK3!}c?^ n^kz۽ӄ5= <g-ZBq܀jâ//a߯S^~7{9O<־S^oid o^CZ={RO.5]Ƕ;דxBDkz?i)ᩪtՒ^&$/dN FxQ\m@X2&~E|]i:P<f.znG3c%)?xLG\<q`qBFXbYנ|)m}3v=x,ar-	 xQܟc_YBhC'Ya<eGV#~'56/xgK/x^іV86Jwgi^xI'$/#ӭdQ@7eؕY֗KZZwj=F'y$'$zV5k${snWs:xǯғw3fZeơ'oǲrxF]xWQ8i##AGGƟjNcf }n{!~
^7r&LBVNzTa#]3c\yFC9Ԫ1xN(O *RcPF>?  $d0T'rH$:Ls~\c#sq׊(\|`0898(w`g~)ͷz) c*i3  ԬlqLD~UqRڠ ^_3zɴjgpArLa򓞘/@Q`|\Iǻs.[oe 	Nrp1JsVn=S`;ܨ֡b-c+5XѮmc9n}c~y_h|CUl\Vɫ坤+34Z2]SNpJjoT]n_i*Q/Ν>*ƲB$.WCaUfE[.2=c]<Q$n;gc.VۧҺpV78-`? #NO9'#4n[|Ok(,G{Ւ?kTiNjtd%X_+0Ij\^I篽F[$OaLP$0;<W? +_-#G%pml@ %uMJET-c׎ֹMR[:?ߘlQwǪF}zýi	P[cme_O=vrL+3?Ry&;1ozXw>,jؕEem1-:1BV>[IBy<ߵtwvڤ}tcчc\pϩB!uUz'; μ&i:6MW}U|"yGFuUNSMJ%= |VtxIf>UvڤL=y{zQr><f$bbÓSASF68)vٻ4p5[S-4F&X-{79O%0BGѤ>_.>VfCYCE:Nz_ŋJ~ZOV^[:
;]g^\p8*ao_ҧU߱ɚ[9 }ǹti'5Oxs zUB{5xJrqU&|}qgӎj[`gTק]Da220GV|mFN}EXl`8UimmǾ+uFWh@+N܎1Lf7BF:m<A-pzUi!#pyw03gZ׏|1[o`1{qLZݪpr9U~=EƤslZOQV[nN3{9Zd#ҽ{t=拚*?az}ǣ
68{N8Z|H}qgk/m 0TZʠҋML񮽧5k|l>4w:Ll#W7S9%8k9S-ѬeV?ϥF_Z2 |S]m/ޮ.5KkY9[ga`_$G^CAyۖԬZFH@Ak	a)mbh_㫍fkn~w>U,0Q$r}pnK1bI'SoSnFsjHRѷx/ GƕC鶺4Y tpeЯkUY^"UאEzYZhM]]/>d(`{;d%|?1&џLqx&=i;r7h뎲;#}zBG~4
!{}S!Er76Lg vSasLdQU{1HXB yS5B^ cKaӷZV6awkd;p8 >zn74N<lx,~5E.b(Fݤ7hFOqr3^ yxyo. K ~Q |Wv
/TR]NÚ;VM:!O1^eKq}q<ݹA2 x0r x"#&8Mȏ^̺Uc#ZxAqsbޓCrLfM$A\Z6p+GCic\{$?梟A,膋VRjPj_.>;Fe?5%4c]CPh5\ \tn?|IIYYe(h|2,$z~Ml#5>?5HUٿv^+a]& =/ُloì d_kusFyHCD{~bq_/ZϭhϻLek^&Ӿ]B8uK %c'z4Ҿ5]^}>C+ixC1͌+6uF'0%ѫ^)FEJ=A1~ނ f2ۿƀ_zx y4ҘxP}}E^'`czО
 r֤QS는ՏwmCBV/ɯ4d8^Vo 8*G%#Y^D4zqVV ⿍־+ѷCi2h) ǷWlO|mQb?~zOy&yspOw3ۄ[JQ:gڬ/nj𩗵z(Y"i$D;EEf< rOjۓWџOa Jwi9 \}sԊ!/eu\S۫쿭'_BNy1c<J}}x9{'	չb 9(k }٤#T<EG9=YGB4i+%]4`ci` ozW݀㹧^sZ5~>IavX޷Ohjsb+();$PgLLG}yKI-OpI$;M}>zWGn{*#sھXxy3\X_BgyʰcO`	φ>VOd&ĺ1AÞ	:  }Gk8沶=;;_Z0'$z1V]~w**IPn-וx'QkkF".=O]MBnxO*bzho$fEyſy(:E?u{E=[ֺx<'foz5;#4YdbrYRMIߐE㏭ZU 5ęz-3W#W0;c&,)ݫ
9>c\t;j,D\tR h۴3nsqqHQcץ 'f=s;?4߭(8'FrsQ@=ۧ9SuO@!x⛴FcO嚖!6?z?ƥ+w(-NpqF1۞fL>ls#f<*n'5+)9' V NHr8{j2y)+G=zVna3@\.IQFb*\"pZm6v]A4p;DIGׅ~sc9:Zca<s*PSzpL
sʌq^Q3B }/P|jII0!C" W\Jn^գ|#qz26.v>{5o5>[s`O4Lq~ZM1<s̚\ 'K-֟kbꚌf0Ƈa޾9tNk[t1;YR/} ̒^&
s,D<3k_.VgЋRQH=j4m;wnkK0۟-I?Vovx9g޾/0ͪbۄ4}F[SZ_z  (^ |6riO_>c-qǨbp>Lʡ i&,qFLN Nn4iO2=?Z?7KtPϚΥ(V:;{{Y'^;P?
b/b;b=k<MG[*ZQ8q>xŚVYu ?z
$|\RXizY+enIiK6Nkk{"+/o5[}*1Xp[FP%Ҭ)`7β3,GQ	Nv8ϭQvػ  =֞r\G#TyyVq* qxҴb_ ҧ/ǎel(: _oҽ|eYNBy+icq'WPet$qNpqU)mG
ہlARd';EϷ T#GOHc*3( ?oYڨ-*#&F;dqU"݇~}bkR8\g'揖|>U76A'Dc`=r=ΒqB`9 rs+E89zi9ۂ=ր2
Acu-@뎵!w/M0il'fa[q˽k ;ߧ^=}h̦ |F<hF|SqӨRfC[@<k~@8*~1@廜N)OgJlpc6R2+p֡Nh|ՒIQ<@ӭNm^*3h6Uq.BRi#HL?*yx پ I𵔺P?wn k\Z?0|MrN~kgѥgo,ecBR[<+b:}_2'\<~栘/[ OՑܸ@݇󊁘bzҾ}zIsǷU0쥾*
c, 릻hARO_q~l>n1R,JķFx}(R3p99:C6P;}-w?D0ӰӾ#,wzh3\&6+6}-="yq!+J<k22kwig(f`Mڟi?ʺ]3VYǷGyKᑾ?ʦ6mפpy Zs>PSX܁lm}/ Q =b Hn,m;Raʙ~@[L]W;\BkٖұMFy ($Z-M?x+ƴߏZ$O #MUԣn}e[VOUt:#-zg6E&W#)~S\kH z5M7R@C_.-nԯ-ItW`GLc53I_tU|yHn'Cҡ0^[Z??
ϑ΂)զG1i'p+oeQq
®ꮣ3v|~ J9<]Γw򋥅O9=2Ya4.*'f4Cm#wVnJfM	jWӾ4k@.z~F?W.m9{-bQZyx~> Z]Hm!5	n`Ҿ0xsPط~~)r?º=_O m>I]k曈+uowg ]`ܿV~&ߑ㊨%s7Bf}S @s|eOh *< vu1]VhʮWLqT[j{OE+⇆u\m6qN+Uk;nPc12٘㎽vltK	|֒gO\'ĭvLd@d
s
cG=@ 
x?RK=DxX ^[k^S=$y}\
<A26ٮa[8 ޙwu|UڸڟX?;a$ىv<azuh&2\ӷb`	T
xc5_>MTzhRD=J9$saquF|Z<I<~ ?(OHqu ?x>ck[x4G(T@0 M~=t8T(XDU9,hxuN}G*]2&p.a , *F15&5ď#fbO>,ۀrhloV֧2M4sWkߘKtϕ>6pm3FՊWాswy$R~ :Wׯ*ݡĺ	"Oי|QyWichAfn<"0z
8,l",Xm`aQ` ?s޽XȹV4<$36N	\[7U`.d^0yQ5jqhOspAꪹڼO^%/$Wݓ [v<dg7]9<kqۦN:?UnQfTY {
zb4F"nzAFv`׉5icAP{דUU0zt8jLi	ǥZ=h< ɫDR%j1JlQ񊴪O^E3D{J~j\`n H\F1	y۞=1N=z6͵"6ی(;9sH	G+6T8{
?
dj3xL -<t@ X4SA$ qӚ(䪢)F:𦑁'90T89G ^GOts'8'9\8 dwϧTMzv H)둍s! sRGp8<}<f`'= +2}Gjk3qv@'v*^>\iVfd<c jǹ:קX9m/L19t |ך*ď?z-eA^NhE?3:.>pɣUVM9mnI~7IJWjw|{G {U?''Sm=H|)+zlU 虁VWA9NN^sK%,yLOyp?2J]wJXYMt?ֽL0RPkN4VJri!K.`mq'.?ocZ#FXWXk3~%e%2k#fx_nǘdrzmWnӿ|Y0jX~/CԂ;v@k5n:|}v4s_ sSM] ,ӥ/' SY`􅲉Bi\wRK\eo-܂8Sg8D	rTdZo8%>@}>Rj-pп+ٷ։4̊VX2O00~Oy>՗M e8ǿZ-q#ȹ$ro;-aFBVeR>SH8jŒ<8f
 = T'7?tᴪz_RȌP]9 NpN=,+!v?\dee' {fH1*}w#q"FV)?1PȾ`\1$guL['ww	zqQ'ˏN?* 4iiÓ T@15pv=7chj߼];gǨIKI<T&2Kl
]r{_Rۧ\ cy]{?j{L;7R:/veoO*g<UNY>PqߏJؒ<.v9>\l RN3]OvZ3#;L0249^sПU扎#ڢa~R32×m4 \t<:Ldd~^IW3zVcw˟@0N()A ^qǿ|RG#~(WɜcQ<Y_\kU]/=*'us%acL9 zہ<c[@()3>JQuw*AT~dW+kOزg	?ZDMr3k.6Gm]?#^{3M33K9?|o[᤽Oi{
aFy@?ȤTe:7;8 =eǮkO&, w(*6C/ʫߕI2?,yG(\~")UnPI
] 9} 7dmFn?ҟkuIysŭۺyp{@jguQ|E[iYd?;s֯ͫjzqy;!=رu5)Qd|UjҤܯt[:p)<V0(>S9p#QՈo~Ϟjҝ# j _~MFw`xqVYڡ{%ҋO,W=[O&_'[ME@o"cZVQ-g\rm, [z"	c?#ަۘn+7Jƥx3NAx~rX웻( Ue*GYn6K7̪?|f5{)[:+8X%77/)qǤihiD/ݏw
_;g^̎#_Yj$n~Ev:wYm[?\]F>]ֱf0}3XchWiL)Q^1|~=տӬlI_Һ#K!uՕ!QljXje	#`֦[#*jO"n}LJ 9S Rs7mgZ-Y<"9ѡ$mҰt|Zʗ6 S0qV ?fe, 0'lJAzQe`Feoӣm}yP?uT̂/zTG*_1Gt6${AHtT÷j[yE/#?U)'$ist;$s"Fw6GNOl~K ׬=^.67wp1_T P5@bR2s'Wy	{7)i|HK`WJ%&Zf7<YkW}vS= 7]bKѠ7-]ekV~J[A]__x[N*\<kDii0{;t>O$.z]&{a%,Ga5]Ca\\II>b&v_
2,9e.iR[y/׹_X-+ut`<^1k2O;\Үx2kw2k	YD7U6k|g6Y4'oO)k%6(WGyc5% {?繷"xj/^=*𿆵/k'֣(Oe$̆vPY
2OGcᦃBb@&Ȉx.}p{\޵eJ7gw_i<'k~53n;(p)ve;Szu7iM: Vnns~$mZ>Xc%B.	<S\I<!xSNj d<?NާPk52xM~~VyD)|J$K;1bz{x43ل8ȻjIM2GJ<H6>(]DS#5n4;x'<v93{ZsHNH!OJ>^ sj@3ҠV#SGJ	Ԕ19xxuz4`ץ9>?N W){ury=qJʌ}(>N{f^h.v䑎?9Un>c?MWm)ڤ=O5 ?c,Nq7 sl Jx#xU2%vnJv	`ARb#nwjd 3=@c9=yb3iX`\*NjB0`G9{C"S1ГN+Y<<vUȌ8q #p<kgL)s\A^~`o&kI~Oc J%Fp=;P=?әR7#9=kU
6$IFG!9aw/$;<+ȮGvpqƍ5ʙe;H|& F_<|_ß' ~I:SH\I!_?^$c<t?:'ٝeytJ3U@]m721L=cn NཫWN^/\nL.~ﺞ~8 ?|/zzPϥ5-\։0e{هqSov-J	T%Uo1q6fP*
 ?<Mp~t# sag]G:3SNG 0ΘG<?5x|VV=jm wgy;S$?SLF#r>Pàg vϵ:)<;<8 ת&ѵN+&c5*DBk	NpNsOi188?5 2NA緹pOѥp	$_jO&W]q1
I >12	1   #P*V)9'O~M"r  H R.(<*|G'	#HڠaN:݅R8Ab8l[9"`4'^:韧T%ܡwcdZ]ոh\$v=:zTS*~Xc=GqzʖrΠ8V:WArM-<Tề?Zw6s׭T!#'ҷ̸P	 c@oqsZ2gnO$\IxJxGn+dh}$x8$)#F`0?U݇G ^hvۍç 8MمNBscFJ3Oo ClIۏN64✌)Ph\3Ҙ#
wgv*ݳКcFc41 4A}+A7 6rxJG.9 ?Zeʙ^3ӿ@P#Zo
1\t廚8mTq3,q 'c^.Ǽ&
/5)H[j;TvivVvoŖpx}Y2zp ̬=̬;kRjBHH#k᱕lDoIPclERu'fՖ`M=|6Ġ3ɌOXTyY;KDa,cmIX+E=ĝ@HzR|n%8ǰ@CQJ!sx?7/S'/ܩPy[|2?UeQ?/|V1XWqnCşd O!ݩɋJ_֧r.M fE>2O	SsS$DDgjeb(UCT[4m¯9r(#k| Zmֵ_?Zo8zh1M۳k:cFae늉3[F,1)֫WqQ3ӧ;4bqkШYdQ"\j6^Zwe<Kd\ڕYTEx0jۑ֡m5Si=+"ӕWLM$L;F&?l^
t+4Ln9n#:j}o'ʺ!$r`KiZUlz!Z=cR>j`ڌw?c,59lt3&o
Cѵh[wN-ҝ>,[rPWWhI#HI5ęu\'c%>O>m&]SP s܃*_56./"E!^ȋ*?3氧a<(qUK2Ϝ *ޕQs
b>=b兼גok\L#Yܞ 	~G-Xׯ 7DKBw=W1K?#v0XOHskGi#׵ʟw8_;c}Pۯ~KJ\POzx9vi=Ocƾ'W͍Hח[K>*ojrFNkGm7)<'>®x_Nij/AXmZnxWWisKd|q,>H?}D%4^YcȔ'zR!rJɩ7?+_LD~WV='`@/Son|9T	+,%T,BcvvѬV"cFumVNR
+#Tuevcx_]ɭPk..ZY9cZZΩ6u$mĞ{Z7.,@};7pJ_u_ʄp9αwMZRX<(=Yf]SB=OoA\kfQwurxU"ݎ86nİj~r4	Q&Hq8VG|֙׎"u31 #'+~4iGæ *	휞AI;feCŉMY8K)'s@ sz`!8>ZQ+3h^z08=i:xm/E3÷shzcRp3}J7mxZ):pp2hᕉ
~b1S:g$9lԃp1G()nׯLAۑrqRpAvF␐pzdxcsbn@|gKmcqԅqq1MRqϧJ\o=I}('jRX6а?1 > &s@RI  čM['lQsĀ`vK]F}<{KʕlaOfZRzV&8<u*/_M 1rG4PUO[mo;yR6ǯjBT%f~J<DJR}PTܜqQI&9nH?v F}p: E'=srgQTdﴁ># 
pY~l8ֿNjdݹd>&',=Q]y_iC.ؚ.;g#=*Vpj6b`}cQe_=Uӷz
,Ygqֲ4R)e,oOXL>@v^>f;Fcy ׅGfڎ9i؁f[pI?<:Wg#٣ (I)ڿ7U`2Kik
Ikryc|3gGJϱfƬ10'ǂ{WP֜_<&KrF}	e 7<{)L'L6q5.oIv VdsOJLi#.7 #LY+ӯJ<]].AdZ;WE7QX5~O6X0$08Vy-rpqx\G2<y.9e8$p{v>9PA03SZ(c2?*?x~=89з+FT qH;fO^<R3~ ?O##Jcz~9cEJy~٦*ϵo֜
6rF~nIW,v8TVfx<zwךVcݽ1=jB}$6֯q* '-1 (I˂gv/U4Q`{g?4$)aI#>dw`(x7ǝt2Dtr$!G*I /z< >2HS~H%#n38^:Ӛ,/\zV"1@2{g17)moA	>zV^'Dxp 8NIۀsQ"r#Yg;/Ic99/^LxW*03ZtjH%eUT9=qG3/[Hcv9JGlqCǏ@bF}:^FPi_&?ɬ\Gd(?
0W9:W|9Ǉ>jz牵|G\ƌw(|DbZ.Pk4n#q''YNҮe# 8Xk&	(hc9n9ʱ9_.mWL`Rap[FZmtfoC`QLY2ADZq[B)|-G7G٪WےƓCi?jul4Pdhqg_*<7[2У)\.3`FN*%kӽѣ{ CӉukmon 9h'{4}$-,!{f#gCORb8:}[n.Q@v	r<$[J@`L2U  j+?bRm=Vd+g,5z?|J-u@0Gi?};o\(UҬ./#_h1 }jt^F%Ig(yʧm> CjZ}Ic^MAXD'})-LOE#,1aG_qW.C" gCSj8 SCǥmUr>拌9x|ӄX1M2nUFIH[bF_ƐY㏯ZC	;j̎8#L;H;qOXr:~X="7g#hʦ!	?Ga~x!ӯ+8 cޚm9Wgpqs0 Jֵ$	 ( L8j֩LH93ZNZ)tLxy>Qq}1gh*3ǻk{L8s4N^#R_C(:m&pְ=c%_ 	ךlĝD*};8kȭR8b0Ux7:4~9/k^Wwӷq2Kƹ_tv7.1+g5O.jU6Fc]VI%b9'9>zdʊwwd-+u8WAh31V6]ܚkͫܛkVͤg?zzxl;;-7f0s컔bMv dDާHG\XW	h؜$⾦PvG上QԨٹ;%|f99?S_>/XgÃv|X|3+{j	|&GߢMj~ΝEE#ݽJ,zpkx?]vr{Jx{ّn(A[8Z\?t3dam+.	d8sDԯ,xŏn<9%S ['ӕ-XZn:kGo|ݎf9$JCN8Z@;֦w<#s^<c=R1%GCg#Bwl#{;?3W!)徸ӈʢg=ۓX.pzgjabgg嘷8z}(cʻU*3we;u> /gߠwSTrz  Vlw`yMZҳKd|´*T ӊ	[ZGmvs'5\]}RF?w3OHy	1lybI>grXII5gkҚk{ڒJ~d#;R09ފo3N8no_M\!Jv{ztzvm~=9~l:< ?+gc勦~P8b_,\nMB>nsSg9\D6G= jB+ɒ09Ps=p: tlcnGҗi`p;{8ҴI遏HhKIr#Vv ~: GoI t xq늯t7H=	+V;
)׍g.t=:zKFF]rvq^SkEl|1WiZ݆hiǛFpkqJ}^]ahK;m?)=sZ%S+=s# @*s#VtVִ/y h2 3Y-h?4k~L38>T0֬TqH?oRm10X8ǽLT]
w*qc(񮬣?T,q^k	m [b+6WT%˘LdmNpy ~~1  8ݲhg V תHU8/oOWfm Z͎U, 6jI$诧kF'Sc_i>GGa$jk0%O_/m?}koʻҕWh*ƒ_43$چHQ,˽yvYYس;RjGJ+ڧJ4֭*coN1c28Oֲ=1޼*xKknz}?m1p3ҼSm5j*@ZMXcc\R;cӚ?;eqz?L}kn+s-OzKΠpON}8ӽ"MĐd*cBCt<c 1]C( rcwEP =@Wr͟>4] 2A6W=c֡,-̈q<l6%2ؑsQI૖c-?H/lpONS2Ϳ.lbd`>O%G#' 0?\L;Cp;s߿o ?:Grr gSAj7}}# BKImQ<	Fq}#:'?1O V{u_/pj=R0{>=C640~"MvGzU+r#9@ r:K .xcOVv,{'-õp[8nxeL`dJMS* FA#*"N<h噕-ԼBDxUS_Dx$ O!q#W~#i{95{+P~Wjw7hFēsZFQѲW芏T=WbTr6M; "Q<s^'d&HXĹjjwSu$3o% #㌌/+!܌@F)4bF#`|巪l",.,#̱Yk)bNFIgYnp1$e8(h[Ms%glΡ h_8F3ֽצ, =X#U#m?gOXLƧ=vhOW:6ŽA8k<i;A>z8z8T/$3=*{<:
??=1M^i
2Nx4*מjs$)D1xϭ+*FnVYH;1Nyڀ+47)LT^IR*nz+lb=GJE*40siʆ<3O,tkx^X$篱M1?5?@ g?2( GSS(9Gz\8 *㯽GbǯsJ˵ooZc)8t4͟Vo9hF8([r>yιi-Kqy Џ@=}U4ֺ^	ᶨwf rM}u/Xx/BLӱ,q1i۰yxgաgL:?̿6>ҭt"m$HIIMkVF{4v-:uWO'doW;c\Ӳ$QJ*lgڍ\IHW/_lF חA|'ּ%΋ʷ9a??һRudyL0R3;f ;q#ָ1'5j4}]QXVu'Ed}+oُ|ALm"n'ޕXjDxWI}>7Q >N:jB%eª".  tu>s_D#˖f<9g$]*[6.e9¾SZ֫`>`׉꺌rM;,Iɫl)K1cCk*Qb
#vAv#GGT;=VAdm[t?{GAJγwMZ}KQ}(`*]׊ R=]r;b9GMj@Pe9D*0!"\c=Ϡ}@RYEQԚ&\(*g> /rK0yp=O5QdڜOiX:8>"dRiqҝ hhG5)%y`98xzzօ-Bcz3sA,e8[<۳՟H?>U-cA1GSmH9)ۻ2{Zn#qTW*>ǭ0+C^(ZhB7c!\f0>yT-Wg 
ݸvvErlNAQOU G8jö~Ld}X(}rNhRr>Ē\ұ/c؟3O99 TXw$zP	7<?81n9N+=<~H? ^A+ʎx8B!*x$:恈2=Jz4~K8]NH3CB͐rE1$%)waHw8`9 TGsqqwz*cq w0oQ8 cZ1 QNvp}Rv1.`yY-&[I$;dCe<*$\3<ѫ3XT]:^JZ^|M 7CҺsĐ\۶H:א
ȡYA3wZP쥳xQA[ pWWiq?iFT1S\ޫK܏*iU7?>}FGOƳ{O4RjA$8xQ ]-k}ٓUysןJ>PK NSמx|ivEQ]@xu5|@.J~i"p̈́erQF(ւ08*0I"ܚIަ ~PrsJd6崫є ZCI"Ec/#*R}+^m{.aX p q^eVnOVCX yOA_YPZQ uJOb |u&e$u/~Ǆ8	8#MZ'ԩ*`pҨZQFCӏƭ߯MzUK/!^Jӂ=1Xђ8#9=r	1:mOp#8uee9z*!oVݭ:qCF-ޫ'VO*he'Zp^ۖC;ztĝBι>aH8Z.X
Fs Zmvg, < :e,.=4u|37ˌU>k9|v$zS")UA=x-aׯ=j}IQ3p3V<1eNy~ϥVCwSPfweY3 0(#TT ZH*nS3#Uw(%T}JE8n#sU]w)?0{*~NOiS.+chϿj m-cVxB
 #'ZEFvv =WpB8?6}1Ng4e>`_9ゾ,m)<x?ZDpr=8*DN3O֥	ң#qL+;c?JHݷz$EYԖ=hV=oCW<_G[LC!Q U 9FU/a,Ӣ\y=w6?
/|EѴhC8,F%ga#6)u: q޳uM-K"Q J-kxΚ5	[죁渋]~Q&q=ӓ$+WVZ~'=lʔ>OtfZcI\%Ou893Xŵ8o_9]<	qfLQ`͵.#u<ؑ^eumm}5ޛv	HѺ їW63,4,b቏%w=Mjiv?֖2gO嚤bh{;k{IR ys?'='&*m>e<Wk(s?{Ȩ-ª+3z{*Q[#T擓Ogk<zVݼ;SJm+z+ϧZ-ܮHz
ajO,lbR$WpN*B1ӓBo4F9}GN
cx}'ހd,N>^ޙnOlq ֫*p8{ѷuNhݭ۟Oozw?1Ӏ1Her>vߧCg9PE+6>k|Dq׭JW%@tӱDXc;O	>=}&* BJrJ{J
7w@nJ0lM!܁W-*7S#= <|RlG:2ьqy԰[q4vмI"q; |7J#_KhI÷R?_`vf=\N"8jnOz^[W3*P۫쿭?#݈oTO{##ER,0OWxĬdͰrx~SOV~QPT+E7Kk7?fb0Ƹ[Þ*kkֵ i$h2ƺ!2U$z#;ľ Kyci]+'K4${jUl"OX\2gU~M{Wmޥ#6߽+F@w$
]*Zm4&Kɖx#i ۭ~|.eg«BsmV^&8O8ܕϔWM΋@дv .ǲ%!'cݘkBXl丹a1z|I9ּ>'dohxֵf5u{\ܸV Mx^w+ƃzgf5R ^=氋QG#_Ʀfir|nOS <ylqMEgb'$z^Tʇ*ziHW ݉-ʏ󚾈3ҙxNF3WY` 
'4s;H@onTO#XI#dr{ap;5Ǿ49'͞ǵzƁ/~hXV%gvs[~"q$K0#`?(VӴ7llrgs?S[/-QUPd*LzOΧrO+m1r3>,ͻwfg?#99_z$d}~iQhٰxWҀ7s8kQI乔fE%#qҭI,j$#rz{+<En_+gVǿڜ9ߑfw%Kzֶ ctV1;PӇv1׏ʥ^LSU;ߗJg(0 :*e8?ڊ@vf12OҜv.@q T^7#jxvnG#~];TynG=9MY/>Q{ SF<.@=9|s# 99Ix' HBcd7S70 95#>i9qߎ(zTgnXv>~y֣:%9l>SJ*X/',T(  	<g4T@ pN*=8ⵊr}1zU7Kp;(Zѯ8rhL}Üqjp2HL~%*`]O=^ V>!d=7X9U"?͏CКr򣜓J%r*'։Ui}8G9N~yEZrqO   SҽO-[84˄DrGpzUΝ7m9=+9oBxvH}	-f } V8Qk&"5O9iް>Jam1ZGoAYw_}vECj<ao輵R=+ZKBS4ߠsi?qxCݎun_EH%?$k?UUxmf8Y#'99I݁()` S񁊣;JpZp\RgLB㊕N?`v%g3SUOLC%Wz	 R_ ]NiL͛lps+7j)B9ʃ;|{r~Nќ1ҹkkИfm޿(J3 ^Il \YI,Q:g# ?sp^u\ ֭/67E^zKB6Q
.Ҭ*r:ӯ0s#j,:dY:r9Nx7i}
	TpI ӣ$nĬeWrqAafT9Ƿl
9wk'ϯHF)*1qOMѦ!Tgh===j\w s4݅dN{c}0+J0öz߯\BC619x?ښQhŰ?Jb) [`Oxf<1oaW%?0~essIl1N=Jb+weicߎ@c˫]1; JU69b;u&R0!ؤ$V\uCpO]9c+m୯O?1R/'ߥUHx瞸tKRqf9}:B+aT/\by*.2TJ~{iW{'%&K_٣mg<,ɜ2I"g'H 犋8{N	+kJN[or7qUڳǜu<2T`;٪;v:Z6Z;mo t\u.3{\KMJh${2{xė>?|}d-)-#
9|bzk>y:WWK28u!6Oj~lV>VSeE$>,Kr$=ZHB~FA0]Cg*DT@Ɛ(aޟٔہ?!
㎕c`20@ՍUqjC@ʫ;tQHGLv ^ !q9"*A'ۏS #*?
V0ݸҀG9OJLGB3S Mӟ@q9H' )Ң0 6vJ
q?t<p}we9a@+lcOjj7tn7'hcg\J[ ڑD$dt5
S]uCӠ79T(9莬>*iSWl6]%6E.]J\wE?WO!r]N1AivvpPD* }~Oi7rGk8b9?S0wzݘ1dzDgh$93yI_Iu4ŝ$=}sOnjaI+jWCU^;]u*?d_µiCpY6G`k 1_G/i=ٯoI1V څL#i4-GUnzdH8^O]j|-J
٫!*Ė5J,iJ5\Ȧy<4u//O+ĺh6Lˆ`| hDx+^&]2I"Ҽ{q;j^,Y.(GpGR}6OKPlG
+>jյ{ۮ'9([>;cxXͻ2[T<nS
cLH(qݒ"q.zwx5jnyla .;sX:Uq1}*?U?W&+}.s䌀Aa@*cB;
9_8^t$|=8[Kw7$G5=+
3%@Crs}O\Aie~y F3^xoeQE'reqMI٘a;̭#+{ޜͅcc8so`u9YBz4/\2Yi2$hAq#VW}>w1(sִ yIB6EHtVaϵQj֎<&Gh9g+`~b dҔ]ß\ҕc{z*)UE v{0+kmv3Gv7JF 
WOCǭ$Sj$zmQxv' Pw38sӭ=v䍫R$~L;=?r8;H$|)\s*-ņ	
 QRn|b¶'m(y֥nEm'Ü0%wJ*UlrM'ʧqc֑a4.[cqw&Xjl#$ wp$[92	Ao9T;0sqVnl.	)G{9W)qA䞿UY>QO9//.pF??QPMRͼS2fS._޴4=sPf0Úi'E"e.[ L~uI'^񭇊sMEW/g#|ӑQԑOhѲ9݈#WM{ږkxJ7:	Ɦ+%(SL^kgwG{t'dlO61;r;}zW;G־?qy٤$$<K4>6zSs_L>yXUb8r8Kߥ2[S?֍wU.2xL{}iʜ1djzgz+qN8'<cS!1ަgF2x4:S&^ ԋ!j?($nÌ'=9ҧF+K7c3ִ̤W"62qzduʲs -b[r㎾b`l|ϿҴd( C@oN{1)L>ö=?u
1r>VS?7$?JkA/*A ڧn B2۲N3Հ$wqn'N; Z_1J6
w{}jh	֧dqyjH٠Wui?w<vR"ic?0g$#14*nMg;t?5_ 0du,$N
S#h
$#?Z68?ֵVh@$`#`S
:ZXªyۻ9=x;קMx r.1iluS?F-&=:To;$ژQz7bzc,wFcSJ2}zS|wy=M1`oKRsrķzx@6$#1L665 ?_H,;qJmq#T96*@~سˎ=9	qc뚍.Ҟ_-$`p=*#0x@dN=zP'iǒA4wJ_1gEbI*1ӏ E=iH;K>RI'B J B~SۜsQ`sxt\~4qS~Lq@;0mx$i\d9Q@ǀ:gޛہg?{>oC҂w}1^^h&cI$(a`d{ 
l6f_93Kʼ}7:iTjgV~*_3aq~#nKn/X)P8ozc==nM勠RR?~5Z`Q,v~11[>n i]KiXsiYd[为bHrIS[#;>ƛHzV^8cnoڇuCkFQ'=?7:τo~(x-6ejW Szۢǵ~XX:u%e
mT'$9 ?[xJ+4``:(u h!ִJUuejVU7m

 M3A}+g>$:!lA,s[V!5l'g&T=U|I֙{'ڷ`s=+9,ĳ1'Ov y/U"SF3Rw2Ey#hwm3ҙ:C / *qƛ <1m&ۏdF8O®KHS# _m	iCkWN\HY>cb=+v=nHE$K5]c\*^F;攨t\rDHUBszDyM' u /ο.U'6hq;|FpZPA58N~rZ a3:ooYV힋jQogn{EgA\POc"b0(:+!IAۚeLsZx` vN9QF֧8}9Q7qޜj֚n4 ~lzQA}hgrAVO1۞)~)Tnm9NN?!R!<?ƂiF:qHN[%è=)yx<P˳h#o ӭ1w c8s :Df,_{BJR9w)tbKwʙ`W1"08OQ0qM ;Zv	\zlv˞9P$zcSҢVی֥;W8* 6zuǥH991?$T{`tPrB3$^6;II{&c'LT.>eP}\5l2wHc	g+94bD	$IjN玣5`YBfK9}rwҰ4w«'8T`pxΈM^Щ<T^Qɮdk,7 ұЪ;f ȫFޝ?/"Wg8/VIᗽ1\c?M1W^qpCȩ}q@5Wyum5Ի}=:Z{@NHTIZ@:{=i?HG^˸qTqʐ)G==1H2Nr:P"U,SO~czpqO9h6I=*5ep3Ҭ#~oҁX݊傓 Z݀=W5``{{W9a6%
&i?7ڊF@+:Ոo9ܟsSaЕN7n#'-
~8n)haǿJVĎX$dC/Оe<u;q:х}360HW#`wz::lm$qe0,6[= =*[Z l.
g_;zW#QOLE ɵ9&ST 3lg'5m9'56;OP6ēkAbqa~g'#~i3dx<<5bu
qڡ!LV bp*6s`{jY3c۩*ǓԌsPXe'1\GԬs߻Q\#m8Q`PZ*y Nj\*x?2,펝a@;f[;w݌tǡr3Rų=Hsosڐ2MdP )ҘW 5+pFc1i21i%s21O0N?:$Y(O=z)e8fqYqrGQ)' !oRj&B ~&Ӆ篯 Fq'jn==>:c9),qP9Uw^&hOZw9`j']4:=#A	cf¹}]d6'TkTB˘39
&,rHX:ovCy{eXfiWZ֤#539׸ROsҢTKTb6q]I-ZZnrwlb"1FI>z/{%: }MDxzȞm|Qpw*OfWc1$bKORO\ֱG8}T:?p~:,q}gH#ߥymq6w`eǕ)ޣ>k]QCpҩy3(\6[\|EM[q}摻(5^I,ƀǞ0jJĭ{?ʳ:S15V_'/t6p:"Q*(D⬪Wm^jYlT0O5xtɡgĊ_񨢏Ӧx5eW6:JU=dF(UxN*Ks<Ew7ʿ̟K(]%uw"(8Ţ?5PxG}鶚|:Eycf
.fJ;cp@\cT04ܷW^@ce i.[P _y(C(v҃uB]t=JS.Pn?a_xK^ _לVv$ryĜd4F57}^,;77kO2i&sؚǵ]Jf(-JQ]ؑk]4$*6hUncbZm#ZQ ztV\V,W>ޕW  Jqx {{;r	iqMug''4/AJ9BҀXrG8Aap9 vҊAs$XM?=AQWE{~t_> @?U`g:{|,PŁP$݃$(9 vmTPg@Ǹ,'R9#MI;*U8<dڀ$$sSTv6T^@ǠnhN`
ǎp9}iw$ztipHI8bc#R)ǂ;(b	7qޣc''&qA9 _4l|݌й$zb- 9sA,P*M(prN@ PzځpNFO 
Ϟ:7L](9L@*p1T "BNin7<yV#SޜSGsA{ԣOƉw9@њȅy$cϒV@VY- u<tQzeOώ&KNs)>5zHIޣhrG()2\ʕS5mb8:(#X.sKՅE/69@\cO	@jR8ҜU@@0 ={P;cJU\`W ԋ@g֟@=2f>AYr \րFӗSZ@^hWa֦<zQ:Pv0*h*yOW_ր5cwg<ը
x_°жjrsgw!6{U❳r	v iX.mF' S3cmrY
l~5(dS`4!*32>#sz
*W$zBs9$^FPN	?7 SOdhmJ$P Qq*r=:gڞl`Pr@F1]wrWx'NB {Pw"|3 W19$~{ *HO$
B [8L{UN=Q!rqgj1;N}*r㧮*Ǹn#*<U&S{SYцC<R{[$vzZ`.Ur;z#> />ӻ 3icc?ɥ$/6\׭8Wh 4K#RwH_Ԋ841g8=\nⓂq:RHB4O}b#߁ڌt$c'94nPN'gho$Ll 
 ׸>P0~㿭k+>H?(ӵ?NA4}"`O\#QNzr&8
}y恙W6k.ryYsi}c<JsOZUp[sgx]SOpGC^uu=%V2uo_\ۇˌVe:GqWcOگ4{+[w5iv`0+.c^yx5U"k9:yFT~0[hE^Ρp@B;>6jc;⿎iRdxb?{E68˹f%I<դLYEG 4KM:UxI=14l^֯MڮE5F= !StjFU^p3H͐lPy[7g$L ;`s։AjTP	㞜Sv5+7;<0F2=o49h7~cڥ]1$mǭ/^G4ѯjiʼmkp'@,sqRރWyPoN@&̨8B8Ay"h ScAZ@n@\0 O=ϵJ ?sa
}8ߊ*aOّМjj^	z+;4=穤nP}~R-{QH8*sǥI[*'G2OdS=1Hlh3zfOqh<лxcCe@;҂((->iŏAZ)3T^s!*%zR IQSwaN:jc8
C Gn􀗆<`={8i
zcON@ '{Rwczv̌2O8I9b[h~~h2=%A8o>nv~Lҗn|yU9	B{X>~`6> o M޽pWڐ`p  JctinF14	_s酈OSk`GLc e4XYҡ(Ř.9'[ٻ {w Ix n$8*)*-`CӵGU\2zzRI' qh@[c
aV^H`{Tl#*(XG weެc7ӯC@\88OJ9Mх7W}sTt# d+zu^~U?Yd$`sMeg9^Zp<F4'0}8xp239=Bϵ
8 CPPA' v/YGqhI^	T!
sڜ#Z 9$)Zn B;U}j)YL׏LV][BMW#*l#R9f,GOCS,=pp޲ѻ(x>"3w?ҋx9^zrR6'*O^Tb~d>6iJܲ,F[ (.Tڣ;W\a0M9x
KJ
F#֗n8N,jxZ "p׀*RvXاvǮ*7,By@nSpw@O=Fȥ\c#
_
ݽs%@[UeLt0*0[<csDp+J,;GN9" *Fw\ׯ1,{ԃ} %8]xȠ8QHZVw>Ul}wP2Fx6㯵)xLp^qIpyiN@m =5=FOl;0E59ccXnOO$SLlAa#q4>⻺t4OҁcbSc?OΚN8Z/?(&#sZEQ_ӽ<L'xq ==:N(+'vB@4֏އj29p;#:KpIRҰw2CWUjo3E1ŋBpE8By[Uk=}4py"ǵZ[@=S:hBsP[<Dh S(2nH J H zSp#JD	P97zy 29
 6uo52AӉ$R`9@d_JR8iph$wҁ]9⛷*KdHOj BxtG~'6F4ރc8':`sAL3 )
tHpN;Ӝv 08$SX#z=s@ojb#h# ?:Bsn1p?
cL:Q=;1A8=h`9E;6FKnm۞W  u¶8}O˃==J<1JӦzfܞ;{ s*>\TlEFg<=N߅4Ig@9}h`
NTHT $q94F\/8Ǟjԝ3@	99O  N|
n`[C`nn3Lx*i7nWiק@s$$qB:qڌ~]!HLS$Qq(6|җ%N3֏К @3q֣;s}ju8M<c8 ۚ,!})6\}i= #e98\Ң9qoJchhӥGLޘ*D'l/p1P~SP"{fK4CRlҟj9z
):rmM0Y7U9nޣ1/lP2G>.ۯҭmsqϽB܎}?:WLyMxqR`)v=;Mz(`9q184*QRԡ7u@	ܓҚC`ObI@ؑߥ4G8X)
cրp$h^0(aӚv0ϹBϗ# <`9=}*>OH>qzԅ[csJTbGJx'i>HGJhcsIjXcu ZEyǭgE=_qҀ5ᗍO#ڬ$䏽g>Օ?jeR$[7 p }n(?1OX0TCzەxGi5ă#'eFx۟JǊN?ƭ3:xHBϯ呃lSǧ֝$~^ԀP1$lIN:n9
8m(#ր"=v tS.v<G[1zS&U_Oj "q}M7gɞ2:ՃB9'@gp q)ЧvWJrcE2O8}*˕2zPvzփǺ!jP|{S)g8^ǒ;0|}*?/h_Z`B iiOBHHl/zJ ]1R;Һ ZA `z6|?zێ4m$ 
)ݎW'jpl4s @F~Py\OzK}֤Q2'4VFާҡ*g;:/͌簨% 2ʁ,*GoZ_AK&k Ý893Zfޜ; /J(\x&ph9o_j ,vӜeGstr)Y{t4G3yRn{zTYR)<@,zT-N#LO14¿6?*"##;\*:SϡIjuOZ45&J?#eG'' xU%N c0# SHz|9{P
Μ>5}9=  r}iz@1ڐON)H#;7u#"9?Ҙ۸xӏ5Q
)̌FNI=}~KM*B(	jP1F;iず`IHB㧧lSC(Rï~q~~dmN&IclGԜAך)WLx\100644 account-active.png ګ;dZ9%Öye100644 account.png Ѥ#Q+b"!100644 courses-active.png JLơ^|bj[,100644 courses.png $,qqpic100644 home-active.png IJO(}Ev Ua100644 home.png \ʭxD'93Mw100644 points-active.png ŜTJ:?Uh100644 points.png .KFBf3H[q100644 referral-active.png lg*7/câ_100644 referral.png 0_
%6G<\N*xiPNG

   IHDR   `   `   w8  0IDATx1@DQreOlUDnwr         ¹=O-\XNF`$,.I(I @$OIW~'" $! ;  @ _[k% HP	P>	叒Xo- ϔ8?qvTZB`-TNEY_S) ky   NB>動m޳u 2KVB@5gPR[]X\       qj/E"s j    IENDB`뉽xgPNG

   IHDR   `   `   w8  .IDATxA@DQnօw׽Lu	+E&n        @8ւ,T>	P>	(I8)#dQD=>`  k{ @ 	7'!Q#B%~U絓G;NBZQRÎ
[KjI H kk7C`3w=[=@ Iȧ|M{N PF@u	{J&l*SjK         8?ZE"HDbm?_u?    IENDB`xPNG

   IHDR   `   `   w8   IDATx0DQ+WM oX      o˦ak#  'n~U@  nR?)  d0^^aPP   89L+ +ϛ> +B  p
2`vC@ @,@ b      fS+>    IENDB`XBxPNG

   IHDR   `   `   w8   IDATxA0A+@Jq'$      w=b07_A @ (`'y|Okk   8&aW E11 &   NANA 
 
"f9 % 37@ ݐqCF @ #+mm:      }?i    IENDB`"/[.x8PNG

   IHDR   `   `   w8   IDATx1PE߻/[mm(Hy;%I$I$I$I:u?V(c!!`|ÃBƇ0>!!`|7^(_iCA(B00hÑ7Wo:B&0iH  L  wWO4|Z?s\      @Vu               x/_       ^   ƒ$I$I$IR8+    IENDB`nx7PNG

   IHDR   `   `   w8   IDATx۱PDǶ6$=750$I$I$I$Ig
}S8>!xxACBƇ0>!k|;q(CB ry82jM[A<~<c&  \C0>   }5ꉦOr      >ъ                        ]1   wX$I$I$It&    IENDB`
rվxAPNG

   IHDR   `   `   w8  IDATxKn0Qs/os$G0l+;[ $X~        'H t2'"$x"B/")Q#J8)vN KO7J]K	!uO]?'~ZۄJ@	;:P}Xqv5<qjtJ8	wVlk+ Rt;M<Yo]H0&I#$x $,3) iW$#6ӒsiMʧ^R=0ʸֆAVGwlSG,G]`|gEIpZ0	W7OE\>0!"-!ł        r~    IENDB`WxCPNG

   IHDR   `   `   w8  IDATxKn0Q8[/r-Ӝyxgdu       w8)NFXDODHE$?JBbcD~[	'N᷑Px	;:FISk)aw;$'s\$|ѡOksW	h#aG+`" N®'\?NSN'nӊmmVJSpuv'kV ^?Wxi〱F]7IpO pe#$43$`D9fZrbN8MIKS#fYwpu;ϳRmJHK2@}!ଈ 	NK&Ipx_k'U"]$B2X        >    IENDB`wx1PNG

   IHDR   `   `   w8   IDATx@Aɋ+G nD ~       |[!y_+/DEh0_f>9 y+D_l#D^lǍGx&hoMCsy6?XUDAC̪A";%"4`)DADA^ #XVZr  p^+
pnFZQ+%      V    IENDB`p)x0PNG

   IHDR   `   `   w8   IDATx@Ag̕C!y[%_       x=VHJEh|/ Aw"@E= 99q(}l.	`|h0|Sjr<x\^͏ VA DAjÎ.k "XJDA D)90}" AGhqk:  zVJA       ,fw    IENDB`xYpx& 100644 theme.scss &3|$aAP;8'oֱx	.lf-page {
  min-height: 100vh;
  padding: calc(28rpx + var(--status-bar-height)) 28rpx 56rpx;
  background:
    radial-gradient(circle at 8% 4%, rgba(23, 190, 187, 0.2), transparent 34%),
    linear-gradient(180deg, #0b2f2a 0%, #0f3d35 260rpx, #f3f6ed 261rpx);
}

.lf-page--light {
  min-height: 100vh;
  padding-top: calc(28rpx + var(--status-bar-height));
  background:
    linear-gradient(135deg, rgba(255, 122, 89, 0.14), transparent 32%),
    linear-gradient(180deg, #f3f6ed 0%, #ffffff 100%);
}

.lf-nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  min-height: 92rpx;
  padding-top: 24rpx;
  color: #ffffff;
}

.lf-nav__title {
  font-size: 38rpx;
  font-weight: 800;
  letter-spacing: 0;
}

.lf-nav__sub {
  margin-top: 6rpx;
  color: rgba(255, 255, 255, 0.7);
  font-size: 22rpx;
}

.lf-card {
  border: 1rpx solid rgba(11, 79, 70, 0.1);
  border-radius: 16rpx;
  background: rgba(255, 255, 255, 0.94);
  box-shadow: 0 18rpx 48rpx rgba(11, 47, 42, 0.08);
}

.lf-card--ink {
  color: #ffffff;
  border-color: rgba(255, 255, 255, 0.14);
  background:
    linear-gradient(145deg, rgba(23, 190, 187, 0.25), transparent 38%),
    #0b2f2a;
  box-shadow: 0 24rpx 58rpx rgba(11, 47, 42, 0.22);
}

.lf-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.lf-section-title {
  color: #17312d;
  font-size: 34rpx;
  font-weight: 800;
}

.lf-caption {
  color: #6f7c78;
  font-size: 24rpx;
  line-height: 1.6;
}

.lf-pill {
  display: inline-flex;
  align-items: center;
  min-height: 44rpx;
  padding: 0 18rpx;
  border-radius: 999rpx;
  color: #0b4f46;
  background: #dff8ee;
  font-size: 22rpx;
  font-weight: 700;
}

.lf-ticket {
  position: relative;
  overflow: hidden;
}

.lf-ticket::before,
.lf-ticket::after {
  position: absolute;
  top: 50%;
  width: 28rpx;
  height: 28rpx;
  content: "";
  border-radius: 50%;
  background: #f3f6ed;
  transform: translateY(-50%);
}

.lf-ticket::before {
  left: -14rpx;
}

.lf-ticket::after {
  right: -14rpx;
}

.lf-cube-mark {
  display: grid;
  place-items: center;
  width: 76rpx;
  height: 76rpx;
  border-radius: 18rpx;
  color: #0b2f2a;
  background:
    linear-gradient(135deg, #17bebb 0 50%, #f5c451 50% 100%);
  font-size: 26rpx;
  font-weight: 900;
  box-shadow: 8rpx 8rpx 0 #ff7a59;
}
ǲx|	@import "uview-plus/theme.scss";

$u-primary: #0b4f46;
$u-success: #17bebb;
$u-warning: #f5c451;
$u-error: #ff7a59;

/**
 * 这里是uni-app内置的常用样式变量
 *
 * uni-app 官方扩展插件及插件市场（https://ext.dcloud.net.cn）上很多三方插件均使用了这些样式变量
 * 如果你是插件开发者，建议你使用scss预处理，并在插件代码中直接使用这些变量（无需 import 这个文件），方便用户通过搭积木的方式开发整体风格一致的App
 *
 */

/**
 * 如果你是App开发者（插件使用者），你可以通过修改这些变量来定制自己的插件主题，实现自定义主题功能
 *
 * 如果你的项目同样使用了scss预处理，你也可以直接在你的 scss 代码中使用如下变量，同时无需 import 这个文件
 */

/* 颜色变量 */

/* 行为相关颜色 */
$uni-color-primary: #007aff;
$uni-color-success: #4cd964;
$uni-color-warning: #f0ad4e;
$uni-color-error: #dd524d;

/* 文字基本颜色 */
$uni-text-color: #333; // 基本色
$uni-text-color-inverse: #fff; // 反色
$uni-text-color-grey: #999; // 辅助灰色，如加载更多的提示信息
$uni-text-color-placeholder: #808080;
$uni-text-color-disable: #c0c0c0;

/* 背景颜色 */
$uni-bg-color: #fff;
$uni-bg-color-grey: #f8f8f8;
$uni-bg-color-hover: #f1f1f1; // 点击状态颜色
$uni-bg-color-mask: rgba(0, 0, 0, 0.4); // 遮罩颜色

/* 边框颜色 */
$uni-border-color: #c8c7cc;

/* 尺寸变量 */

/* 文字尺寸 */
$uni-font-size-sm: 12px;
$uni-font-size-base: 14px;
$uni-font-size-lg: 16;

/* 图片尺寸 */
$uni-img-size-sm: 20px;
$uni-img-size-base: 26px;
$uni-img-size-lg: 40px;

/* Border Radius */
$uni-border-radius-sm: 2px;
$uni-border-radius-base: 3px;
$uni-border-radius-lg: 6px;
$uni-border-radius-circle: 50%;

/* 水平间距 */
$uni-spacing-row-sm: 5px;
$uni-spacing-row-base: 10px;
$uni-spacing-row-lg: 15px;

/* 垂直间距 */
$uni-spacing-col-sm: 4px;
$uni-spacing-col-base: 8px;
$uni-spacing-col-lg: 12px;

/* 透明度 */
$uni-opacity-disabled: 0.3; // 组件禁用态的透明度

/* 文章场景相关 */
$uni-color-title: #2c405a; // 文章标题颜色
$uni-font-size-title: 20px;
$uni-color-subtitle: #555; // 二级标题颜色
$uni-font-size-subtitle: 18px;
$uni-color-paragraph: #3f536e; // 文章段落颜色
$uni-font-size-paragraph: 15px;
QTGƥx% 100644 format.js ͐JC^^`xwexport function formatPoint(value) {
  return Number(value || 0).toLocaleString('zh-CN')
}

export function formatDate(value) {
  if (!value) return '-'
  const date = new Date(value)
  if (Number.isNaN(date.getTime())) return value
  return `${date.getMonth() + 1}月${date.getDate()}日`
}

export function maskPhone(phone = '') {
  return phone.replace(/^(\d{3})\d{4}(\d+)/, '$1****$2')
}
W
x Simport { defineConfig } from 'vite'
import uni from '@dcloudio/vite-plugin-uni'
// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    uni(),
  ],
})
9x100644 .env.development juٚ]0iZ<?100644 README.md mZnbB3IC%100644 index.html C dX_Ǔr4^1On]$100644 package-lock.json .Kbi	Km~"100644 package.json  	PE؂Ȃ:~/40000 src D~:"Q'3q100644 vite.config.js ^Y<O#*%
ܢri	x bVITE_APP_TITLE=立方和教育管理端
# VITE_APP_BASE_API=https://lfhedu.ruixininfo.com/api
VITE_APP_BASE_API=http://localhost:3100/api
VITE_APP_PORT=5174
;;xb# 立方和教育 PC 管理端

独立于 `front` 小程序/H5 的 Vue3 + JS 后台项目，目录按常见后台分层组织：

- `api`: 后端接口
- `layout`: 后台框架布局
- `router`: 路由表与权限 meta
- `store`: Pinia 状态
- `utils`: 请求、登录态等工具
- `views`: 业务页面

本地开发：

```bash
npm install
npm run dev
```

默认后端地址：`http://127.0.0.1:3100/api`。
Bx8<!doctype html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>立方和教育管理端</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>
i29x}0φ!{
  "name": "lifanghe-pc-admin",
|6Slifanghe-pc-admin",W"Xelement-plus/icons-vue": "^2.3.1",
        "axios": "^1.7.9",
        "echarts": "^5.6.0",
        "element-plus": "^2.8.8",
        "pinia": "^2.2.6+Iy^4.4; Z^5.1.4M/^5.2.a[%`D@9Zp>B@9GlU `b<n6>8	ctrl/tinyx6@ctrl/tinycolor/-/tinycolor-4&UkzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4DlMelement-plus/icons-vur2#@element-plus/icons-vue/-/icons-vue!s(UOzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2%pn~fݖ) 	=14?A1./#V1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQQ/+@(I14?1./#UvCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zc/+@(|[V}'14?1./#Uc0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/5/+@(|[V}_14?1./#UD7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakT!3/+@(|[V014?`01./#VDwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQQ/+@$14?`1./#Use/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbq3/+@14?1!1./#U5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4/+@(8 #14?	1./#VJ95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQQ2/+@(8 ,14?1p%TPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFc!/+6( H14?1./#VibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4QQ/+6( )14?|1./#UYvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18T)/+6(J14?)1./#UuHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQS/+6( )"14?(1./#UIajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jb/+6( )14?1./#U1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4q/+6( )?
}14??1./#U2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDD^/+6( )R14?1./#Uzus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41^/+6( )14?1./#V1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQQ2/+6(G14?D1./#UWoi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tL3/+@(L14?1./#UHLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezoq3/+@(14?1./#U6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garK3/+@(14?1./$T0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA85/+^ #$14?1./#USWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fY)/+^ #14?S1./#UtQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIq3/+^ floating-ui/Ac1.74floating-ui/core/-/core-1.7./#U1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLh|@floating-ui/utils": "^0.2.1q%floating-ui/do1.7A4floating-ui/dom/-/dom-1.7$U9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5S|@Bfloating-ui/core": "^1.7.5",
        "@floating-ui/utils": "^0.2.1q%floating-uim5@floating-ui/utils/-/utils-0(TiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26G7ߩ/='n[~P#;/+@(|[(OO{FOՓ/+@_OOM//+@_O9/+@((/+6ܷi9/+6 /+6ߗN: Oԓ/+6ٗ	6/+6ٗ4/+6ӗk5/+1/+</+3opperjs/core": {
      "name": "@sxzz/popperjs-es",aE13$@sxzz/popperjs-es/-/popperjs-es-2.11!5$UwOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+H|%5Eopper%`=/+@(|z/+@(|9o>/+@7_/+@7)=4/+@(1N@FƓ/+@()	U;/+e~@/+ee`]	M̓/+e
:/+e_A/+eL/+ek	8/+e+
:/+/+m3D/+e`	8/+e
//+e:`ɓ/+_,@F,Ɠ/+@()M/+@(Z/+6/o/+6//9/+6//<4/+6/?"7>loda4.174Plodash/-/lodash-4.17P%UgIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atws|7>lodash-Ѩ17.1Q3lodash-es/-/lodash-es-4.17.1a#U0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9Oz|@nlodashN/web-bluetoot0a5 web-bluetooth/-/web-bluetooth-0. c&UoIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inx7+)*#H%`f+V?SM]:A|9 9/^DFQB@	Q]:A	RzC3W@
_DM@	o|DNG/T_D?|IU	 f	F՗@	U]:A	?V*H>d7?|IMX0|3/D,a>\xfa+^d_]B@`I&UapX2wt9sdfDshS+a2xueFZLVpt0GkRJZSoPmrW/SA4yzXTznhfcMVW59gr7h4YQeY0vJhdJkk2rsIDwgfFgC51dT:^6F@DFVa2%Od(]:4e$I&Uvue8vbf2QlV4quHqzwmJy6dWfmRhP1J8l4wtZg60CL6VoKqcPY2oe7may3+1d9qfpedjK5PRLFqd5k3Isj9mUx%fW f	F&`^'~f	D,5*!nPc146vueuse/core/-/core-145&UaHfz47g0ZhMtTVHmIzMVpJy8ePhhOy68GY5bv110+5DVtZ+W7BsOx+m61UNQqfrWyPztIHIanWa3E2tib3NFIx%fnweb-bluetooth": "^0.0V8use/metadata": "14.3.0",
        "@vueuse/shared": "14.31w8Jm5~f^3.5a.)use/metap
146vueuse/metadata/-/metadata-145&UBwxmbAzwAVF50+MW57GXOUEV61nFBGnlBvrTqj49PqWJu3uw7hdu72ztXeZ33RdZtDY6kO+bfCAE1PCn88Tktx%58Jmvueusf146vueuse/shared/-/shared-145&UbZpge9eSXwa4ToSiqJ7j6KRwhAsneMFoSz3LMWKQDkqimm3D/tbFlrklrs/IOqC8tEcYmXQZJ6N0UrjhBirVCa%58Jm5~f^3.5%<t|2	
4u(synck!x2#async-validator/-/async-validator-4 k&U7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwf6|7axio1.18P4xios/-/axios-1'UE32NzpYKp++W7XRe52rHiXV2ehxmh3wbdgO7MHeFM+vqxLBYHzt0ElkiImtOBxtOmyp0yoC8C6uESVV84Y2/hx%f6)4.0.5&Iproxy-from-env": "^2'&OK|2 )B47)wun2qyj1.11a4dayjs/-/dayjs-1.11!c%T98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZA;7%>d2'Ƿs7(0/w`'A>d2(cha05.6^3echarts/-/echarts-5&UoTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kF)tslib": "2.3.0",
        "zrender": "5.6.q$element-plu14!3element-plus/-/element-plus-2q'UeNH9uP3wQoNqieEIHXiNvIVv+zO5sZDU0CAZq5b0zqSN06DD0/V9xIq1R/qm3rw5k3nBTM1JvpxhCfRbaFLzD|@ctrl/tinycolor": "^4.2.0",
        "@element-plus/icons-vue": "^2.3.2",
        "@floating-ui/dom": "^1.7.6",
        "@popperj's/core": "npm:@sxzz/popperjs-es@^2.11.8Tlodash": "^4.17.24Tlodash-es": "^4.17.1m14.3.0",
        "async-validator": "^4.2.5",
        "dayjs": "^1.11.20",
        "lodash": "^4.18.1",
        "lodash-es": "^A4.18.1",
        "lodash-unified": "^1.0.3",
        "memoize-onePAnormalize-wheel-es": "^1.2.0",
        "vue-component-type-helper;~f^3'03n" 5f8`67xЄ::9g>d2 ?/;13<1./#Umg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqQ <1.5A?=
": "0.21.5A?=1.5A?
": "0.21.5A?)@>1.5A?\>1.5A?>1.5A?	
": "0.21.5A?"0.21.5A?	?1.5A?ia32": "0.21.5A?)1.5A??1.5A?ppc64": "0.21.5A??1.5A?@1.5A?"0.21.5A?W@1.5A?@1.5A?@1.5A?@1.5A?@1.5A?%A1%iK*qw2st4.0$UvKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRC|?ku}^~R|2(kn2n" 5
Еj`s7`w`mxfJ͛>d2{߰69n[+@е!~u'^P+@n|7lodash-Ѩ$;-es/-/lodash-es-4.18QD$UJ8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI87lodash-unifiqKB2-unified/-/lodash-unified(UWK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlf|%pnn:lodash-es": "*",
        "lodash": "*",
        "lodash-esN(I|3o<n" 5$	memoize-o:6memoize-one/-/memoize-on?;)Urkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNx7*-nF.x /hp:.^@n[+@AEwheel-6'normalize-wheel-es/-/normalize-wheel-es(UWj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJXŰL:)l|+@`'|pinia14pinia/-/pinia(UkhUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVua@=3",
        "vue-demi": "^0.14.CPy	5[+">=4.4.4",
        "vue": "^2.7.0 || ^3.5.1Q=#\T0X} roxy-from-envA2/from-env/-/proxy-from-env@(TcJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkY+C'<p_6UG)/-^G_Fnn,)B_7}
_tsli|t4tslib/-/tslib-25&UN82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNja0BSDQ҃f5vite/-/vite-5.4!c%Uo5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25SQR1.3nU43",
        "rollup": "^4.2!&Zass-embeddedtass-embeddedq)X]5oI&TvAMKHfImQlYSy0C+PBue4s3ERZ2xGKfgZg5GXAsLInq1dyh2H78ILVP5sK0KPFPVW4kv+OGCIvBEondcjpZpa&fWIU	o|	FFF@D'[%component-type-O3.339vue-component-type-helpers/-/vue-component-type-helpers-3&UFe1jyPJoUGpJOYKOri44jduR7My4yYINOMJISuMAbmrs+L5LbIDUc8NTWZYY3EJLK0yPLuCmcd5zoCsE4k2/K7demi,4.1!2vue-demi/-/vue-demi-0.14.1#UnMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISph<VUvue-demi-fix": "bin/vue-demi-fix.js",
        "vue-demi-switch": "bin/vue-demi-switchaG4LJm6Wosition-api": "^1.0.0-rc.1+0.0-0 || ^2p3"W	osition-apiqK63i6$UHz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVa@=5%zrendau5.6L3zrender/-/zrender-5!&UOFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4aa9tslib": "2.3.3Ĺ"x){
  "name": "lifanghe-pc-admin",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "dev": "vite --host 127.0.0.1 --port 5174",
    "build": "vite build",
    "preview": "vite preview --host 127.0.0.1 --port 4174"
  },
  "dependencies": {
    "@element-plus/icons-vue": "^2.3.1",
    "axios": "^1.7.9",
    "echarts": "^5.6.0",
    "element-plus": "^2.8.8",
    "pinia": "^2.2.6",
    "vue": "^3.4.21",
    "vue-router": "^4.4.5"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^5.1.4",
    "sass": "^1.89.2",
    "vite": "^5.2.8"
  }
}
qjx?100644 App.vue $
	XM|\5N40000 api WSKdÀu(-40000 assets gm{{7$Pafə	V
40000 components 4\ǭt˜sHr^AJ40000 directive |_n52*5[KxM40000 layout   C.A<Gc100644 main.js `[<#'3{100644 permission.js 9-˸6[
 w"t40000 router y
ؚ)29a100644 settings.js 90f/X%_3o<40000 store 5<')tjm	u40000 utils -oZ!OTL>40000 views oF=@g
jGRkٖx) <template>
  <router-view />
</template>
#x 40000 courses 8vU"V<40000 leads bG_8iŝ<w100644 login.js ]E]ߙ"B40000 points [ngb{3& D40000 statistics H ę4 %$air40000 system %?kha9x7100644 upload.js C?j'19^WNS9m]x$ 100644 trial.js z?HEN9؃cr8xpimport request from '@/utils/request'

function toQuery(filters = {}) {
  const query = new URLSearchParams()
  Object.entries(filters).forEach(([key, value]) => {
    if (value !== undefined && value !== null && value !== '') query.append(key, value)
  })
  const value = query.toString()
  return value ? `?${value}` : ''
}

export function listRedemptions(filters = {}) {
  return request({
    url: `/admin/redemptions${toQuery(filters)}`,
    method: 'get',
  })
}

export function approveRedemption(id, data = {}) {
  return request({
    url: `/admin/redemptions/${id}/approve`,
    method: 'patch',
    data,
  })
}

export function rejectRedemption(id, data = {}) {
  return request({
    url: `/admin/redemptions/${id}/reject`,
    method: 'patch',
    data,
  })
}

export function verifyVoucher(data = {}) {
  return request({
    url: '/admin/vouchers/verify',
    method: 'post',
    data,
  })
}
*^x $'export function login(data0#	uth/loging(pgetInfo(0#uth/current'-updatePassword(accountId, data0 `/auth/password/${accountId}:updateProfile(accountId, data0 `/auth/profile/${accountId})Njxu7getTrialCourses(params = {}A#dmin/courses  paramsgetAppointments(params = {}A#dmin/appointments  paramsattendAppointment(idA 6`/admin/appointments/${id}/attend`,
    method: 'patchcreateTrialCourse<(dmin/coursesm9TrialCourse(i!*dmin/courses/${i+deleteTrialCourse(idA 5`/admin/courses/${id}`,
    method: 'delete',
  })
}
$x$ 100644 index.js pEjrk!p)yUxFimport request from '@/utils/request'

export function getLeads(params = {}) {
  return request({
    url: '/admin/leads',
    method: 'get',
    params,
  })
}

export function updateLeadStage(id, stage) {
  return request({
    url: `/admin/leads/${id}/stage`,
    method: 'patch',
    data: { stage },
  })
}

export function confirmLeadPurchase(id) {
  return request({
    url: `/admin/leads/${id}/purchase`,
    method: 'post',
  })
}
TxO 100644 manager.js ]_q4M{A~Bt100644 redemption.js 	Y<e&vuSXtMoximport request from '@/utils/request'

function toQuery(filters = {}) {
  const query = new URLSearchParams()
  Object.entries(filters).forEach(([key, value]) => {
    if (Array.isArray(value)) {
      if (value.length) query.append(key, value.join(','))
      return
    }
    if (value !== undefined && value !== null && value !== '') query.append(key, value)
  })
  const value = query.toString()
  return value ? `?${value}` : ''
}

export function getPointManager() {
  return request({
    url: '/admin/points',
    method: 'get',
  })
}

export function listPointAccounts(filters = {}) {
  return request({
    url: `/admin/point-accounts${toQuery(filters)}`,
    method: 'get',
  })
}

export function listPointTransactions(filters = {}) {
  return request({
    url: `/admin/point-transactions${toQuery(filters)}`,
    method: 'get',
  })
}

export function adjustPoints(data) {
  return request({
    url: '/admin/points/adjust',
    method: 'post',
    data,
  })
}

export function settlePendingPoints(data) {
  return request({
    url: '/admin/points/settle-pending',
    method: 'post',
    data,
  })
}

export function updateReferralRule(event, data) {
  return request({
    url: `/admin/referral-rules/${event}`,
    method: 'patch',
    data,
  })
}

export function listRewards(filters = {}) {
  return request({
    url: `/admin/rewards${toQuery(filters)}`,
    method: 'get',
  })
}

export function getRewardDetail(id) {
  return request({
    url: `/admin/rewards/${id}`,
    method: 'get',
  })
}

export function createReward(data) {
  return request({
    url: '/admin/rewards',
    method: 'post',
    data,
  })
}

export function updateReward(id, data) {
  return request({
    url: `/admin/rewards/${id}`,
    method: 'patch',
    data,
  })
}
tB-x( 100644 dashboard.js r%<Sk'j2]	x/ :PcOverview(paramsa pc-overview)QgxI 100644 account.js "RvsBIo100644 role.js VSq & T8h 15xZimport request from '@/utils/request'

export function listAccounts(params = {}) {
  return request({
    url: '/admin/accounts',
    method: 'get',
    params,
  })
}

export function createAccount(data) {
  return request({
    url: '/admin/accounts',
    method: 'post',
    data,
  })
}

export function updateAccount(id, data) {
  return request({
    url: `/admin/accounts/${id}`,
    method: 'patch',
    data,
  })
}

export function deleteAccount(id) {
  return request({
    url: `/admin/accounts/${id}`,
    method: 'delete',
  })
}

export function changeAccountRole(id, role) {
  return request({
    url: `/admin/accounts/${id}/role`,
    method: 'patch',
    data: { role },
  })
}

export function changeAccountStatus(id, status) {
  return request({
    url: `/admin/accounts/${id}/status`,
    method: 'patch',
    data: { status },
  })
}
nx x8	istRoles(A#
dmin/roles4!RolePermissions(role, permissionsA !`/admin/roles/${role}/permissions : { permissions },
  })
}
bx1%x import request from '@/utils/request'

export function uploadAvatar(file) {
  const formData = new FormData()
  formData.append('file', file)
  return request({
    url: '/uploads/avatar',
    method: 'post',
    data: formData,
  })
}
dM3xB 40000 images cWTgFbsEr40000 styles p_1J>cDiB$x6 100644 login-learning-studio.webp n&s-eeGw0k].x   RIFFp WEBPVP8 p 0*  >U&F.
gn!N#ٻy[V=Wg߅y6X)?@er<5yB>P~>u`Ek;DNQf:{{߸!#c/W??G?owO~x>-Yn7w/I_~{g{??SNtwfQW/ly~{L?
*'׏\@jec{l46?0.z.G0c;Үkﷅ.iΥhj(G$U/һ#Wsu2I5ރt%2Hs%|-CbVʸ9W˽xɏZjep  3cW*ǭhعONO+|qX|$AALm"GQʥ8$:Ԕ~4+"huRTcR.;I/.!D[uM4?iXUȟgţuY=',67)Z솇&~Q
pAeW)a8 z"Pgf>d"(>-"#^N¡ޡ2&%cP)
s,_l[GJ9h/&e KF=ȵ:6E\!k\O%jӺ=(G;!ho~F膟aF53ar	`c(9}.\-v/]m{YNpZj*y[Ղ?&قXvv88VG7fzC{|S(19P.k	1Pn6<QDW&Yt4&DUȄ0Dy"w;htGc=zr2V<l쌬bl%"
S/68τf"լHžR
H|'
ۋTCl'$5]+4Ņ='|ݕaP[@NXE/BnnyEWQ)U~HfW>MjU.ވMHo^̆f8r1v`eSD=Бpa+2U"YLm9xy&#@SpTNOU1Yg[ |/Ñ
fkJWLuڌQ
ZqjvH=`wmH1C]<x&;$bF6Z'ƚwZS墴o݂Կ.+1߳,TMDi܌yz^1| oG	cO"9}Mg>^8(JQ@z}7]V{NU̯G[)X21^AnۮR.MzQ65NSL2=l$MM7	`V"29&
ԎuJ	z)Z,-"tS޽	(ݠ'<Y9V Q }V]m.E/z8Et|TgVܸ|8a Q8'r)ɠ+5?]䩨	`".a%LJ
>-:~-yϦċԪ@vߠ4eo'
۩]vbn:Lc+_'ԄfSSB0t2B4K>GAʾϽ)ޕBd8 [ĉ]6Uی@lx~Uu!S׷>ǣRp~-2mxK$SsLMmak`'C>^?oF\Nơc1/ݾnĕʰ+$1>"59-9qGARUܖļL S<bnp!"n׃kټc#lU;;qv=mH./Rs#i=n)-19	C:ťn#x3T2۠=v<䦶>z}]]z^8,='~\舞6\VDZ!aB*@+P?4iwkaCʕ'!XqAӞqݶS5;L;@	tlY5O|߃^闷Y& yҖM&?TlNHE<vs	EЦ즋ZU0,]äpb_$*}mʹahZ.jYq*X?̂O~m?1vbYd<*fÄbU#%wJKFBf..y%7`<2);_TOO>TqJ|ȚChC#+z&٧u<:zd'ɚe)ݦix
~L_E|9Y3߂\|xj΢	ļLAq"B<3J`u l{SjP{4)57f4wȔ%#-cۈVp9~	#Fg?<=}357P3ttaHSИGuhcGB^Kx=]fWեgKLhq-cy7Fr¹?^A,1>Um=ኞY>
I]U-J F7͌f)]-$u-Di'j4/ur	<Ki{1 $E' 뮧\aMP)pBM(uF>(x}rAz=]W8v*i~B-*µS	񂠌)bh6;q%Q8RֵU/VE9urӰL&uN't-snDDӞB>T;pI.ʁ  t
|\T5׹jH6a[WoHh&L{D٠R;G%^$GݥR>&l{{= lհ)w??N%<(FGXlKZ8qT7M) Zvu1[H>W"6IM,|sG7z.f4ǠZOs{p圄LUKJx[MEnȥώCxf hVnd<mK`h<e07^`˜vx>bWQBf~+\r 1cWJp.V핓`IXno}<]	AOɤ՛R*؋q_Z!v0goG<tshg9q"
Aٽl
8_LKd]sv,~K}1i:}n_P̎_yi7B<6q:SuYy
gejMTWsA_k-?hŧ}2ƨ&F>`٤WbUڌǞqL2n5@0*n^8|f!MYvPd1PcRA(I!gD\L2C[[4dF^utq2@g*ʼ)`J7+pc]"\>LzWµT^vBTo'LHtHD^՝?M+}wԱiˮicXqѸ8zP6h?*|vxk#PUTI4^̼170Zyeߔ]3YmM,6GS£_zFƤ]v(༴oBKJO%T~uε)}`I(&m3{MMO%I?a]2Og`koH;|V԰:C;α8'L^Vb6$Ao},Q
|q='M8{k|;ۗ^ʻ+/F%?lQf4;8~@p<(LF[oJ'y71{Me@e])yq+-YlS2Yuw%miq]ŒYaX0;}0H9k1@-4Jݍ"B F4}FB&d5XzhMMZᵛYI\M GѽN7la-ͨ=nG*p C_C -y+ie|#lX*H*S\ YcYX\ag:EeRaA[`|(ifqQAr(W508$%~O!_F56U%.0d{<gź"x4xA6=:<BHX ;nLyb WaB9F<>͋#$/*(d16Spcx/>8T],>E2oB8y}rC@rrdt,
	9a@ sU0*_I\L lLeZدTΚtTZC?7LUW%dNg,r[ھUYc@Ŕ^24GϩlTr<`=/A.)LZrOђ$ugPv+r<Ȳ!d]/hl:6P,ZS1lv+!d,'ߴBo'{× N$E)@7f% A	2;*?m u5.KrG>Eeq}9[ކfy1i)Xؐoqd2n&s~r:J}i0sKD+!VR*o
XioXgibq~ٷ>Xd= SE)<l'P[y 	}Y)7vةpZ"t&Om_R(^EmjF%bogB^]<cc&|԰&^"hbQqgq]n:8E(8m6ԖP2/_޻>D˺̮80t|J=?}"ˁ2 #+H\-F9,ε[Zp6g4!==PXi1wm[X+}08Q?&t[(a{IwOP /M3q? -sA[U i='Ş_OމjQt"z7Ts]6QWFUW9;9h=nWpR`sgkanS:Фqetrw3ᑽȍӟ6YёG9viv Ҥ4-h:-D #'ܘS=?D9IsWT+q=*Ѽ$7+D֍L1-%(e, lU_.rvo5+p0顼!'Zc%8{,Bt#.UϽTEB(Utc,{9YdR8S͸ň\27i0QR;"p<jR+:jsܱk!c*l1[t	SNX#y6)Iu	}VaJc-LǸ?Gve_"(g-;+F9Ϳ2 uK@I̞؃ayJ6JNtʁxlĊq{!q@PJT`5\нzlPw$.6!*lߕIn Vjข_U ֥М++]hCX.V꣸2:~$ظSlt4LIKۮs3O#G^Vh=-Vk<di;X<H:.o47/.;zD"e2C 
ʂާQȟxmҨ'#Cgj\*؞o6k؛U;w"tg͍%l~0^<1e7181jPf1v\?R-$ǁݹ`˼nMɼ$N]x+$19ׄ'JK~WrcۃA0%[^4miCtx=)JH?/f)G(r=o		Mh #7;})ǗО;Wpdߩ*oH萦-XhB	ÒIijGo3,Ga$<	FpEP]cq!vj]^jKtyp2*teG?Ik֋v嫀%u%y/o}U!L"H ڑ@=B@TεȅT|OlN[`IҔb`޾Y4K$/S42D"$orm1,a
^/*-nN|hFA4?f}kߦڞ4<4p&ȊHWܪQdT5HFwnV\0tW/!ϏI|h1v)w7ÉJmΟt<TgJ!z0N22@?4!	IS%^WD1.+P?6S
p-cb=?K#Tq@
_a>vuu+?s0=>S/,9S( XOOBÈ}n@ھIp1эL-lxқyeލ0g/zl}_(x%}i[➣\Sn#B- klwS<<! cK6q_l<X6*F5[D+ eI	pv8dPB
N;PEp6wD֘ح~?3ǐ-9j _ĉ*lYqYZIm6	p+!mA)|e`&;@]>+]r}F*Z7q,e| }pJU|RymRUm0Md1! lyД	$_<w,Y-nB`mi2qۧvƬ_j?WL=@}<{ᙊMg[TTI#	Gs16q2ad[K|tj!1v[@X[6'iLVMf;䉡\.6Fhx"g+5Wyh6(:h3?ey+GNʨ%%~ %eG.D(2.>X2RHRϞq|jv|h}
g|WDC'̣0 eXPCa-q	a(LF}H^.I}!t}ځEfT[0Z`HhĮ|y}=ǜwKms@D-񬑋DFATgŽX$?_A!}6C+eֽ'V'tDf`VSx-?tVY3w0mKb	*zeT/taf/v!HzXX8)@=4튯`,_H' 5 9O!78hØ{8qvmѹozA%ظd4Y+3l~ZeB{`(xUa*?:V޻	-
ILHWuOM+sI?Z5v4(%٨͑MU\wr
ޔP`~Z@Z.Ǎ	q%hC:m_=I;[~Dgm	9TRa"W)o0btZqLL$R/B, 9kʭoX:
5ȄTۏYѩ}8DJ䁢{.&Nh>B0'|/Kuבys /:U(Qʧb5W!\ά#`6vGw2ķۤIAtovF8osřY;yǍ->4%,[8:8zMtڊfLTEkI1aUbtJzɹ׌#]x(_9bL{juYwbmTn~T,ӟU~jzE:2<R
J^Vͦ0e]j[ 5-^'02z\¯SQ^$Okki)3AP\iaMuB`JZFRi꾶nw< DJ,JxHn=8v%B@qQU`Q,_LQ<Q=/b	ACc"J7en߮~j%͚OB:̈́,BgE!p?<)?\U
9	p1)) R۰\ S*CĚj}v ʲ^];!Wۭ5i=:DZdJӁi^?c##%*u*5<,uMf1)McTāP{L^E,Ĝnw2*e9zQk*M4ombK"(dw(r\'2W*V¡ CKn 󈗺9NCqBE3@Rt<m^حi_Qp`y$uZP=O;;ds<vOVT(ƿ4j<3Pr[Xn7~!5qI`mohCpI,<>E"muީeu2Ѝ8\9J­S3Սl -^nqnvU&SόBLB_0!a=-u(RwQJE눥퇲C|5O62W唺2OO
S~S	|J{il/QIlh:qN5G Ӈ=-@74q+l:wsC?2';^uC@dj@b| NURm8ʌ1Yɟobq=zyAG)2E%ɶ\9RWD$$pR"Qq;i2ү-VqU:aBDZP*=@E
>i/Bxt'd6!|~_L\2 9CDF^8\4X j+Lf(A~h;]7[ b!py^X[I"3NJZ4ݫbĆD8ģdaha^>'q m-K7aOň?8oXE{|،l>گtkM݉8p\~ƻQ(OB2ߙWlE%hIzUX {v̜ja
o^kCrtrJ,qޅgqdKJY%r	QFXL\!ZF].˺qȈNQN &E&Mro̠Mj?T|jIJuz8b}T'ҽΙ瀚!1-gM+[-
I*ԽGvP!";PbG=cVÅA6U> xAx|=%a1(EZ
Mro:7Ibq.k>SyBټ޷'O_QTA*94 ϢNs.Q<ba9FSSDk*"-pSi!]viM%`W>{󣮗{'.Rlxԝ1gw*IiNy9n$PŚzϯqPc@%!u/ص<縱mn,ax.(֭΄d =s@=>M.aNWao-t}:+0@O۩!HátzW< ic'H#zY4Fk2wd=rYvBօ
 8?JSf^@tgW~I3(aT,:8xBwizn
1oi֞-劧[G t\.z=Uv315jk4u;ڋ\mLm<4ฤSR:Bjx½,I.$jFmO6+ɍ&֕eEP3BpT7uŕ`ada
XOq&`4<zk i0i\>(6@Z2B#h=eQTLW/շzMnKh67PS[I2u5"6="&U!(? t65hmi.
_:j҇SS>'{{RQ;kxb\?.	y v6_\TsKMLދmX'ӕ\G."XaBNyDPR"@E3y*c ˻yhzabfo6n"J
9D98'd6Z_TeNn7-~AcͲȃ\NϪ^!$P#'7|HG)x>	Ao6}RRPNL%6TaYcT\m2w	)VFE}>p7{Z$)K
@/-$7PaCӪN'KL>btV1]2ch/9u*%zxFrQL J>rr%X*/!!`w,72@k6'TypCnR-xl[Xϟ_#?¶*AoHY`o\Զ$KZzg Et0rdR\1¼lhy;p-[FW?-Gh:N+eԄ
,f챌Ʈ(+ӊze9`3ks:IqsvŞ탱:̠BEٖ&mH.χF9Kz-N-=P̡Qe8P-yQV4#lJ_g_Pӥ<ʰHr䕾~a~G#]v.6+`4#u[@EuqHMTHȔ~_<["Er;	:c)$wҭŝx>tJ4~|V]]VfZ\dB&>CJcLe&UvS IFsظ&ZGN~I
CܹV(>$vW_94p#m4Ha;!ܚU2p
)3saf⿓m~k]5n(lћcP*3بas5LZI1s׶49:hw嬇9{RXʥCm9nV2 Y?~N֘Қ6E:qr0I,M[8,i:VkHgj$-<W?A.rtG)3ܕW @]pS5V
۠n (˨ UZO?XP4XeEhyNr+ RaV[֦CymqY)]p>u5MQdc[@$V#ud窯>`w(Q%g~p<}<^6Qf$#r+`9#V+WG1E<qG7=n}>,ɣ?qOeFY>R ^@)،'A]8<toT\~6I >t{+^{?UfxF4G?@0:S/ſt_^,4;t-АL^,>3WP9(=GˑL. INq9>kqL:\au A{?{ 	u6]ଯ:×cNe
[p:J2zFWeFh\忭gDw3w;ު-A_q;֟@auli4ePARهeE !IQOOIB¦#zrG(C C~W"iZOGQTs]d^_q_/GvZEx]u0iY&~e5oI$>M*g_dߙr~Rsô.%,$ìS}}
$ylPA
ڢ ~?Q ??#=	ӡ6O۵n{_ sy).RGWX>.6 Dw
} t	c^iYPY̆-I`̬q #z1gwẔBjzvPO+ BQ* }Tuo8^f[]XSq?d+Qg
A>uBL_QA/b\7?ocMk]_v|ᢀ/jan0}!U\.jywiNPy4έ/Rс@ν$_᤺Q3+nQ_^SUZ|6E"C>S22]T˓vFO4]o(OrsiLB9;c-\qP態gHʏ%QqF4 *ȑ--cEYdZ5Q?	ʝHDmE]<urH$pDHg7"zޓ㜌Lj2*SNL2#D$AC޼82G"B8#u?˴͢/fTi>x!{';
@߆Ёx~}Ղ|x&)?\V}${ܦAoB)GɽvgE}ʕ K4J \ENǏ@Lik`hkJgzzc?e&dFх%d5Xt46;5jNӫ(]HRyXpyrlӨdf2Lvh6ƫ"1N,1ү]RHbNlfԶ]GbI~k_#m|$V{0xkid)̩+w|WZɷHn[SbӦ,iݰ뼸7fέ9gI 2=|k{/XKZ	vBxhAZ{x+Rx`h?]Z bFZ\P PcJrA@IY!̚y~1QZte9Fw\s:OW8v
zBjzIV+vuC+ϸ%*=i@[٘<!|DIon_uKzN'#\Z8&`$)Q՗Fy߶yԗۻ
.6dNmV+o	6^E>mIq5>16mH$ǩցvu:@D&U_L̎ʎdY1+Ym)=&]a0=*{XO#hl)HM)͂L}tFb{2w}7bCޟ3Jy&,Fȗ>H	3WPK͂N
Jgd	{y QXV0 -SD}z=^rx=uO/Iƀ$L(7d1bnAZ3R;<37fM
.`7:7-o+"s\mk$^W
֛<+]"5+kRPzf.C|k+F[\J50iLZ.>EU~L:A۔gLc$w:5ng
)lN!e'Vvl+E
0ܼ~~Mo}@hijza[f1>PkpHU9|n
u:^NsFE%L1/8H7#CKP.
ICaə|Few-8NtLa@yq(]:/yFCVh	%x)sy͵KYN``BE-HMl{ۉ3{*@Vi\dn'ug0"DСjϥSsZEbxg6*>Hjܨ6qS-ea?tc[kgr ̬y9_0ݮmalL*H+jyq1xA-?huJ3]lJnUH޻4&,}-	JNL)q&D~xq-_e:rm(
Y+7ˇ꿧xPEXEDǭGT>Kkgᚍ`i/f ]hNF@j55Rk:ouke*(B2srUWobH[diWSS-["]hK؛2ۅ[WMA(:0GWI$w2gF$1_Uw!ؠmÓ/+^yaWڿJ/}fM(AUZzȾzUzP9<`Mav&<E˕\UhLe!tt7d5*\~]&ܫڱl|ɋh~$cR4ɹ῝|մ~
Uuďhyqbx6sߕoRՁ}ly"hw0x{g[тqj8z!	qیM0	.LLqs^A,p
dG4oHĎ'Ǝē0je"K@cVEq(v7<viF1[mT776a3|]\ (M0-.(OG&,7giϓ˵Í/Czw+;Rz'Eݍetȟ<\nf)ۼ9p^fٓaё6u<ABvx@dqN^^jNK&".S8jTD
px!%Zk&F`FSq*=jW>~^@H06Tka]wo1p=My/TXyc'6u"ixl#-щ[*Q
	CTWlPNn^άtzڑc|<5Ga98x0qOt`qJC'=Yт3hߙ\Taq1׿$4N"0X\B,^,!äQ;<gj]VPH<Yg?4ꖯ5]{=X#jXiN
f^O>&g+y\SLИNJSUg=ל`rN}@ᚲ7R
lF|8l2>z4{-R
`Rf\d#X%!}]IS$w`b%m>Nng6eAAъzBŁKpOo3beЧ7a23nd9/v=7q>:`c)BqYsASkDj{0ͅu|s~?[pk:~FwT.Sú[iX=ɻJh<wp.`]	B9ZULoɃn;݊JPF5(9SHZphV$~hT7Dؚp@pD_Ju"ʿ,qv!&FVBIzQT癗'|@6^agn,ӂ./V(*E8 1K>אAnZr@/,o	9QF8"gSHADeYA+3K<]8'|yhَk~UK{qҥ@7EP''Ŷv?0ʼT!S/IWӗyT=bS&pT'_ʐ:^/61CtI"r+XLG$AmFA QS*4ڂy!fөoF2̟J@8+:[ny0쨨/*^u-)͌
e1SN}~KZ-dx-51['bUcW;ʣާW$ (2(j'*ץwPEtmFZXpV=g)+8;koUS츔H!Ծ ƘoSxS씝1u" aQ:kWQ4L}NJ '
IMp<)YX].H!v{[!7h<:~h/"Fs~^AItݏϯK&vݷ0Y~3ev]JcMOCWӂ	U2"W,JSvrؓLa=#]i[}gxi4Kd9w}Swe<ǦԹTQBcՒoE  ܈^{}\lHcZ Ys%y[pc4Q*y= R]@#Zaw(be48_ކz]c1T@ lCzFO#yyNc)dugq/Q}_HfÇx⸁aAVW@L =-㪬Rpsu6@eGl"v@oϜܟtX$&Qv¿z s74	_5 U#ρpug$8~SsnloS5aocW3[spUK&}=@{[se͑H8~C(Q<^)#wbn`Z-SKE=e{r:VAJKX^2lG@V#ju;q56hFyMnf^f)j-";ۍ^|{x>؂G e
yȷy<E2rQUk0UCމmI܆3 %\Y:roStׇ͵6*a+c2AGoPtpE?b%%t}
-@W£+6y!Čmڬ)#|Qg BTd |+<M)f ζ{Քze(Aӡ5@~Cs]Cɛ6R٠
H2lv9MahO[q؇Q*|a8A4EtFmځHVuR0'#C"T=1.$ʣ;,ˤ ɎNc҃cvyo"87Ư/oG.oETَJ~g§6*AHabvuL.Jf㝞FѝlGۤ&?W䧰^驪c17O$/9{<a_D9\@ʴP7R= f2/Ň׷TIfUέlT.3*2l˕8fܮiL~$D'rV&zpv7Hz|T:aL[() ĕbT1ӏ4Jr8$dB>2XHY`PGhLFVbA[sF~zS{X0<!ԥ.>.6?ֽ^:Mz\IZLzҜt wM.o/9!'t48ߞGs7v0K1KAB6_XpV(NsŞOT tozXY(VUPkNv)z/>l#^hII97!:79M 6Qz){̦)X7x"91J.S?å`E,ɋ	xB摢pqY]W'_@N	5жEZKvslv_.-m}*>=gїoz(\Ul#$)6P\Ji&~{L<i'ٺVRQXAW6Bj/Rf΄qoE"󖖄~cg <ᗭGvw{vG*y)-~Tч ˿}1rŏzĜ#d"tTGhbw#(~@eb]d5!8s mFjNVc=EL`G3]Pi7? -JqzADl89Yx.3oOL1Ճ`lHӯ?>>!*$1KՍsS1pxwsaFhֿn%iYqx<|.ϝ][S᠑aztl1-p*hfqx?m_E3
%ǁ\BV\GNRBLt9_yt$3]PX2ʼY~[m ~+) [(<\Uxp\W8+=AcFDeQneO=-k&9a|P(9}~Nv6Ò+DKRlyXX0l$ݠ/`ڨrʒ2Ql{8Sٸf3TcS}xMKa`h"ild$i<g 끫F
>eDZ\7KY$?I$$9=OR$+}vk~g +-@:XY-> fr=
ΏnnBg6:HZ&;]>}B19]Jf&B+3F˹_ήFeo/0k@VA"jЄ]F)ҿk7bjr}PŚV̡3+wL~˜o?jq[8t5tIs'^ GʺY-ܹ2dZr5G^%?(C|74u4J}e=Q4#Y"ț+4XqT:QR?p	|JlD1يCYYɀS%t	b84Cgf	ن	)T'OcCjeiDAS`}'DQS| T'9:-$q6^Ŵ>KpICϴ߽v#êz>DMAz[%cj.7g,E&t
qs)BkMFς~:ʂ$O0͗phvqITt6 3<>ޕFZ~KW%+N<8oQa^Kw?+&RQ,IJߴKpmEV|
.XxI*9<960O+<J\&+XC2bYbu*6f^Z.a\X~3tʰ9,rBt7#k:8
5GV\zic$m&ѣXN_L)>|򰃺HmaKa"N}{ȳ|̯vL~op^dDABjfެ.#OadBCB-bhrk9k"JwZ_S`ˡfKKf@3U\q7ܣ> !${f[٤RI]@/4߉JCOaR΁L%t!{h9Mf9AT 4Atkxh4:' mL	/Z+wm }:CDhƙXaFC,.Bǋô\F;'{%,L}#?Z٦Su[1Tk=׷wb11kNUnC(^W.ZI<=xYjXQR R=-P#J_ݩf14~
uJ!?SF)k[U~;LuZf}bdBl#xk+8A@cKAC땚T,bZCZ1.7LFSI#05 @[泳pǳapPSҲ*qn'ݗrMe?HB{ =_g@ْPV{pcK&ci4Zf?n'xib${ڹ$KvjvF٠ZJ]$ rȐkEh4w<BUjW?=ߏ8tDɽqݳi&};>w#0Qy|<<:qr3M`VOH,yX.}a	&O+0tW*"Ў񖩅aq`^vi*<j 	X=;󉰫Dx,Ԇȡe|4, Z,[L@*79K݉;	B_?TJL>m{H~Lz *ǭo￳jp_w	|
t+2pӳ,3[k4]=`Z~t[c+lb*2k&:pDBׂ~߼qkL;^'<n&85w"`;$Ġ* LN
Åc^`*}"a<Pm^ Ml͢q_29;!KoQ0NOa\D7׮pjlQcor`خ tQ$/rm|\ +! pq2]XEkh,q5!+gkkr;G
*y񫒬˦{T`2K}FlK6^?d}w(;] ,\ϋijp`O%b:`C| lgQ|:$DV "bH'T"4%bjL@d\S?bW򱈋`l+F:ᖷ̀8-TkZxC[颹/G1Ѝ*xWն  r2ȉpDW#Q3U[fē]a=MV7ѡ3ջ`5ơ 0j! ytg`ꁙ0p@@U#8*A[跌u'e_A%H+[f{e87/l,=FMJzBo[ ahf'7NnYԚO9:A9Y0,   :.֦L[ ЮJ`469LL$M#|=}=ц2VURL<<8A.㊜wdKS<8JT  0`e)qJR^dh֓%%9.8BFMqH^~%0EA܂)Ie5LXC؆A)x&j8?	\hA~1=84,[XYA(R)dL)nj.0Z{PCV΋&k>:ʹbEÎ0zsSG*F+-YsBuj!+8f~	7D  nm8%" һR#ރJsڶb5)##xeW{aM֯]e<^)-Dz[7c  JFF l{mf *$P;:  |{\<yqۦqܐ쫗 J-e-xbA52'U3Jw1(EߍOpHAÒvZH,M`h=& 2ߩb0"[!;o(-R?I&+ VU,5ӏ󚅉GUz=.F3M	K Uͧjnp>\M	0"QА!P֙c&8w  t7fo~2aƍFtc	yX{a;,רK: .ɰM7a5zxQ<"IH䦚4Qc!kșuzv{G(xG/h9\x#=Pz-rnZHK׈؆'
O V;9]NPWPOke/r!g=EpLC͋h|$gmobūՖN	Р =vpsg$RykCQzcHr 1mݶ-B|I/[CCB
gW^UH*ߞh]m;9Yt =3-=3JmS:i>~$]g>Qi ˅ Ϟs?˵Hg2"UN	0HQv&Pl눾tr,|`^3XS5b$k]=izJ9wc6)+}/c<N;{[숇; qiȡ;ūfoʡμ8jM3(b
|AV4)d}܁jG
S5uv
xn'/m2Md4EB̘j&}篳fcY]pz@}ȟN|h3K'W*ӹ[ŕuԩϏao,@m&\oH$Vo15*-;gP;Жmm}bކu^a!ɛY`68%>'>\ޔR]gW]&ݦufdzDe:\d51ȕbK<vѐ tCMm7!ݡXf5څ/l+aV/n6F*v^EHB9$s
NwJBq~j|}{+._B.?;ƹk!Sepk4泦2g7	'agPL?b&mRb+z+4)نqy(vQ߲w	Vxޗ"nVqEjrZH*j692\mr@{.>d@gBqlExۉ-E$z<{;$IEwd}2	9w}3Ȅ iہ3SLAae6oϺ}!"$vq CD[Hwiu"&&kgh4Ь,xx!bn,g\뀣zBGD$Wf:鐵n.ܜ\4"#RnvFdX08K,M&2y!]NjJQxҴ;0>ۆ {LswDKZlU]t>])`# А%V=/֓!
Xkט*3FoX٭Mp"3qq5'}z$RzzK7	ю@Hg5b%C[XmmoQ^8;GZIE#Ѥ*R.A
8֛bS/>6HgN>=yfkccO{W=*+05\=sky~8p+̘ QH4+S'`]1DhabN(GdT
e&w;FJ0voBa/e^	IZZ>Ez.ylmiZa]Vx1ہ̃X{[Մn~w`ءċ}VceC(wtD{o7 Z#80Z dF*ieDOu {+.XMx`ˡ@C@^'j|5]J-벏(K>۫kvP+9?PBD
>'D|!"z*x#+IW, ;Q16nK;[-ysmsunx	5`笇q?Hoc>/㸚Kx0iݽUEG=*NPMky`:
-F{MhBVz	"0;*U.=J##
INְX=8l]VvQs/y =1JLjm,BCcD	^RZ`NE-81i7dJsRx`'E^{D#EШ^$yvsb-RC ciA	esZ
fu̏SN, 6D[f&h(Z[Sݵ>HcvTR^BkՏݾk szN_`۳	l3]
/,7@d?o[w
	5{eyL#$`d_Orho\NpkA?TbE 镕idj̾On6+i: IvkXY_PXivxT+v@տaS#KbEā2]\$BF~,-nf2y-:+1%뜪gWRXi8h034ih9ڢ?t0\u年y	uE
ݱMro
x,WcE-&"g|ΊU=O>VVq*%Z$]j%Ы-&I_|E	{	1+5!\|!,iAzdN0,9sp[Q|.7,@(l
l,,v'~FE9Y?]hKwzyplQ[uٿʊaVnWM&5[駍I;Aay@Ok,`VʨrXqe:%\ss+O3[O=80W6&u sR^wxddz$Wn\,lIO,Nc?6<%ʐmQ,K7mj2sQt>j0dEI{9?[ D2ܖp\T!HCN%9o۟ğLWhpc;.=P2)ksd]u<|%'/ JZ-"`%E,}ocŁL{k!vK#~!Zn
1D8ò=efއ 0"=T9Nk:ڪBwӾ 	À])bS=N8 ^7)ŊquU
VW#ŀIi0#mn=VFoR['XῈӠ/@CLz ײ^ĝ/buk|$=7n~68t7bTAس>**܂}WQQoY]y>"mQAWɵ[x+SviYsjYi^U㪻{kgr}A*aՖkCꦊ.9u癇7$OHMr #*W]+j:CHy)HȭPJսau
9),60d<fϣ>S+:Q2(q}zLS͒J Ns6M}Y}o y{,sxG1L~<6R2J#l>?fx|bzHX 5.iI!3i4rgzҗUp^@FBaD.Z} e 	9
BPZ)oDm#|YǋlW0AhU,~8>%P}d T: ظ)AY#ڎ^Gn}5;{O[Y*yMv}I|xяy,Yu$Hr 8g3X{tYcp&-%b"{k`PoO5kz_,ُ߻]n+Zy-B᠌W.:!Tq)YN7-VH/[91J#᷄3{+l?=1*4>AXuaD.rRv_lv~뤆m=]U@AuPEڏ[׮q^ ~L^+7@ܓd|۱KJZ<sLls=ְ/H2V<	rгHD㮅*T1dܿ8Lɞ^)LHƓ^)9-1{GI(sznuf化r@Kt(wʪz97*$K'e	z3;
<VKvɵM#CG¾M$7"'{o!uOnwzh_,ԬDmE	!)'@$2k)ˤkAVŮVF,(F`n(OGv,!?W+[2&g;@=bn!fD}.aoZDf?2ވXb$LrxumyAZE﫥,USU9P we?z9qXUzmOq)șE>xLB_xn*4ī
G ^NKTn7Q#+
S캌d6 :L0Zlxook"k"ޣ4&Npi0)b#k߸JF@0\q7.zb*.-@Pu^knHeDڜ't74ajE/2Nn6D{<9v0qڨd/OG`ES
#[	a,1	-a♗#Uy4;ѻq"2u'je</ǤJR06]l<~m4:{oQ<BÆ|H|k?t
1Ǡ]<T,<t5y ?u7/z/|ל]'eLyN
d%sAd9,,]V.onEFdÿ\+(wPβ.32Z=_rVL֢e/H_≀ش@lw@N$FTp!qxIK4ڳ]odY>3 "΂ FƼaCtކ&* }:xG,X*V
pP70FϨAAU| pnZxyifm%'|3iȀCCV!E$^玔>GlRV(n:m#4dIjIC	֕z巏]y7zR;$P@+ì/nY~2=
8E\V7DFЂ΄|gx?Kt,w=d	ߥ/޼ ,WGCRػW.ck|&Qk*```/v,ąk+.}2[t	N-*\xb7;?BaJKu|#٪cbzPc"qevfc2"L +%-{l|CS =߃1c0Z|J	"c?G,hL05<IGm)`4x_P~5#l,波7FՂ
|OӜa`w#YĿXoarʮ^&|5D$[$8y31 5yOD='V{2 E,X]/fP3PU~ Y[d6v J.nwIȕ	y5 (4MuEgAGkBO3l>}uduդv#EF=53__66%zI>Av/:̭rw$geUƁlxgw3;ڷJl_}gf:w ߊ׷lkw/f󓓳Y爱9phd0('l;3cl"z]FYsICxXvJ"7hsXc«Gw`Fy|"Q}Px\*U"UI6#I$:h0dҹض[IVY1=ʗz[XwjfZn?X=,k&5')l/-\.K|lW90y<X!WEqTY?K$<jQ0-kW90Letϛ'9M^"<ɏtSD8<Ⱥ`P[5#Bkc	"pQ6/"րĉm>T,*2#[M=!kb[gwoC
-[L<]m?)ea@pWćJި8h0?Z0HΏcic yi ǽ2"J-cRO:Td+Q/ޝ`dhWD	yԀ  !a\
%@~oE3(%+g
5dI핛Am(kD:fȱ=iL=oC36&{Pد"UkE
YNJil%/,$ٟp.7
iqE Hq!TEtֻ씞^`YB}}\|ދV\)0	L!JIeW=i˺񚃺ɗ>jMnɅލ)J^d`n)sz
ڍt,Ǖ5mEORɗSl:/DW&uu\uï<{dUV"WJ5;yfB0HQH2zf-;.*%W;OuI2	8mW<qB{$è-P|B8e7 .g;M|5ⵣ/*UU{=҉'AsO&~.6!y++m?O
OGJ2rWCfS蟜5Wph]5w6K,8>%6jAH}wy*rTSjhS7Tmfo){JyxIIvQVM}KOޱj|%4E':ŅY%- Tc%3ڼk*wv.Qpifxl7nx*!? 7"@߱}_!v2^4$U ׀i	:yYHe  Qy,۾xe%EEe/D7#j,AfCAy.1H3Vr熎(VfZ8˃"V/ܰdĊP9sح&%kWkPc__}Xz"0t؁E*&$(!_ɝi7=)Ci3BO5D`M]}1b@GHd8f݃!Ed*OJB1՗_b~B) )EYtZqAW2Ї}BgW6$-UZ9P=`Dcȧ_L_acx!a[P9wHѕގOc@DߴCxŇ ԉbՔ%fD6{]`@͎G=(.FEUN0"YAixS&:LmPsGN|Sv{H'zR>WJi1ߊ5rYɼc]Tw.x!lBEut<<`Xy{U	.9ǰ	OEem@d<;!6CO ob4?}y}01[ >f(فX]ԧ ?SZo4\pkRoDiy1ȵOʾW{RkS ɿ>Tx㝪+dչ8>rǪ Kѭ>AZ  ]$gFŚ.4+.=uƧ%AIXs;v6R/=Ƅ'hӋ#O;i;TC6s8Rҥhr0F@#!<Brª_;'~2Ey
	>2ChID&a̯bKSNn'QWqZQ2-R?4K}pگT렜RDgOVaRrjNKWܷ+ȯ1Ci
HGh3IR 5V|:\	u}mC <Kl;G3IʥsYⷧ]S#Cϋ1v/n^'}P)k2\mz_VOjxW"UrLI$zQaDS5}_J<@gv0l&
Dg.a;u^apL^
`Ǹ؊\d)s-įhz׳||د$mlE2񦨔+р3He!C7`4=M>2G  y(%U(NJ,rY=R&ϵ:1yk)O5Nv^>%+s%	~ )mݺ6	%ϓM|+Ǳ ᔡ}2#^ DgҙEr<ZhbtY/zԁn1RwGϫ(Q_h$l:9¦6aE!3/51ʗr"pjuNV÷ d[kH`0ӫ&5E~< "6esJ8'c,eIZ o2#^Eb;9 oSyed	"	tdvդ.hq<0l)]ӡ1Kݚ|5S'L VS(SRsh:<Nmzɲ.)"!ŭS_ltV,I`$ecPq-Qe*)KJsOH2ź]j('t3hbMP\TADK0g@pYN2ɪC]hx%:L'jg̱򿔦`(&ĤԁrSDbVMM_1QA`w; |0&>KGzĚ>_9jі m~EQ.Gk5z۶[CDF)Կ:@z]/Aiqm)0@J)M $-8]a{Y>Z3}0@bJ˨kx>[\sXMlc	ԬF*	E[/J=~$4}YTd	9dyQbuNLW~d"!yyT"!osC?֒@F9VtFyĲEѵI*Q2RʺF&
^5Pp5MlEu1yF?gS3:EN2)8zLߪ[=cJY-Ut>`})euA)UUz绛{:A@f𩝼IwD @C/9q^0(A[Lm`tAs'_5< JBIcghH+,uk:Dٷ%\
Y*G~EW)RXJ$M%tZFӢTkE]l2$=+̡:(9ͭ6!B]>K"/yv`UD0[P9GyDn1go2I4SdL)I=PwvhDW;牥@#0|J+<hFەLLpUPKJ%%PO:MG~]^·-4kn6zЛ=c@zAQ&'v9zK*37(?qkֵ
ut/5&* 9sҁ?v?dȃ.O 7sy"aM(jayWg}χr1wqQ=$˫Li"`-Is`i*K2hiLVAA]`vdy'eM"r8ƷU}#HAwGD91{h38gjDGMg #2:(%ɋ$ӌj4ƨr--fT>`Rzl۾[\*dIiF@ս2H,j8 Ya~/Oz,z{Ǣ4EOqYlB nǿ>zg{l$ޠXA.	6FkpCXy/WRaS9g##+C5,uwghdH@KeqFeo}/ߪZvs7iކH}D[g@k RNoN<qON_{W;52w?)Ş@ܹETLiOPɻWfNB}>J&
kKG_En >.(y47
RR<\FNkȓ:V60!}Đ%hgW*9'{ßǬUt[8 >~3&XEsL"PYRsHCziit,6}E`OEc/ ~SEi<l9{,DfiIx."_6Puucwo7K(4#`үCO[Ɨdn'th?o.
!?0G24p7}OGċ6#;s熰0uf؜F |:,5tV?4<c{8Mf}FW)x_ѐTBv#y 6-#{H2$UbXYT7e+1qm6G< ISr6GYXOU/D*iz 勏QxYQhuU#=W<fis
OocS!3cHyż
2V(Q_f>~V_1ڝCU;?9_x\n~
pӡ.5VB!oʆa_RfЫ1Fwld˨%"b}<!ׁX%Cǡjp hc`ժmȝ6.ki~\yޑztAGZ߮%?<jKE=\)'|~bkt~N+G
Ѷ/pʬ,Sw	lyT"3K]@Sʳ-#hb jxw#Y)c;0&n: ؿNlUkqIsDv(97ըS+^5YiϘ! Ib>ɪgjR,EcvkLi9R:Ů<ca(ܳnּJ$ML˭++$q):su&&o*
|O긇qZ":kAϭq/X^"0rKb|-KARlVMmT;~d˶lWIZ޻VA_1:߿c;41񶔧0ח4<EjY|!LU<I8g/+IE^F섏 
!:;r-DSvvfYQ%vе/oz?C$VgyS )_<?W.=|0@ K@^r^f7ޒ8i}i0yZ#YR>3^=X[YpL]
*!z}^/Bֽ,(O(޸_枚Js~\hԺsȅ=QO ͍	@
kB-̈́G*f''jT ߬E#9C"'LuaTzQ!j]3@3F7k,Rw#yZaeT]5Ҳnmv\#)˞XpϴHJHw,
qBJb~WKQg仴&d\v5Ss܆}$榃;ʨ =Nmv	xإK2"S24@U3=<sAG},.Ga QH9b߽N.G'܃F\sԷS\	7=yent`ߢnQHy.	̅<sIRl>1<'綨\
(nv"2XABS6{q%5f>+'(˖0Dl^񮺢)#X@*nYF5U͐z
k^bV.1f)'Y\k_eƌPN<<@"pIK
JL"x7C&΍3)@b --s25cNܧX,;*GmՕ	Ct`5XZP"loel ~?op痭B)c˰Ci͢ e3vt-˰XIn@z0bNn H$<\Hސ=̳0dHJ29+,c.9&f[ bˏhee&IB)o^r[+$$ƤDYyB֐2zzHɌJaR޲o.{[Psf<8vE^:jn<Y+a9#HU-݌(2Z3W5RLL7KxNt=t"[ Fxv&N[r2<=Hpj5~CP$HOxMJd1!LB|8["|;L
c#8Ca.XPg}	_~G?BJڇ?FY?b"!Y^@\-	g9ņxŀ(GzdXY|w"3c?qdxo`Jgnb2`VH
v%^[ia"Jcf=P8j$~щ?zOvh}M&lzsQqa*rٵ;EVu/$=^U ߀Zh$8{yӱ_۬[ZϤHUCc@v 	o2b`#g}+V毈ZQ7u8W 3:'VfRCfKPOy;ڛbr@r[cGŉRgTځc+\{- DK$׾`0kq[fB*P$Ёպ$QS]Z\?²ef3N(_bB1OHo}Jݵ,%+riNOi3yp|J2Ml\?0<lUܜ{&ktPkCG%6z.,@7!dymD]	V}*`*yL0ҴT04+C ɷil~c1j*VY1ҬE J>	+G(4=ƻ	02*iIujX-֖w26u}ȺGhk&vzg-6r NDyN|2䥜[?CUb9?d8V.̞(:iXm0qQREէ~ϖ:ffl+[|exC$h}~<0RMxͰ )t7/Q'xkn,ɕmsokǗS
7@ݘT3LuBWX}1xt+X.w>I){ZamD ݋A^#+PӟE[#ŘP*;_,'%8Х_e1~DJ /xw#'xx]q绋t9Ε2~FBSgɪ{+N^>,,buTIz]th@|2$L|8"[49&yDiʼ|{7ߦZ)op?899Vt׎ɴ)KD"n7LU._sm(+o6<ڙUP6dMXyJ$̹f}#`X!@WJT9ףJGKiTt0/!Ilg<^煻4×vm=v(<Ǘ%ԍRKU+#u!P^@Q p_m]i#=hk!
;lss>#.Vt;D t]e״o]WY>tY	]j~/mXZo7=R^@`SB$>'Ruve(HbA1t.AfmNbSk
v
2:t|U~q`1I@͠Pj[򖫤}Ă-$[A{\9E2\_jx[>vTy{=5d Cu౨I٘zHv"lfK[Cݞ\/sioڲij%߰MҥK;racM4^XG_i;f9d
.8EP#XI`SqLMfi}WqX
e@zrvkQ2ug{D^5"tG}o16ա[Er*}V6.")TԪ}`It7IJz{ަYW:^* oTB+ yji qքGC'mc9
ԭm-A>QoT!UBc9J¸&hihdEp~~$DVmA҇.y4+<7uD8@;8Pfyr\}*99y9YOQ&1I
]$`ǾfwQЕWzW*ϝze8vgkfmѮ֊~RBRf~^iVvP'X̥;_҆i]Ebe[.9KJ-P-\-}W9Ck	'bZqv[+\FjFjWI?~0Y;/K T&@K#Óf:Zh~kl[lOc>r[G<ML~,}ŗƣG-RQ$o<zNCAXZb.VdtKv)"/YyؙwTZdkGD_AknTJmƈΩ;=}|ȍܤJJ9٪O+!5c~f䁯Oq,Bzjښdbx97^bVA%aQoVg O<Pzmq;E?kgq-'5,(yc7ц9u	5B, iڞ"r|(TLgRI ͏D&ʗӋ(KuaJ<4(OHmȔlu)YF\Sc}I.|gd fys?2ڶmYH">hE\:y0%4"ED$"Q=`et({h^nxhmD$8mH&lFv!7mJWVo)wU!y_|C*0#p!eZN^	LG4U9#{8jhc]5Uv|{M
ZJK+#+LQ:ِ'86e\եmhF#+EypU$_jNybD"@2ơGe\Ωj?'ǢABjfuٕ4})Gy`#cX3od[= 0DPr\mS7voz7PxOOWe?XS4~iHH.~hzL)4&mQapXa<@VtUETP5ĴѡFks:a m7UNz^{H3ĔA:%`7Ec а2eOw&ҟ='-'4'#'`39`K *_n>Mh
:Jf')aM3?9EQ1@yX	$"%t?5AXqU]æ`Aݟi,?.ˑb7vC6	!8z\R񅦔/F'IM>/NI&ySk98"Wy!	(S˃w#~\iY ښk/k+7[[1H߰KlV`ۇǎ-DM:>0<=fВe%lmh45QjAjx[Eywp&|Kh#)gd;ى2r̓*Ãf~YgWǃ˟u=Ă)!*@)yhĴ\fk3% r;+έbD!Diʢ:,y;WP֠&Fx{Δx޳P"ޛ6@7NHhqJ٥2`řNh6~9ɮKbogھ]gܜ-W*%oym Z܀(= qV႑]gLȻT2yzRnn&f$4蜡ϑkXw1:|g.us1^zHU9126,[fϾd2WYke^:rbE6T7^"?ﾠ!ׂxCߝ{QixR4r"xJG(r=(PA"ÅS>?YkIũFnP|?=޴áz¨׳⇃gPh1$sm	
Q6ǅ{ަ9fwcJ{^#]̈́;
c^VeysJx+W`%6J`Q&hHu2Q>.[9;Kr1[@7	ib_悪ȧ(stvUn*2k%ğ==z/'< "=؊[o[&o97C/[Ӳ[
cHl֣t}`B cqTl8op[I}M8dpB֖
!&LBJoYL@\nC}gAbT{aBޤړ<XE`beE̩Tv84n	LsV诰u=ĶxgvFp{`~=qMajLTĖ+XY*)k'{^nH͕(^4oڨ !fdo{DnDXkaG9!B!=qOĔ$vuY'LoVxXΪd`SYa31@oV"NkRr:8;壏_>S~Nr=VwNz#BrT]B86>nJ<~<~ (jg#oz@nXr)a/9+%/1֖mD%9aSlGj<xm]i3h'ڞp$^@Laޢ'[^Jx Tn-w䨖p|ৌBy?tA <]^(E.<YF^m)Hm0b-Mǝ8PdL&L؄ԜYV7JlMԽ9Wb'{|iGQI;MK ݽHkpY2CYk3,)@[{J?Wn'ï2]l$ȡ:gD[`r{Ar͚ϰwY,g F
Xn5IKxT9g58"pe:(+$9&f#|RPc4z߄HWV<֩ID̊GzM/Jq+!yex(d%b޽WK9u-[;xp:Ql)MC#2抗b3*[$ZtA0'|$a<O92D>ܿ;{_~N0 %fxj:r}zBskxVSH>LP7M*=nMqYČ9Ð<+ভ 	5^ү^iĐt 3|3KPzh|ڨr	j)Ugct(cE]-"1\y+.sm=\SzIB&Mk
+~#7ǋ|ų̌qTnr\>z,EsA 0m QUqTlަW$QO|8x?Yڶ]*dtlɞxTYxa18"4cSU	Ĝa8KM1z*#uiWv]=V~r aoviWy&^Lv[Suqp鎌<9%4>b=̮J	I׹ %;v,멪z"^JЫa9CFED{-ZeZRf.CLWocQK[Q	ZSn_ӭ3ʿ`!q3I>^ڽ!ۻ~zlWY}4'8aF4ݦW)%+VB٤<(~l)tvvk Lu:tTz;X\l3~P9ҋO=1T5JܺlVdFg4Vi\N:ި|yGd,$=I	Oȼ\TDqF1G2ŞP0cƌnxbzgvhӚK[}H2J4*IJis/t1:β	Mz'C*֬p,&C.f3E5'7VraP?Kk켬+eU>l=^AlF&T07&xP)Nlczwɪ#YdɈ/?(aY}?PGoKnIΡVa#`9_UE!zYR(!OVt+JOd3	=d1XFM)גF:ɵq\-iɉ+1phZE'-ai5ۅ^_DņLన367SODT}`ư{Pۑ!NT<C1oAV6r*TqW;dUa[/j%pqq&MיPN\4lI@s
췟?ii ۝5InstYn!:SB.GVta:$$x(Ȁ>KSdǢ&~̭5rl-)Jej!@Vh.ݳ{PYHHA-ψԕ=H1h٧h9-0@N'>0e1ʺ%1-4:e+'Wc{Xy24rcdV&[Fѿl؍rҷG-x du7%W'%.<oM5u	-Ô[ߐG`
77\8:.6cbΝJ:#x`,PAʎt5f̤A*8?6fאY7e6a0v{k#vz ,ü+póVI"k	qV<7Bcɾ&[xLK~	R~b<NL@MNQ"ZDk@w#5+7J[_˚sJ|L	&,*q*au-L2K45٩kq1wjk_skMI:- ?Y)q,H.#E+ݹuGȰs
긜E>qY+<=[>(T93k\I1_a{J@uOشɧ L;?zM$NAL'v">B*TsR,>i;3ع ͛<dZ$ 4w@rU7R2_`xyZ^:'4w61{D+{mnRe^b+:-Zn߆՘wn`vOvpxS1㡀iyPQ$y~QK!BMG%UZJ:dSaÇp#gP{SF6i#s'"@,?ɉ.L"(>=!~ѥalA)ZMdQw Í-G'_fUH&ޯQ.ZCqcQX	Fu箋P(IM22+wbX
p~Le,*N{2L ҆Wo|:woj´w\,Xn-[KTy_%׶7Eڜ`v\ַV %V06 [G+@T{IfԢZu]@4o,\@$|+*p/X"I"	eҚg`|7g,p.	"%鑀`'zdJl]vtPI@aF3HV[4lA[wr'n:JAE>dLf>7w8X~7 nX I5;ՊIkv I^W`*7\Ň PӕWXj4	i%3H0$W$
>15w{;r4hF}cUۀ h:{,%Z>qE<F&e7΃h*6R|J[xTjlB'ICM U%
osG%U}6%&kڞI|ٗ)(ÇEA
jW*jݱR@ !f9n$쯡 }$>Q> 4؞8B0>)~s:w);"hB1"B& T }םQ'j5(Mcd́vN]G 2eUaBVo<*y)'S7!s5 F*H)D|_Rl ~@ϔ:&`k'ѹuXbˋ֕[%)g#ょ1gpk+%џctyGqٸ#sG -"Aѓ^zC8(8?9dOJ螦p&p_QS{Jj/E7PJ*"^zqZNw81w IܻN\WP=KvLp$OMJ6zQZ[HAe%:VmS_/duF3l5ppS@p~c"Efd;l0K˸N:"v^<}DZ-a#{:$1sE:b(Ʋ;6
[ۜq&I^6(tLSZOÄGJ$c$IT"ٯ'rR6@~JkC=c-7?\Q/X8z̏5GMOٚH}@wUsPHF3|@B/7jtZG$<1]Xo%b<N<Ȍz&66E͹X#ȷPHN_P.#3r ʖnh"Sm=-ڝw%O oKuG<P=H[o%^MU	+y{Le,pNu&sDhJW`!M2S-j80PѪq3Ƹ=K\#f|,kӻlv|$dqu \ vC:t{y} 1*Sk)P	[^tDgui=0~@xU+:e{5B%hq`Ux:~I5[}\ߠt!!B-njgo!@?sFdCZ?\adzww?	T4t'ev|91ڵ	gox`-I!k33{& zl.z~iy>|qWIJ]ѯz)'GݣL>5Z\B٤ɞKj`8V-ߺ z%~Kz0Yo-ph\FPom
ˣϜ;6P&`Uc-	v(;6?7p
o$9='ۖVT;F잕!
zBet!Ƥ4	컌{n3I NQn|C۶Nyj<9>XZI&fԑ7@*Ežiz;(AHi$ڴT,!\BtyT Jm@~bcZ؁VrNp#ˬhũBgcnwJzZOkGa?K\o|/C@_jBwKnZf-w~V؎ 18խe%e|	Z96(9׾n3E	ѥL>u	Zy3+j#`0'$ BEF 
j(Ir*Aɫ͜aWdqrTVLRݶdT<A^!rcœ%w˭(p2J b<v _MLZb&]CpKo-|=!Ů"oɝj؁qqE@u]vzBSZR@7On&zJ) Ay0Їni7s7Cxvrݺ9PQP T|E=BۏNJjΓZB5][b2E[$EϑͲBVJ6eVxVD޴Z%po.aF
(qmֻNС-rkWzq;BF%ޱ%Qӵ!i9(5c{.@u")5xbB`'ˤRu"5,S0.u^ojPcZ]Tp%-#Q!G}&3@6#yRZ"{{qC|[UНex&MasFo&7̌'W)wzOh=fSP~}u~ڿJ[c<K%V4"hAh_4QQ̀xcƿhx7Sig:_ f@ηJ6:.`&kMH_>S$_e@`v6.bUKsa}(̞UC"e<%,>.{"UjSyVWߒq16r\BF:clr0Q8tg[-|3K(\=Vk;j)Mmi6؟O1ȓ5Y
	7hkDg&#T9ne?&	RLlT<$^tA26J>/58&*<w#z5PNRհȈs1Ǣd!,SR8)b%\r4 012綶't[]d!k+GQuǗ
$G*)xjT{5NhfsW
Kv%R{| ʑ@s_/"L2QOG 2ؕ9|:&iP} Yv͖d>e^E2lQ1G4-e/!ApwiڐI,K6yC;|ZiqcuWS;Ox1s`L(&hgQ.guW-ګ&ɀx Jُs1Cv>uwK'w5kD)I,@zlf^b_Ђܭ~]%`Xޑ4O}}jָE:(U]peTcp3u99NA`E.Pus\I-L2KT'=KL%X'&(<;?j{*(.3ڗcշzr%&anOxS[1.`C v*HׂX]/VkſqU_0$-T{+Gx,z#I}d<2wb=^?r45_VY V,@^-BGj[Jԓs;R;E=Bd89cp_#rçDv4,įG]LJ"bOM.ܲ9LGxiiU.o<\|-b%tU36xD3f`ACk@kV9P`bקޅx%s5m ZKGf:zZj> oV<fZW3GiBD:3w*#nJ˟YM\mBn"geh+c럑6z"d#3T> HN0JN>4c/ oߤa7g	8k	]H+XGZPd%4L2Χ{6vB0,
eϮkGZ=SLeV/N` wsѣDRחj;h,( >\`55Y|F*\wQ(lF/B3Sj}@UFp?I'2H{Z{
s?<Kk+z+g-
{0N\98C̜SF][`=O;6'0jiI!D@y}3rb:@I[ai}otGWh"JaDI=C
qT2-813,XRGb .oUow+:͉āpZM7lſ|yJ.(KLpa|9T	(r^pbV,+`jiZX v	6~fS%|$m(h|U[f"oTY\2g&7jM3+R͸WD4zu~0E	ZLE-TsH0PJxGFĝۊ[˖ly݄^)sNl8T(V2e6RgUQM hq1siؚ!ݾk$emT@qM<[P^GY$1o1F\ﰊ8ݣZKI򈃘QƳwWLwQ..3[S%|K G	cQŏUc0M(0"QmҎ8s>dԊ-wM=O>+GAdm,Bb<P!ژn9Ԣ"p( GN@Ue_V
m=@@	.Iv*EcW9Q^ɔUrN9]c 2t!Ȏw
W1/u)o"J/Z(8nvAj\]ofAalܟ?e} *jb`~jo,Vh|p$l@.*x|f㗞
ɳ3U((鼕&%?1ɷpY#Pӟe!H0:OyԫTs_s(㒆t ?_XxeR/~׶cq{cj#xo\$ɱiot3QOÔ0mnۻUiW}ۮ Ksg :أT_5Ym]iA9_m,qX	kJF/ZٮIC%V73	>ް%p63Q4PWI+#\Qn;:i\]<kED*n+ߥl1bnx-$H)sO򊎓S0Y"=rv8!si>iN_g7!l
FNoJ}?=܂~A-{귀ltS.(p LvUl'=8}d H?ٱ!Cpg9ޮA8ҭͩ]
[ a/XGiU}߭̸:̩20vN	!3ж$#s7d;_-ъbˋ6nҺ&0V}S+oh6"k5,+`:,xkfNd%ԂTΑ+XfG3-wz"TbJц.c2& ju5m?6oDe㖚¨k|GXM}Z8wD@	Q{&րcpC񆣡&j }7CŒ><]/Кq	JC^lvfX@a HLч1Y=]|9uDz3FRR-Y?$+'g]Oe)EV Lh7xz}p$6dmÓ~ҽo+E9vٱ}=PioЃM@QeqV_:OuQ*w\d=8	nQ}fvMcQT$<a׀WEISpwØo?2]	\+jPo_\nu⭆^_OP6\? dtC(E ie,0h,y:4sg.6]{\>,iQg 4{H5VMjUƽ1j 2L,ɧ-WP;2ȏ ?jov7Q~@V5q&?^Y]iyv;l}GpŊ@<Lk%aYsmI<@Z2gG$U&~CN7c:v-/ȏ'֦ wi:3/3¤}o9sꝅ;iX{l-|yfkUSI+ J vob<kkɥ>oWkb'C=.9 %u7d{qF=YotROz߯46:Ľ]-{w^Ѐ#?<rRP{ՕB0І=2b0.Hq8wv ԉYM>+qy&OW7؎U;6C4$u愻/Ր@@uJS(i۠'\%Ѣ700:6 *a/=0Kw~}_"ENphw~XSeZ4\'>!B֞
=ꋫؽ~hycj*Q[,dYLtKQi>5	.~稠K]-*x}aFO@-y"ӎG:82`&%WxS`1{tP+P'n
I#hw`1\59|^asVæPSb" w蛵1@rq⏣[uwJ
W t:QN^(|	C#ԲnvWG<.RtnVm7= .ѕ)+2_C<G	ZsHE
tbT*N|tZ2\q@Y_`?b
hèXG|U.owAL	5ffCIqxZgEsiN$PSʘ[
 *ˉzەJh;6ิiX2T2`uEz,H_>p^3]w9е[ tL>ñ*-˨Xw&?v-9jfInk?@x Q'nrd_ƃ:PV9	2\{9
@4wE/UtWj/C[zocl	,CgCWƶ[,u>>+7`z']A,0&=+=J3¶xtQ,>9䮚!l	l>=~!o[ӝà4"/}qa$6'؉楰	yGpQp-ᰶ. bw|2m![,
`c
Dk;A7V1H&+ٖ{o1B83	# ev[.Ӭ@)ph7qgkF7$ǘme>z敷 45><\J0)v'd
ֲR1W	\x3MMhR.i9$cp+!	;OWƞ=_?h3!*T:3$}X3ߘK@z)ojAZ>wDrq&DݶXrIwzH~Z
b7^c ;Rp2تѬYgr /G5/!H7nׯ;)RqVe1\ȼmy{cR 'm>:z[E<eatnNj*'˙?
smX-?&30!;.S}xpĖ&z-)-~IH$PaS>оoS36zcBMu\>LԖpœ5|)e*Nmpe$9$x7PN#GU"f{3LŕZ<bG]$1pXgiDq;v=ԟ:uO`awu[v5h4ߞEӮ[[6kթs4IrIYmK
38=hjA(!};dX?;m"ۣip0Q@hdEwi*~>;S^ZUSwӜ՘vtL!14^ZŐzqf7#z"*Eg.MGUIƢט/^d3Ȭ[e7ڙ-_WbE}_>Ţ1vZmo)?co}٤~Kv[XzM ;I0#1?7=k-Rt$Gz}: {DҨE׭ִb,bm]~?aj jr[Gv'YqUNx8${ܨYi<S>D4ԣ.W!']d6`jijuOKlX;60|yra>qqq6YCHaSV(' 9/&DM:ԧ5F**0wM5>JW4lfݓaBmaQ,񟠾`r&p{s!3Ң:9$}'aDrY~if`#@u&X?c,(sa#+V@5?C%-&S}^61ۤjH5U)+"D
hjJg,J=	)`0#G[29sXto'Vi۽2-GЩ{lR~S]ME(LXm*4eZe}h[AgKˤA8xOky[yz#Fps&>uoD]j_[ZWw]߰hq}JއvEs*Jvtw~/D 	p|h)_O&iC4%kػs[.1T{1iVTd!/3iyl'?wCy|R&2zO'2A#ц1E*5,6wX%TQjvA﷮CMuۏPErXPEb5<Wu?{<yuOdҝľjlxCVniF#*nZ=;;.LS}GӦq;:fBQ~6JniĘ/K%c((@H/Gj/Ԗ:fҁro9s?fs4mgL2ϨNm8e)'u[C	,%~3|}&l @T"RL_Ib#[2b+
t
@|\>4tʅ	84jLk6+wF%>)!s _b&iV3Q2N6=~a"FlSC<MH$N91Xsc.Y%U;ԏ1-+;9g1&y~<y
b.T;m/my$4	2utWsIW#cBvڠJkNSjNd9AKg'Ճ\# ^W}5
kC%)<fwFlW+WÑhp3xUJ?>5]e}6tQ5CeYi#%O7,.:'6,%Ճ9z W&yIt2H]k#}=KRp~mi˩fa50C s :F8KyRˁUIi19qYZ@$dhHxU?5B$P8
ӳoM,c|2˃&GgO^h~%iAR6pY2aPZ,Ćo%Dpxy[j0)wʔ	(6(( &uqC	;|)&m@\kZTIsVKLfT]uD9|l;qKFrG>MF:6m}	.ߣg/عR4-X~ϛtCz|,)эcB{cRK#r۴
+޺ED.q14ό11%#W+V2	'dI>90ᮾZtTC#싎NJpͯ\+A'
U@s陫@ˈ1pw3o2;GySgKkQз!u+2{9G/އa^=x:0"L 2K0HZ6U~=#2sC2(ѠibYU<thq&E#4=VoV~=bX]tٯ*GI=lZ1eҧGK4Ȋ{4H;̢*eդ=¼{W"\EmBBLf'@.]LWY\.4ǉ%څ;k7bP4W?6Գ۟2hIB?	`4:hLcze$2 x%XQ̎a*EtKb|DK>i@w}AN$%YF]J4`º4Ɠ\+3QLČD>i͗9^FH*hA ix̞3|rxp= ۬w 5h
61Z',^s.C{+C6nV<*o@bzT'cv!(ZjTP}&_2	D^k$G^X OTW  iz`yamAx\
raC})%MSnFo2%v-/y#RH9w+4NzgbQg"ّ֜œ1ɈS!f a~j;)ܕfMzIc*C@}8}v
қX}#?${G/˃	zZLQn- itvH~/UYQpA<<,*k6`1o;4\bVj̀X,}«pgшۥ3JRS@9 j;~<5i.Q1/C
WBPdo #8JUvN6KĴWB0EɏCw
x\D	fր}C/;KNf0dPClXBƈE>
EU]	¥zD;6a	*Uh3wS|HpR?4םo<HZNY!Fީ^F#?h.etѿJ~Ω}xg1cVl[c+xtԇլފ1-lŝe8~b\FU`8Ԣ7}@%@1NG*.?MGڦCN3Z=2b5jV;]OSݫ8ɃQň@@NF7y<><uMTx$b^ᨁ?i+oO3V+3b35}@ݤ:/vņxSp%7-"mK+MҖAGՖ;fao}Opv`A}oJL= Z^`[XNap%2(װSv$"à3%AC+0Dps hN}%b+c$f=J+PC^N]vh~^3bFX~~;nD2N7ne|\-㥤B##f~Z @}!>,w'}[8Nc&H:G`X)^>19,@"z@-P41jyO		9zaϼU/EE
wQvTTp+ޝ8bnEyxnOq}`sqZ8/!3aj'0gśC-="Z+63n֧lpZceɩ7Ft{uVT}v2XޝWQy/Imv)}A3BݑR>NmXTǍ0\>k3KMl>!m?ƍ㧚l?ICM4Qz}͡G$t2F=61SOP[bo<]cdFfz?3P*Ljh8nȪ5y#OqČq䠁rD/	UԉvqjKb%~r-ՏZ=p/h?ă~RGnV?֪}*Qdf'wTezBcL𓅷3r=rFBn%% DAża
$APX8Vl?pu[6oΰ616iCS6~޺@^Mڢw)6	?;mDoʳ&G9-GE4i	:Aڗr&;+~5Q Ykpc2#ӢNdJԊ	w75
+9yXu*|tW6i=*SfpgD셝lGkn`_
}L.&yQA=w}OhFf2_ٰa95D^ `a'2A NPŰoE|f/s2Tܻ,IdOWept=ԙk3LiPj/ԩeo̜7*t]3Xx5.d%g>kBǑ,i`$*	bCՑtYm宖Q%V	;q3 	]	MHʇt]7*Էc-{.[ ɕkh*Y:Դ3]Hf&Sy}Jnt'=бHȻs-ޭ6/++EPk-Q/	j%7u]ZcNKv]G|Zn*?&Imu]l5uM$o'>,6Wܦ
B}؇x:1'KS XCŠ(_3N]V4u[*QB{ӗ>	 4
/'BO\%}5\zEv#Х+>EzhLvկȤN?,GP\*P{a's˔ڤ_4Ր3Q𡑢o؃m)y0LK|Ƭ^Hkf΃t)a-`B&Ӱ:0,g&.^ֱ!CpmV]4132dݦn Ag]Թ꫰6e2CɉS1Q974h-y&]LAvc~59+C[Oث^6hQ;6Ey\]\"'?06&FJpuO)iSLKENTEqtZdT[q&joS#W&SPN)!]P25"F-n@{@0Ǩ])G<naqEm#/	<^C+lYLKA]A+̤Kc񐇶"i3kLv{==u~<ԿmYV>X
C>ޙT$+=HxT<&~X]usU`9t}bĈ<bՑg?Xq4ćohsp@u&W'r{ydʙkqyDZ)$!vގҵ+aOcey\Tueulƍ;0[xYn`</`¹9}zȲ lL#Z܄:N钒[+ ^W0Tm)	QWW6͐1<
p307+#z֞)3rvasteg~ʫqXBoao.3%b}G?d5wIsxm#
m}Z
$ cED2EetI̍cr  Pkafy	_7]8?mcpZ \Xw]4mDbV9
$E18}a ;B< ̰*@zWe!<QN1{j_/~C4dlqSdo*1Dz3;p.z֙MC7f	FX\3״o54x;uhՔ|^bݏxPDHZ7*oO#WznHqԣHP"ouB~ݽ0}	kac@zF@ɬ$Kcu|dfy2l1aoTishZ6;[z4lw#bϹ+e7a1؍>`Oe
^hqlKHquBpGWn_i8[m?ZecB]熌ǈB["uk1~JKP7wm`u3CISjׂ:nFDMlT`bzJYdn!Q: ?hle-@~U5e*	tu1JWzm)ė5>i!VLXtj͆Ҋz
/R,&4
)$Z	є{~{:T33cIGh¡$ʨxt5j;*#tJ֡<?f~7>Ys~)!Տ9d)U$?Jr~Wma`q[߱F mdW6ҫsWvypd P0co~~yᮄ7YYDCĞƇ5q1	Z/\/+t彡]$K:kW&k|`*w-_W5,/kn]:k˒ݙPMے?p\Qqg)g9*(bè;RDFȫ#YNt+{'ݽҩᗾ5+^=vOyP4w|oEG"q\b0Ȏ ߺ{>+=u3@Q+jKblXjps9`LI{.سu<NMvK
0)܃[ڥR<C|ԉt=]LOz);fa5$eF;|J~%3z߿ o:5Sdە1[+-K?b)$_\k l_VE(
 ۫  7"!W#dCYٌ,>8ʇ@XdoE=]""2!`_z x-5+ㄜac`Rw׍AS"{*YZ{8f4LKk/^1s)|cc6ˢnrb	y-ls6a='	YK!-M$5loLCz"%*Oc$3%Ru{ʹJDT%~_ڔ:"^ͱo(efJLO4g^ɥ"Lqh)z-K1LilFyO,T1SؤJcq>S~<rKl{u Jwǁmy%NPl
6}@n5 I.wFW{_qTҸ^<cSq{o3NW΀_
B=umW\oB)WL~K_w+LN9qW'ryZSκiDo)Wؽ`^yPAE~%l#I8#iXOtJSSRJ(BfEy[+]&seә+]bƍ@
ui%eqb*i൧}p*W]ِͧ&p FwƵ7 
uWI߅GEG1
%QX.:J"q\G)4;]J<ec&6mYe`pyGW6PAlAxbOَZ\mlףC9zC2ɹ=S<yu6;ca$Ԧɚ] Ibϩ,=Q`NB{w/or	nLT0cfD pmvD!~^K޺jq-kԱV$a0~,Ip&#wEQ#*C
cPW`hyN9Gnh,bc)#=1ʭ{QxCrurE]WN] AːW@ b' v*0>_ܡ1yq}#y&?s	ЏmsķA$qiiޱQf(*yZ[҇h#:2gXsud\}o3nT,1E<|{7֠{t;`Vycs1mQV?Wer:1	4rgrW܅)n%lTIЗμphp<VRp2N=jo^bO-bnƃ:t"OȮJ8$fߘ;*2 1^;h04d
)MP6f,d0JK=ҍ+MJCeÄ0#6@?}#6BO

GIy%Aa'ngZvWgpkP4dң&
rwʩp?H?m|X?oqyZ(&P<$Jj臆K9~-g.?	[/	ʑGZ8yXs.gv82:kۊJ
Sۣ.&zV Wԥ+7ӆ)icz̰SLd,60JLkȰ;!58%>ἧ`\vģWk:ͺ?WLo+,`dh|48-[dgE鏲X;dOzg,@Jc^,dmñqŅ&vp+f?r>moy
4[\">	McKGK.LF̡?F
"m2[󦩐][YUϠX068uy![	- .1.ypG@̻kqw]NjYZN!6'u1]mvo93Cɿv*9ϣ	BsNDde+TT5_"_~P$pCj P<eF4Fǎ59+!=
C%L0O_:ͽ-~znL\VV'?y )ݝ
7!|W9aSMN%k1h.-R2RŎ	/Ecj%":
r#	M
:@x9Y.EԕHڒ/y"R;
Jdr^*64΂=>e˃{ɋfdS\?ꚢdƋ6fn%zZ麞ӮQv?]&A^ewnv|X,?8Gϑ@Y}H|SGW~`jlT}\x3+tjg(11-ktd?zQ,h?
H<~<Eqr1ZVqsGI2a?}c;Wb
[/6E:.OX̗=;ItSWNQ'
gPc<\	P։ `Ze$
dR;CG0vt;|ߧm^̎9\Sؑl~zDGDH9m{z*rc ,7Og2ca+3%B
qijgOn{ɷ> ǤՏ0+PU"ǳTQ6s"~b5t7PԫBҲs̭[\/AnqIE^3[:o9t~a="fA>iӱK=7->Qʐ[.6J,oy>$J.o*:A Es2}РG	&rVԼƑ]]MDs/ҍߟpM2m5άXN,(>~SLg2ޞA%rFftmRG?O^M[O6{&/mzn؛s(A6\xX]c%=FhvtU뤭tT7{(МhHD#\Xޱ+7ʃbPl}vu6E2+jnLx@'e&lh9{l`kb&.Y߈s&"#pgB徦)yѯw!I
<׈x3Lֵ8&>6
FentC͌Zat"uIlVkɜDniV{YB6ҎskwdK\wڼF>[Ly8m߻jma2Av__u'6E{Pk]ramߎkUZ{h7Go
h~]Y%neq0$`}
|`̀)>М`Ԫ>01dE >QC>9Tyn@D_mym5o^tZ5~[֔m)^fuFZ6ݞ @`2>qGa
[#k\m}Aۻ`c ODG
;j!6ǫviǹTViNS:.淯k6]a&iN5zŗFL>[Ub(t[n3`9=N|qEa%mU'ęq(CiG'u
mKt𿬜wIɘ$eQm?exHuU{$#iTc]եqc>;bpM
bD./=uIU$ˀ,bٜTmcI? QcsIQGyC`µPFWm~Wִ&9w6L<g ĄO@|rp!NgA_x BZCZٙKύF^9.(գ!V!&#ҟ8յ=ĄiyQb3}pT~)ƒLJnV219BJlYETS~
AW/%z%boB"= NõށeG!D|H	e7<}5A%({[)xʤ9G3eRR䏑LXJZs`gǮScAbcPaM_wYi]$ zJ.([Z@Pq7mj,b_1Ch/P:@-A  vć"><Ó_S64i,2ƾw%|W9rwݙCkJA+{cA<j7жXqXԵ|7w_1dGx+~M'eHp
]炝 &6jMx\
0_8'@Zf7Pl<(>M}ݴﾮ#v?mD=,_f-z9Ade&Ma%{*!r+Yq-Qv4ɫ&f?^ݘhS\x+XI=vsR^km4/oБii)Be3lZ.}!Sa4 Nw8ʘ(]l1 3ף(~'c,O\l+pRFU@¥ڡc8Wχ/a\wތ0gRJJ#Qm5H[#Na]MY4ϓÄ+PP-QElB`Q^)2l"@O]Gk6hG[yn	.EKqB]8U39+aL}|#knẴ!}e')8ű_q=-4h;>U2=vD[b ݽ'Y9s?|]c2A?x{n^Oa4/R Vl:<o)jes`Z.?۹UBVAR}G_ggA6!`@ݛ/aNpnTM>MmnmR2뇶gr.#){4 /b(G|҄:$d:k{S*R%)H<fMFj	0v$8+"3dtcu3, _bPW-,ӯ)CیܥA-sj@Z$"Q@ahoXsӂ5[DJA &#&8j
31y5PQ6y#[sxӱ&R*xD,

b8mPg]u2f&ح2]3W-h87ܦُ6Y2D	Fl0LJl&ԉ.`fw\mv)Ċ_N]BEh.HW|f0='xR
K}"sl*Ud>[V*7N.hlI@h`yWD;ߗrwel?q2FF [ p~Ex3mp 5i[Dā3s|@0U}ڎFҭ$X \r9*'J|u 3]_ \9Iby]&]ԟ	B0Fv7kd!ނv_$Jf]uEmy%2$BQT.L h;2vBiLm2èsR 
ѴoMfQ0nԷC!}@,`!A5Js=˸4.+67BѼ$Y,JMJ4u(vqgo(+"(_cI\*~{LMp_oѠd2Z`8(h,M~2D;ES򖇿
.׀?jD=&݆ЛLOaD咣k߰ΊZ[hrBwpVupo3iQow\O:8Ƀm^]`P	XL'!3f܌:0316Lyڝ6k*kv)s='][yW\l)>r-ԯkS9T0w8/mm_!,߈2zO$l |WB;g;@X:,ږm,f _'lT(y.'	}ǒm9kΑ-Bi<ӗ(}Wo}DuE~"8+ >T14GL<.|ӽ-Sx@N)բ}f*a=XE$P8@DCuU8	kf; vJR'J[nǶ hJ/P\PY3/-pwd2_g'k6W`>xKEo"
oSG	Ԟr^Zʲ -`"k`ľx#y1ҵ*rhKrW`y98=vd!`&:r	ck{*cY\K(lzR)Uw_+P7}KaK\Hι$OV-!ǅW7f7Q	}y*@IdVl2/m`^U+@AAXh{D\3	*@c(FMrw&X'pǼe!+	7I6[~S:/hgθ`>ΨWm\Rj&v0B}ۛ Df/Vfl"$
3:{F`k>@3rr<{:k+sGB= <b1?Y`mApJ\sBLwwjFK!sʩPVI~ŕlqkoEwBY8C+rH$[͕nb"SǓHScںE.S8ܻsw!1pvZyi3}H"	wu
JPQhJ"0T6-Q'eittO+>3ʃ.V]~%1J5y\)
o2% 4SWphip\>ӽ(`FZ$LB뤤ҋQ"$ҶHn}3+=X6'wǇ-͜+*q	1[aU7CAtIfSLME1۪5etB:Vt}jB:3qK'sxFd֝M[uY6E3w|Mn<K!#SXqcyʍB'
2K(YT$K
ʰÚLt3VjsGƬ(Ae;n{X/p dS['ǓNor"Bsᙾ{=$]?LXRbRb^"#(SL&wՆRXԔ}	M8ℒޖ]r6"EG&$3D!ChW+4zS
FQL(Z|ՊkȊMvanK[yʏ
L\7{ZdX@F3Op2J (f GY=sٽ7xP25@uP"V]fktgf*grs8D'SD7	XݣR>Qk0`"ΊcNP~cLQM!Tga]X-	۝ے5ح[6&|XTDo$\g#_=/^0L\R>~+')0P*u~eGs [UY0t2ʎ4ijBnPtM	/yfY>MP*dg`NpT1gHY#8!Nh%)m)*91>'gGu7<guh=QQ-g"~)S.G>8-W؏mUycRg07c+m3X>f,6(bn=:?Iʢt*G
ū,g@zv^e23lԻ	=$ZHg9IdHծ/HO8F̐EMGGyS<q_T 0^K!~Yc<V,YB2!_a **s.i`t $.3y6&Q[u8T\<A"/奙O֞?3Iƶ_$BS߳OćLT+םc*ttr"sY)X7ķynVo@7~Z'*׾ rUy.ì*6)&Ʊ2,v\: =!4^Ft3MFҕ;KI&41<T!.lPlE3g)D^^˘s$/Q}gXM~fS|օAufi؂w-G4j1$gm!Bhw@ p*$ēwPD+b*_EzM1R!L0
bDk~qUtq!oQ|q[[f\Zj\FY0819Gi55SJ12`S-CLmhSoe3,e\r`{nֺ*+dSmmaSSĝw: M?!^ %9CR𪂼\빬/*PhGJȁcfO$tt9B3l!lxWTQsruFHfe%sH=dĒgnD&inHbx'\ZPȅiB$:P;S҉fN Sh[1x͍CޘIk,mw|4VEL"}}5YA3&rBg7EsK~&5zC,a\LQi`V>:IUOGTno&۟ǮʕqY>ھ<G>߼ׯ;4WR%wY~/}Oe'[J';U
EBI"(2}2K3[5ofyMK70xx[nգUq-uϫnAS1Fa\"n0җqq˝)FI gFbj)GW:r.E 	@+> 	)>}F_2ؑqx|(m&vSYaL}kҔS;gC5qSxC#,.X~ĥZ^ޛsI"oWm1@`^oyk:ѳ*"Np[pGhΠj+Q[7bƏ%abE+qy{XlAUuƼ%
>bL+GU7I^cK.
	D0t{2m_4	dx	|!}4`V4t	iCX #=14;r_!uO"y.2\a/.KE'7Hܜ$o#}oy⭺pE nRK:U0<~ynJʹ!΅M.5_"חzkL.Ŧ;߃?M՛MW%xxp!uo>{F$nV; 4%|I1g8i݌_ڒ1Y7~-{a	OdCO=.xg+Nuk1Ly<+)3n`&VޠM o_QsJSWn&l Cth6U5d{H_`X
Q2?ӞR5.ajUFnva%?mrdq\Ի>/բЃKxkѭ芵mGUǔFB)VE=iXGQEBy O[y:oZΜq*'8jڎhn4؉^!GG04L]r4Zn@ߧǆ ȋeS"-|Cs )1A\>Z
k.x:\B@G2>*[.3NtKQ7\kFEV ivD-P]S7G_"Ƶ<JeuȀww'Ai2(d	gv%:dD#]_	%mi>5QFyvԢ!Uj jPQ4`
ppEJ[! 6Y]}[[j8Ѭ4Ӊ
(^S9MKdr9̚A4еu}@1TOvqTƫJy;v=c^0n=ӣ0[kx>Dev!zyM7茝ݨ|.B%uo{߽[W>u|x>R..#Z6~u4 a}fA>wececUP J="dK0Cawv#<4:ge^oAuϔIJ]7Н8tz@z^wǝE:Id}z8bp[rrMr<-}M#|9ӽcڈRF 턍u">+gs[}&_/TkH\ZI#,R}A0D}u|;}%4E_[UkwU9)`'8H)VqeP13P qrTvE߸IG_)=m5yd2
Ia.9f6#tW䁝h~Q=vZG?=&Pe)*D<@kzNǤ[6/CF@>oJ4Gdd7?k/[{˄|Ś n蕳_*g0i6gf˂lUC[ǃZ?dfLZ"Ds!ƌa\[lA^3B5.gԹ^*8Ws`u14rb!tGwzoX2.CG?0W,/*X7vᶥ<`π c+zW)+غ_E
 lH]JC@FcM@K^>H>N[uD41@Ol\#uWgyU5ʤ( 7Kx­7sFEzzn謻#ƀu*&1WEapCQNdChLsTf?f.;y#ޔIH[giga$5#ݧ]s8]it؉2ο!
:aLF>.l]WDͤ3x+^y*Zŵ?(3Z`Bp8le6iXFk]gQT	CSLGl3ܛA72'x	6bηwU:~sdM$+4B曳R~/]lP[^-ӑ7$DUWDZv+H7gh  D^^y4Gy8"2	IԬWt*Ma:My~oYpgڂ*t'me뭃7ǴR=eHm#	(=VԨ&#sGN:a!kjv	TJ5j#n3::Z4[$9d@zq+:PYe#겠<;8mLr_P0'CT1dBlZ0/u}ʴP;#N;9LrL}:jɞhX]ŽD}'Gn^x2T@utS5$wX_/!K{;yՖ'^$=$n&yHHwٓ[skL2WW`")(<<mO<.=]a/Kd/a5 X9}n_exʗMUknW#vUR.^6_-	
+(e4i9ȰCZAA/G ArƥAaM/E &R3J[>#oDߞM;	>vDgoKo`Q}dKC*'IƊW@a08THz2BˤmkthݿbĻv%ޥ^^%!M0Q@%V3-a9O5',r-ȧ`
j"dp |V+vLy\=$WM{]~W1L$A~:GS=n=t˟;95t_yF68CVh;#>CzH͗5=!((E[.!!&q,S+
6#vHm$]9'X;r3Pp)CyU6b;&$Vs/Yl̈́qvEn-8YYYI<0C$o_NMm_yk8Ƞa?+sNfCwwRhiPy6 \YR$ʵ1Q2[dɹ%U]zQ"n*XOEasKL|~.{;udâ<z^ؘ߿.rmy\la$D*cMg+6}wDCݬFÊڭ7IiJ6OVHl"z	w>k=%ѻK,~9+U&P&72V\dھɯ{nMEB<mKI&edlJF~cpMD.*3%Nܳ%c	QA]k^Ņ71+J+=NC	V%PwJoEԹyCsrؑLls3VdcD
L1*rL3v@%0a؋$tpl"<kCEPhsAxSЅqzQY Yq;SjtM6DSݤ̯>Pm.n]褱Ql6xf8ڻF6<_decR:{q|vKiiZBɤT,yR7V+p݌2QAz~B3ͅ_mɐMk4oAP}MqU4Ch!*eLSH|{oMmh9,Bq
Jx%ht yK;;ߺ
0fV56݊"!MR
-#A%Oq՞7@0RyW=Q-Pw??+7I-9R*q!R؋!uW{W˷Mv'0~0o#f	FW#V:cpZiۃnn%[kCPu5X>O݁e;/shݲ۶,ǳBW豋Y#Ďeƃ>v<!o*`_D<93L?(RT5teopu
#hrlf:G8Cn8iwDGٖ-lG;[GiE/ַ\Sm+7JE#l:#񣅇ck`5ܗ.n;0;9;yFOٚ_x!lcםM3ZEݛN]-E?W\ϡe x·.,l*i !1(cZ0)0~ؽ?
;4+^yg-<gU-{^鲑*^Ѐ{ǒSAϟg3C-hĿ gl(J)2}t	1O;΂Sw%kkJb,"QK:%n4w+Y gDNë׈mz~Q-J''7eF?RI no.L2߫G%|wUMN\ws@װGƫ'u@e?B:p9"+Vah;sr.k#!MIlfy[|e|fdkͪ'I^q0Jmԓ$C%\^2Ctf;1sѿbqi#l{=&@;(	@½WlU"ɔhM%=ܛbݨ	$/T-p֘KڔPa5w[%; ;ZMVU@Lt)Qۥ?vB/B~X7΂Vc'3-x?w		4}=t.EvtV<gy|aYs#~C?$l`;~֜Ui9o"<cm8u4ڸw_$au{R{G34֎~߮jkgG8LW0":yx@pnj<ryZ[|4TnLv-fY*b#S'ٟ!Z-Oe8rSt0	=8@(0_8Rxu|l!,Nމ&)`Є(~9H_O_Ҋ^FnPYb;nXةsr?CoX
7)8iGCT<Xio@@qZJ/-Fgp̸؞bYٶ ??j>ԪT}UPqMTi*BlJ!7EΖrF)|P[KKfW4]EpUZqesVrs-<4EߡXlcɛpTNfs9-fQGyaKP!5>В٘f6&[5}Z^.4Rtݗ7yy![cr?hڢN8Endf<
u^/gc>a&HF^D`G[6|:#>C®`NWqN`ru40ᩤ	r[8%R)]@3ek;ce`~ 6oW"ܳJQVf{ \hEΟyVVG!U=ˊԂmgOiǊud?As{cIw>)lb3MFz!<FB3Ed>P}-D/aŹ`
1s/D^'X'!G	pYw\]1ߓvbgjݣ\:jjL"5[mv!+6XBe«`}$18)=@]<)h>rQ_Ooz**z{r8`C]Me.O_Q0A;2$|j҆XjG]NZ=n:A%U%,Ų.a/ f(ңE
٫wڦ?0~UwbzOVD-qo֚%pb$9ji 90"DFc!MR7SsGz'Pg+H(H!c,	Hq27^&5Wбi~#ݏՒllnF\Bh&7<*V3t>Ѡ۪f6<A#S&!~B&x~q}/r Rzd2t>E	ozcyuk]=Hkj?H΄J=\z"vL*%g3=ihׇF2F9;k<8TceNhJv;_WFXqP	wx.:7kF.|_^dE"/rZ'^-_Hpp%֥4/n~@NM-\g`H>
fv1qoH{D-We(3 є{!:1 S|*#N$O%[<Jٲ@h("ư=pu[Xs̸\;Ex#3⽆TciD?~hiBo:Q8c1B2|q9--0RXM*a9;TIܭz!"{ S>E"l`ri^cXYD1gi-[foz|G˄""2	SE[+}U7b8=S4"񲄚EMH~oeqՆN-Dr>cոs4MSHSL\I-	62D":XBNoE:tU֨~&Xv\s%Ɗsown*0Ut`&mg &Zn*w3HkSDU
˾qxWw	N;X$2JcX#CBE/1ql Q:ȧwen0o9Ÿ#4F8C݂SW<@;&f4D=8Ck/*N/2τ5(۟6_p~6Sp-y#=8PV_d9!|p6I|FO񁙚.f޹YEB% \
W8Z(W)\}R$+Ku8b=,	LP1tYb 0yϘ:KCM_YKn*jC'pdNcT)S%04Hb;Mu% :ͩH]1fF@JyZ~l~\dU-6R'h$#YJY 򆨊n܀ll`x$3;Fhl,EKu+ƈ+ZGR1x,s0
uj/T!pmV0vǷ W`d=@ !ܵ7ǧ\!fppRH.24_r1l_W:	@+vGm>d=34	T\Kxq
%K3ڣ&DFL_Co;9@d\0EwrЇd4ԙ3ѐ=)!rx }ogX=5э)PeLk+;ύF^cd9KyLڦdco678qLENĵJohzu@TOjQa	mXO/(׿ԍ?1{-
- <1ժp;v g;5oB#_ƪ(|ˤg4"0bHhDey͑+cLM,C6|C-+pLvlDf㰎У
oRw>$pFټCtQX[	. B-,rr^0//{t)zgLϪ*.;V0qòrތ<QūB>MBAnd=ULju1U	X&^iZcueďeJ1lgI:&t뜂t8sR;\}[/b,&Ug'Jqۚ׎GȶFUƵ 7T!_fwNBԯe {O{v0Uv]VQ{99	k8Nť/򿍰H2iD]RFd!o2WչN_WtG	$`EtKY6D$)5bJ{SoVyM{BVT?$yhA5:+U&[HO	԰ݵ3NY
egnT26e$~bvB'"K1Ǿ=l-ӵqM4`99,Ex!*T\+	yH7,`7ώU=u`ɈZV7?Q,uFKKZY!}&`S#DQFjlPso]iuJzst
²ha4rSLFZC*dȇuy8][PvLJ:˷g~B!ML*p"$\%DÅ*4h)b($,V}%vF틵]K򜆨/*qRzck0ä[d-PK s8Nr=J*ls m~f×$9L_O,O
oY`S͐#8ImKPp;$pNf v|yV\t:JF'+5tloEc?ƘF5 byiyJ]jӂk炪7˧afp6'ऀ(!M@紈R%o#A&N~Dn&<'!<g+&tQgi䥋>n\nYY JϜ9Y6q+/v\F)>CQ>P)^Eܭb6Bҍ!P)uyW^r(CG}&Ig1Py˱U6LZCoUM)	[L&'
bk<mvPl{`Zc'ԧa~U*ʅSŋ,(]	MB\=T>F}RѝR"Ih@0.¡#{Y	ţT<uMX6T/MOA_~.dahErS3_x̑4з;Vzs#6`Z|^tbllk9[oӎgAsJ~$kS{ڃRjGi)F/bV6Zq1
ɇ#MٜсБWuʗ097 X=ڑkQ77BVy|z/ s6J]mP)JJO{_r& gOLGn#<?kTXg@?e^V۹tሁ﫢,=6R.8]hc{](|kX;pWo2}me|WP5a0-.~hE6H*O/M>`()RحNTKH-}IٔmV`
@|3qb5(|e qZ;@]C%NLڛ~\q
_' fc4qg6껡ra芒pLccx L>d+.
R者=SbH"n'hB}JxYgP32>4':95cTĉ'1<NSxNr]eB^[qU@L.-o";v8'ռ?IM<K	:EO@.%`#@O42K.I)njqYV*SEҌZP;&-kUE|4۫SfT2vcq01_Z<_p`N,qJ$ai- soM'$Pٿ|#u9x-gUE?l;x#YVdI~NgyFn^VåNS{v[15"0gi 7(rm4
n%Y= xMqRzUhneܨKƢ6GӢՃp6TL)j

Z;l
C"Q'$E{&
G'OI5-Nw֊ ;ROߚ.0mκfQ)Im! PӜ<Eu2rhZ.Qm'P#1IO >d\3vgv3#e/J:&闯\L`pr>zzizwx}Qj̛9GB7F ƚ7H"x	кѶؚM8IS3 7\V?OO#WniJ.yIJd!f
N
V-n`ۜf-§];!(N+|mVnAm./X.v"hLC9yjx5K&UunvoqNY&$yw{;+<s{:=/߆ȣp`*RWXEL>0n:~J-eQRM65![#uc?(?*js)(Y*쀃cM<7plƸx+> \GLlRCCY޼(nBsM纵eKwU<Z~R6]uN0.Kd~d-z7^(eهV:Z|%]WR:{ ݱMgKy^L&-`eAdM5"a&gD8`fC!QE[eUqƖ5VxZpkcAeݨRe_>a"u(;{zlR:&,HF_TzJ&54O,1#^!1)8
8FiN2|F-!Ky!*!-QiOH7;$ח>>s:RݺSlueׅlǁɼ ͑i{2Pf-W-kPx}Kf|z˩0n|qao2t[!]neCfQ+chfqPND!0 ņ)j~l)C}1[$3d0uyƊxxNM=K8L(r?W_$pN.{ \ff	4Wy0>˸#'48؍AAA=Zsv*i1Ր#?Ҟ:"Kvӓ3(^ OCCmO52;~SE	|ɶ3LIb9jG:.MG`\tNAh4]DFI]̕* MӉ;;t5 n58"&VjK|̚kYWޫ:|[7 }P̈]t܍xkٗp*Qd\0ߛgpۯZ8ٕ˪(aC}0hjĪ4o"lW}51`ǀfVj'agoDĸС'":X !v2O>"M]`{Ea%?	0+ ̄oVI8atuKFbl;vLsq18nɧh.ǘauf뵮	@@ me˥9ibԥ﯃dSmq7#Z[H'FG,A`*Յ(Vc>WڛKRTwpk8;LA>]lZ蔵Jwc=9o{<#ǉqw$PoMMXu͍TΗzG(@Z3{V.RO,Vt
{`fPښ_D"xs6H]xdEGHJ$tJ8֬vq*9IY z(-Lx&JԒp2nmۚS+\,_`-T1pژ@!l>ΕEe;{,`ѧ{R^$LjGDl\¼|,ow~g˘bJ1>s.۟nd
~&gvw\3ZjAޅJ>>3c"u>SF=7"Pﶊ}qP=F *!%2_/SjE-4ݦGy]bopbXZqz`6,`T^)NpQ|%~AnSˣ3mE80<:Sbեt"ihg$k%Ky~nVc)R˩-xgcF%ˋ$jNe^9{&E2lz}S"`w LtCt]ИM|)Ƙ܃>fҽR.q5>ƣ){Jb8t'^E)[䜶Eȷ7S.+}P	Cb?n*hl>UQf@av:_Xس&veKޣf83A`"T)Lh61NC,Z< ษ-5 aLDR<ڳU+ٹ@MlEC5)k^6uP+q{6ğ"|H+3Hx⡡NlO^?J堪3 ܼܢ&׋vkKCrxO=ApsP)oЭNfE}'$")H˶^\l~g-^	kn>.HtT7u;n'_p@{dw%eMN,+\w+m{*`ҥ>d~m9Y
]´ed;	UX{Q_^SZc7A._p? 3i<*!ja;I$`4SQ_SDo6ksXR)
dJLf<K.Ji0!kZؒ'^G-hR7_|Y[-i `<mjjG疯Bpo=3΁
~Ēyjm7_ Z&Q"o%"ȦC5zӕcEZ#:UItY|d;ٻ )dأA5{im~h¦ya		,dEU"^ZpPT`[{<?yASz,}6'eVT>3QAW\>b܉Y9BV̉m.Z3׬''uq*Wi_#OTl2_ކt$k}4T8JH)u7}`և;n^DE4J8#ܘW4޶ eyCK}WZw@Re󁓬VAm~!g1ԝ#\10^<	g3} 2̓(^1.'?!,2+A]!GRi	3Y<U ~1!p7o(
IxAlP`*|N-~NXRTHo<EmY2d1GT,nt/~ٞkaSwYTC)ӊUr7'gz9$Q:WWЍn!fyZF&}";{+Vϼ&s\s,)!H(I W8eb &K0a(}{ɨL) i3.\alo:D\6XQ/a>n~GuP4]<,Ѕ
߽2)HH	 Zv(͙8Ga#_D(}	Gh}C3ө{;$	){I&P:Rq+SB1D;%u@)+fդjRmm(@@ۍu)!OrSFwrW,+ٞNOZ9g3X`hhѴok,jMYhh[	5VofpNuV!Luݛ.~%^+>|H%}NqFMB=T,QYg3V-gȉe"i0C8Rc7\#
OTm_=SzEBI&1eepƸzxcu/in?."p}@vq{k9#	jWcr>tj#spv&gng"{N;I^}0au5#lmu2{:=H͘(ރ;7'u ;EtG\s/5k9ֵ"7i
'`CF~#[I@qa/-9ruT^5O,uBx%ms,FUM&NV4c%1$(H0]_ g6^1oL/zLn`ZCbR4VU+S'23akxHፄDVm3;ˡ8ӵMh<D11ohv_4fAhRxqhPD-tbh8c.>lbR>2NF
xtj̞0ځ\6'j{m:X6h7M_Nx<M@Nw:RО%htO¡?%1Γ[٪^V$"AƏ%+/1<q~ Л&O&Ѹ`f?_..eL[bS(s ]O	D<0Nll .1D]э:.*IkÒUI	륥7uvHD1폾>Ϝ1v~dϫeIX*_ZGH`#Wq6CcF9H~Ӄ|;?ĆI&#*IL(w% 展uբؿ/s
Ii\Ġ,vϮgL,>ԛf?\M\F:I7i#Leǯi
`O&4D_Q	&(lL1LHyұ̀e.ݎggpE6J?y(%&oY~\aQ*BrYa]ZehF8>)3^Ѝ,r !XNN=em&º"t-G͡jh|6OحS_ETv0ؠ^ eh:R)<}Z?$>zF}(&tdԘ1i'<J`JfU>K+&iiRľ,n1 [参o#tdup7Cp<`d%9p.Xg{Y?hy!riLs&GeBj(J=?idڡ>' WPRZc>Ed5%߯+):_O5⤳Iwj:$UdgOp,ίmBeq\'R|mҜ]sSiiWԏRsAlփ.S0bm$-@K X_|v)zd͐!,\YmM"I>i59_%t*)ڢeu3ĺU31NQ-^q?r~s^H>)gǑ#h8#]upߔYph_1SQ3*t,(WFJi'
H[vbgz}7Ny- sq{Qjki`ĬZǯt;\f'i
E{\dVhh<mM`ǱFJqI n^s虻xB.׻}Ա(GAѴg,)}8WWoY*k*yZkhU'ow2Nݰ+wS<#t{'.*@%@uX~}`Hn1y87QYRmGQRFX);6}Pk!Glpœ*xDwznOkͥ+^x5%>'~ą6M͹kE#or2ƇAT$JIS	ETs)p7
Ө2>d#xyk۫UA^A~ZIkxe{XFCRsG%rm~l)4[<Kʵ5+-xwf;Y8JtMcw6x(Ȉ ;.9[; Q}c4e֟*a,L`C{Ϟ!+ҸaE"!gGDUOP 3I$fYMXqq?HƌmU-L tD30L?^Fx_73(dߝ_6L``b5m5Eɖ`~+
UM+*dþ_K3фk.;')/?	6i<bGTh<e^>]485ſO4e6qoq6x-<VRY"a27<.^/ۥCa%Gfh\P.M@4UUq(3ߏλYŚc)7$=I&3%A'Q$ﬖ\fYц/H2ELR(q%~^fze"N7.Ǔ$M=cǆ|BO R;^ Gdw7IgHto+2ubnۡa#Z#$KEsS]Ja/F%`x)|{IJ1LXC?Plϕ?*?j"f/*T.y\?G%/Ƕ$fiFʪbՌ'ªiݓ ^pK.T2yܭ3*E2kEIT +OM`u=c6حQ%Y>ר9@WZ-h߰<)U)F'nF6uGג˓?OeC\tgD˕;('9}E+Z'si:#7h1Տѐ9 sb^3cp	_1pBE|2ձB{"=F孊Fl\ bΦ H7}OCRh}_?a}&o|s}E1n\hXRU|Zg/4Hie9ו	I!J\l\Vlö_CC=,t%qa5Օqxt\lp}Yоpe\02g]Q=`la	&ໂJ(
TEkRzrri:nog/$MP:K-ޮYm|ý{b,&Ǎk<Ygxum l<R*I ,E~?ǝ$˨;^onDJ$]2,ꩫbv`$*Ё,U?xDbbAJ}']e@@D9~+DǄŁܝJ2gDt]m(a(gU1נu!IHL~T%e "혼{pѪaV3kxf?ǗC_ʇW+vXو_xXT'&6)
A

A1HB!ԣ;6%zņEI @uiIOKgB<"ZXĶ;Yς*S01dlO1$l<[MTtqI3!,\{*n$&..y.|=>zI3$@L42SLE-GL<LptAuʃn!*p w+(?>\t!N\b\X ѿ&owJF`%F [bחLq()Y.!7?T\ĲYIʒS&	x7YsGMQ>kSH;x0[چʩ@t*`-7"qa?PQ^$9z_u.m,^1TڞfV|ZI]&zoe}] au1'=Q'Csj4̊lG&c4$E;=|S[T5Ơu,ceL]ܻF냾>ܺj>X8-:t}m[NxxoNz ߋvZ@ExM"T60մ@9H |XȑTqVt[1ς'aFH -r~KN PGa4K>>>#|KGEV-jӧ1.+ݥA8~LeG]se<lkrLea3-8NƒfU3EEM^-&DNAΣ|N %V7y$uN45Ja͐{!!-.J8oOJ×(k_%[ d~#S)3vzaQRfbӜEJB!fOr{7}rcS(E8"g!&uO/A)sj2(hnN_&Z!-kL?M	(K8Ks'4C,Ku=AߥfI,~8)9H֭(щ@v冿f1׼A9Tk-˾_̨M#gJ^Od2@B:ҳD$]ξCt/"+tގYyΒ%5c8([:?;OhDfنQ.'6A<*R+isdb+rWu	FMMH	*ѕ/Eիj^\BGt^O5um?qSdŃ]&o YHCiGNhqwb6Ծ=a/K!֥TgYM=eWn̸VS7iM01z=A$4 2`9:UaR)k$1^w8Ijӣd~7)3#o,gDgWۤPє#ԟPYb#^$>:`UP0jQ3p^$Y.<{d]0|h3?nB}QMe<TR	Gme>41\)癋]˓:hI6Kz|yiZb(-rlZs0> s_HJ#!s=fF:20	|.c*?mpKuvZ8\-	n(W
	']SݯG~il!U⡉lfN2OQҙ:Q*Aae=Zuz,BY(@gLA^h|\q,djDih;(c5pn:}6j)}~{ҍ3&X/`bɐw*VSvL߿c[23JZ90YfIEn_S[H.bVzvsԄXN؅^w X !<=|/c0<|"u.24q8pό> QE&au){V휇2ګHG};RlX)8Zm"#RJT:q4b>9f#02ڈΟ!DsA֊AXfĹ/ȼ%'BHBݕp{<~k@'KG`:0gB-ŀtyFVS0:YpK]Le$!WN}ZgB-k.JRgY
Ow	#$zJȴ&"+3ƄFJejfot8O1ാ7%wQ7th\A5ipâ\<!_dpz}4N }RS'c(7cp͜|2ܭl<$g$#qlR4bZAJ)W?"ECUT7	@|/Q>>P}*beo?ZgLa/n8CYK@JsKex!s?lރVe
t&ACFuʛ#6U6z?ϩo斟͉&~ϛ:wF[ׁ-tEWŭ-s0^<]JE'L˰t/|#r<j\@MMlg84?IE({/./9vCyq╸^9eCl9F&^y+̈́[QyBt@ fqV[,H]UUgĝ1,J;R+D+L)J3
# µkKDUi)TvTF(@Rk* E._LSsrB߂uzVՒ/YEJ7TndmMֺ<ZZG]68N
|b`agqc~&( `,uttpCH
.cIuJ~]SPB_ymȈ- &oA#8uZʗ0\	Ne7H:t׬O&JZXJXsT{ʲ YPɽD,kMU(]IQvlnɓh[Tp0{_!_Ƿl>%H9Wlu?M"GȞCkPr&[!:&g!r7ƠALw!;Ua2F/^ȿLv|	"qlaT,e%'`4ec]jdp*2:Kfl5YNϥJdLkgV
-U#wf{Aҗ`5-}!Tkw'g	θnj+j"SGԪgnO%-B	D9Ś߀.QkNg/&=]ڴO0Mx{; 谙/9rYa!5h\^VN/WR*4Dف	|x5$`KX=/*/GԧANOn
N0iف	oI/Sf	JS@,cmFJk7at	#"Os-$Q7/%
NoVW+ˋ|;tkMnR	B1Ɋ5_P=b>,(Kn-LVSpW!-bL}&xtPy!	SyX@BW##%<-!}fg)_;}0S|3Y,հ<eĕF#	NI}-GK?f{r;T(D,(Z=2g,n^2	?=;\-ʈlA@|^62nn.!<MW{,@|2AE!2%6wvܬF[hg\~<nܚy~SjbڌYbMΙtOvYǯ<yR^@nvY=p[5J4>,<u׉<i&ns 锚 R=<@Lōq4 I>rޮcw%tlɳsR4@"gG郇AK.poc|OeI]0`X$Xm47ܖP;Ww[+qpu^2t5{v.?YBm\vT:",5?'.eHP +"Թf$xAZ["N&lZSoKѧʎRcx/Zqt"|`
RqyG0|Z}YI?V?H, IfovEs_^($$o]ϷĴ /اdXٻu[ʠQ2yԊMy8<uH~=A#ƤbSGXu\<GkA`{Su{}bF۵?߻oM
b<dJoWn0q)EK'RN]!]3c;xsBFۉԏ= Uh6](hYB=lF%J:!.U@BK`*mm NRxjB,]|^GvYvS벅3[no\BM;[qf&$HXi┭K#Vܯ ySL8y@}P&K-l2N.+*t]x`*Z.elkPK"Gtی.`(/a~Jv+K)U8|H>"`NWܸ?q%e^u!nT:(m@Us=lݏr+GF߭>nAP	>qnslG)}/~\/lH.ejJz&gC'o!(%1gʉFyBW*e\4r]&_y`#K
!Vz0QPJUGe%hΙҾSCT1ު*m{n#ء/k5:{!Ntr0MfdZO8s<ov^d1UŸ(g^M#(TԔy2Wĸ+ݫ+-PWj^I!PLʝ?!6
HR#\[0Ϝ#~)*R   ~xŠxP 100644 index.scss k6bQ)Z100644 variables.scss T]أtQ\0[.#ixa@use './variables.scss' as *;

* {
  box-sizing: border-box;
}

html,
body,
#app {
  width: 100%;
  min-width: 1180px;
  height: 100%;
  margin: 0;
}

body {
  color: $ink;
  background: $field;
  font-family: Inter, "PingFang SC", "Microsoft YaHei", Arial, sans-serif;
  letter-spacing: 0;
}

a {
  color: inherit;
  text-decoration: none;
}

.page-shell {
  padding: 22px 26px 34px;
}

.page-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 24px;
  margin-bottom: 18px;
}

.page-kicker {
  color: $coral;
  font-size: 12px;
  font-weight: 800;
  letter-spacing: 0;
}

.page-title {
  margin-top: 5px;
  color: $ink;
  font-size: 28px;
  font-weight: 900;
  line-height: 1.2;
}

.page-subtitle {
  margin-top: 8px;
  color: $muted;
  font-size: 14px;
}

.growth-grid {
  display: grid;
  gap: 16px;
}

.growth-card {
  border: 1px solid rgba(11, 47, 42, 0.08);
  border-radius: 8px;
  background: rgba(255, 255, 255, 0.82);
  box-shadow: 0 12px 30px rgba(11, 47, 42, 0.08);
}

.metric-value {
  color: $ink;
  font-size: 30px;
  font-weight: 900;
}

.metric-label {
  color: $muted;
  font-size: 13px;
  font-weight: 700;
}

.ink-panel {
  color: #fff;
  background: linear-gradient(135deg, #0b2f2a, #123f37);
}

.el-button {
  font-weight: 800;
}

.el-button.permission-disabled,
.el-button.permission-disabled:hover,
.el-button.permission-disabled:focus {
  cursor: not-allowed;
  opacity: 0.52;
  filter: grayscale(1);
}

.el-table {
  --el-table-header-bg-color: #f2f6ee;
  --el-table-header-text-color: #17312d;
  --el-table-row-hover-bg-color: #f6faf2;
}

.el-menu {
  border-right: 0;
}

.el-progress-bar__outer {
  background-color: #e0eadf;
}
v	x g$ink: #0b2f2a;
$ink-soft: #143f38;
$field: #f4f7ef;
$paper: #fbfcf7;
$mint: #17bebb;
$amber: #f5c451;
$coral: #ff7a59;
$line: #dbe7dc;
$muted: #72817b;
q*~xF 40000 AppCard I<9ţ"*|pp>ÇMB40000 StatusTag ͌qd"(Tz[=ʏsx% 100644 index.vue ltBokH7h#x0<template>
  <section :class="['app-card', dark && 'app-card--dark']">
    <slot />
  </section>
</template>

<script setup>
defineProps({
  dark: {
    type: Boolean,
    default: false,
  },
})
</script>

<style lang="scss" scoped>
.app-card {
  border: 1px solid rgba(11, 47, 42, 0.08);
  border-radius: 8px;
  background: rgba(255, 255, 255, 0.86);
  box-shadow: 0 14px 34px rgba(11, 47, 42, 0.08);
}

.app-card--dark {
  color: #ffffff;
  background: linear-gradient(135deg, #0b2f2a, #17463d);
  box-shadow: 0 16px 38px rgba(11, 47, 42, 0.18);
}
</style>
ץx% 100644 index.vue 9}ʥQj؈=buxm<template>
  <el-tag :type="type" effect="light" round>
    {{ label }}
  </el-tag>
</template>

<script setup>
import { computed } from 'vue'

const props = defineProps({
  status: {
    type: String,
    default: 'active',
  },
})

const label = computed(() => (props.status === 'active' ? '启用' : '停用'))
const type = computed(() => (props.status === 'active' ? 'success' : 'info'))
</script>
q9x% 40000 permission S_ujFLU-x$ 100644 index.js +Lt҈埆iGxzimport { useUserStore } from '@/store/modules/user'
import { hasPermission as checkPermission } from '@/utils/permission'

function hasPermission(value = []) {
  const userStore = useUserStore()
  return checkPermission(userStore.permissions, value)
}

const clickBlockers = new WeakMap()

function disableForPermission(el, binding) {
  if (hasPermission(binding.value)) return
  if ('disabled' in el) el.disabled = true
  el.setAttribute('disabled', '')
  el.setAttribute('aria-disabled', 'true')
  el.setAttribute('title', '没有操作权限')
  el.classList.add('is-disabled', 'permission-disabled')
  if (!clickBlockers.has(el)) {
    const blocker = (event) => {
      event.preventDefault()
      event.stopImmediatePropagation()
    }
    clickBlockers.set(el, blocker)
    el.addEventListener('click', blocker, true)
  }
}

export default {
  mounted(el, binding) {
    disableForPermission(el, binding)
  },
  updated(el, binding) {
    disableForPermission(el, binding)
  },
  unmounted(el) {
    const blocker = clickBlockers.get(el)
    if (blocker) el.removeEventListener('click', blocker, true)
    clickBlockers.delete(el)
  },
}
2xJ 40000 components ;+^@kE7100644 index.vue 08P{|Λ:hxe 40000 AppMain  S_JX46q40000 Navbar [uyϮJ=G.b40000 Sidebar $tn9ynUv,x% 100644 index.vue >OJr' XO>Uvެݲ&xb<template>
  <main class="app-main">
    <router-view v-slot="{ Component }">
      <transition name="fade-slide" mode="out-in">
        <component :is="Component" :key="$route.fullPath" />
      </transition>
    </router-view>
  </main>
</template>

<style lang="scss" scoped>
.app-main {
  flex: 1;
  min-height: 0;
  overflow-x: hidden;
  overflow-y: auto;
  overscroll-behavior: contain;
}

.fade-slide-enter-active,
.fade-slide-leave-active {
  transition: opacity 0.18s ease, transform 0.18s ease;
}

.fade-slide-enter-from,
.fade-slide-leave-to {
  opacity: 0;
  transform: translateY(8px);
}
</style>
x% 100644 index.vue 3)Z\\X׫^x<template>
  <header class="navbar">
    <div>
      <div class="navbar__crumb">立方和教育 / {{ route.meta?.title || '管理端' }}</div>
      <div class="navbar__title">{{ route.meta?.title || '管理端' }}</div>
    </div>
    <div class="navbar__right">
      <div class="navbar__account" @click="openProfileDialog">
        <el-avatar v-if="userStore.account?.wechatAvatar" :size="34" :src="userStore.account.wechatAvatar" shape="square" />
        <div v-else class="navbar__avatar">{{ avatarText }}</div>
        <div>
          <div class="navbar__name">{{ userStore.account?.name || '未登录' }}</div>
          <div class="navbar__role">{{ userStore.account?.roleName || '访客' }}</div>
        </div>
      </div>
      <el-button :icon="Edit" circle title="修改资料" @click="openProfileDialog" />
      <el-button :icon="Lock" circle title="修改密码" @click="openPasswordDialog" />
      <el-button :icon="SwitchButton" circle title="退出登录" @click="logout" />
    </div>

    <el-dialog v-model="profileDialogVisible" title="修改个人资料" width="460px" append-to-body>
      <el-form label-position="top">
        <el-form-item label="头像">
          <div class="avatar-edit">
            <el-avatar v-if="profileForm.wechatAvatar" :size="72" :src="profileForm.wechatAvatar" shape="square" />
            <div v-else class="avatar-edit__empty">{{ avatarText }}</div>
            <el-upload :show-file-list="false" :http-request="uploadProfileAvatar" accept="image/png,image/jpeg,image/webp,image/gif">
              <el-button :loading="uploadingAvatar">上传头像</el-button>
            </el-upload>
          </div>
        </el-form-item>
        <el-form-item label="姓名">
          <el-input v-model="profileForm.name" maxlength="20" placeholder="请输入姓名" />
        </el-form-item>
        <el-form-item label="微信昵称">
          <el-input v-model="profileForm.wechatNickname" maxlength="30" placeholder="选填" />
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="profileDialogVisible = false">取消</el-button>
        <el-button type="primary" :loading="savingProfile" @click="saveProfile">保存</el-button>
      </template>
    </el-dialog>

    <el-dialog v-model="passwordDialogVisible" title="修改密码" width="420px" append-to-body>
      <el-form label-position="top">
        <el-form-item label="原密码">
          <el-input v-model="passwordForm.oldPassword" type="password" show-password placeholder="请输入原密码" />
        </el-form-item>
        <el-form-item label="新密码">
          <el-input v-model="passwordForm.password" type="password" show-password placeholder="至少 6 位" />
        </el-form-item>
        <el-form-item label="确认密码">
          <el-input v-model="passwordForm.confirmPassword" type="password" show-password placeholder="请再次输入新密码" @keyup.enter="savePassword" />
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="passwordDialogVisible = false">取消</el-button>
        <el-button type="primary" :loading="savingPassword" @click="savePassword">保存</el-button>
      </template>
    </el-dialog>
  </header>
</template>

<script setup>
import { computed, reactive, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Edit, Lock, SwitchButton } from '@element-plus/icons-vue'
import { updatePassword, updateProfile } from '@/api/login'
import { uploadAvatar } from '@/api/upload'
import { useUserStore } from '@/store/modules/user'

const route = useRoute()
const router = useRouter()
const userStore = useUserStore()
const avatarText = computed(() => (userStore.account?.name || '立').slice(0, 1))
const profileDialogVisible = ref(false)
const passwordDialogVisible = ref(false)
const savingProfile = ref(false)
const savingPassword = ref(false)
const uploadingAvatar = ref(false)
const profileForm = reactive({
  name: '',
  wechatNickname: '',
  wechatAvatar: '',
})
const passwordForm = reactive({
  oldPassword: '',
  password: '',
  confirmPassword: '',
})

function resetProfileForm() {
  profileForm.name = userStore.account?.name || ''
  profileForm.wechatNickname = userStore.account?.wechatNickname || ''
  profileForm.wechatAvatar = userStore.account?.wechatAvatar || ''
}

function resetPasswordForm() {
  passwordForm.oldPassword = ''
  passwordForm.password = ''
  passwordForm.confirmPassword = ''
}

function openProfileDialog() {
  resetProfileForm()
  profileDialogVisible.value = true
}

function openPasswordDialog() {
  resetPasswordForm()
  passwordDialogVisible.value = true
}

async function uploadProfileAvatar(options) {
  uploadingAvatar.value = true
  try {
    const data = await uploadAvatar(options.file)
    profileForm.wechatAvatar = data.url
    ElMessage.success('头像已上传')
  } finally {
    uploadingAvatar.value = false
  }
}

async function saveProfile() {
  if (!profileForm.name.trim()) {
    ElMessage.warning('请输入姓名')
    return
  }
  savingProfile.value = true
  try {
    const account = await updateProfile(userStore.account.id, {
      name: profileForm.name,
      wechatNickname: profileForm.wechatNickname,
      wechatAvatar: profileForm.wechatAvatar,
    })
    userStore.setAccountInfo(account)
    profileDialogVisible.value = false
    ElMessage.success('资料已保存')
  } finally {
    savingProfile.value = false
  }
}

async function savePassword() {
  if (!passwordForm.oldPassword) {
    ElMessage.warning('请输入原密码')
    return
  }
  if (passwordForm.password.length < 6) {
    ElMessage.warning('新密码至少需要 6 位')
    return
  }
  if (passwordForm.password !== passwordForm.confirmPassword) {
    ElMessage.warning('两次输入的密码不一致')
    return
  }
  savingPassword.value = true
  try {
    const account = await updatePassword(userStore.account.id, {
      oldPassword: passwordForm.oldPassword,
      password: passwordForm.password,
      confirmPassword: passwordForm.confirmPassword,
    })
    userStore.setAccountInfo(account)
    passwordDialogVisible.value = false
    ElMessage.success('密码已修改')
  } finally {
    savingPassword.value = false
  }
}

async function logout() {
  try {
    await ElMessageBox.confirm('退出后需要重新登录当前管理账号，是否继续？', '确认退出登录', {
      confirmButtonText: '退出',
      cancelButtonText: '取消',
      type: 'warning',
    })
  } catch (error) {
    return
  }
  userStore.logout()
  router.push('/login')
}
</script>

<style lang="scss" scoped>
.navbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: 70px;
  padding: 0 26px;
  border-bottom: 1px solid rgba(11, 47, 42, 0.08);
  background: rgba(251, 252, 247, 0.88);
  backdrop-filter: blur(16px);
}

.navbar__crumb {
  color: #72817b;
  font-size: 12px;
  font-weight: 700;
}

.navbar__title {
  margin-top: 4px;
  color: #0b2f2a;
  font-size: 20px;
  font-weight: 900;
}

.navbar__right {
  display: flex;
  align-items: center;
  gap: 14px;
}

.navbar__account {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 12px;
  border: 1px solid #dbe7dc;
  border-radius: 8px;
  background: #ffffff;
  cursor: pointer;
}

.navbar__avatar,
.avatar-edit__empty {
  display: grid;
  place-items: center;
  color: #0b2f2a;
  background: #f5c451;
  font-weight: 900;
}

.navbar__avatar {
  width: 34px;
  height: 34px;
  border-radius: 8px;
}

.navbar__name {
  color: #17312d;
  font-size: 13px;
  font-weight: 900;
}

.navbar__role {
  margin-top: 2px;
  color: #72817b;
  font-size: 12px;
}

.avatar-edit {
  display: flex;
  align-items: center;
  gap: 16px;
}

.avatar-edit__empty {
  width: 72px;
  height: 72px;
  border-radius: 10px;
  font-size: 28px;
}
</style>
ťx% 100644 index.vue ԅB :W^I`[5x<template>
  <aside class="sidebar">
    <div class="sidebar__brand">
      <div class="sidebar__mark">和</div>
      <div>
        <div class="sidebar__name">立方和教育</div>
        <div class="sidebar__sub">Growth Admin</div>
      </div>
    </div>

    <div class="sidebar__menu-scroll">
      <el-menu :default-active="route.path" router background-color="transparent" text-color="#c7ded6" active-text-color="#f5c451">
        <template v-for="item in menus" :key="item.path">
          <el-sub-menu v-if="item.children?.length" :index="item.path">
            <template #title>
              <el-icon><component :is="menuIcon(item.meta?.icon)" /></el-icon>
              <span>{{ item.meta?.title }}</span>
            </template>
            <el-menu-item v-for="child in item.children" :key="child.path" :index="child.path">
              <el-icon><component :is="menuIcon(child.meta?.icon)" /></el-icon>
              <span>{{ child.meta?.title }}</span>
            </el-menu-item>
          </el-sub-menu>
          <el-menu-item v-else :index="item.path">
            <el-icon><component :is="menuIcon(item.meta?.icon)" /></el-icon>
            <span>{{ item.meta?.title }}</span>
          </el-menu-item>
        </template>
      </el-menu>
    </div>

    <div class="sidebar__bottom">
      <div class="sidebar__bottom-title">积分增长机制</div>
      <div class="sidebar__chain">
        <span>分享</span>
        <i />
        <span>到课</span>
        <i />
        <span>兑换</span>
      </div>
    </div>
  </aside>
</template>

<script setup>
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import { Calendar, Coin, DataLine, Grid, Key, Menu, Setting, Share, TrendCharts, UserFilled } from '@element-plus/icons-vue'
import { constantRoutes } from '@/router'
import { useUserStore } from '@/store/modules/user'
import { hasPermission } from '@/utils/permission'

const route = useRoute()
const userStore = useUserStore()

function resolvePath(parent, child) {
  if (child.startsWith('/')) return child
  if (parent === '/') return `/${child}`
  return `${parent}/${child}`
}

const iconMap = {
  Coin,
  Calendar,
  DataLine,
  Grid,
  Key,
  Menu,
  Setting,
  Share,
  TrendCharts,
  UserFilled,
}

function menuIcon(name) {
  return iconMap[name] || Menu
}

const menus = computed(() => {
  return constantRoutes
    .filter((item) => !item.hidden)
    .map((item) => {
      const children = (item.children || []).filter((child) => !child.hidden && hasPermission(userStore.permissions, child.meta?.permissions || []))
      if (!item.meta && children.length === 1) {
        const child = children[0]
        return {
          ...child,
          path: resolvePath(item.path, child.path),
        }
      }
      return {
        ...item,
        children: children.map((child) => ({
          ...child,
          path: resolvePath(item.path, child.path),
        })),
      }
    })
    .filter((item) => {
      if (item.children) return item.children.length && hasPermission(userStore.permissions, item.meta?.permissions || [])
      return hasPermission(userStore.permissions, item.meta?.permissions || [])
    })
})
</script>

<style lang="scss" scoped>
.sidebar {
  position: relative;
  display: flex;
  flex: 0 0 232px;
  flex-direction: column;
  width: 232px;
  height: 100vh;
  min-height: 0;
  padding: 20px 14px;
  overflow: hidden;
  color: #ffffff;
  background:
    linear-gradient(180deg, rgba(245, 196, 81, 0.13), transparent 32%),
    #0b2f2a;
}

.sidebar::after {
  position: absolute;
  right: -60px;
  bottom: 120px;
  width: 180px;
  height: 180px;
  border: 1px solid rgba(245, 196, 81, 0.18);
  content: "";
  transform: rotate(18deg);
}

.sidebar__brand {
  position: relative;
  z-index: 1;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 6px 8px 24px;
  flex: 0 0 auto;
}

.sidebar__menu-scroll {
  position: relative;
  z-index: 1;
  flex: 1;
  min-height: 0;
  margin: 0 -6px;
  padding: 0 6px 10px;
  overflow-x: hidden;
  overflow-y: auto;
  scrollbar-color: rgba(245, 196, 81, 0.5) transparent;
  scrollbar-width: thin;
}

.sidebar__menu-scroll::-webkit-scrollbar {
  width: 5px;
}

.sidebar__menu-scroll::-webkit-scrollbar-thumb {
  border-radius: 5px;
  background: rgba(245, 196, 81, 0.48);
}

.sidebar__mark {
  display: grid;
  place-items: center;
  width: 42px;
  height: 42px;
  border-radius: 8px;
  color: #0b2f2a;
  background: #f5c451;
  font-size: 22px;
  font-weight: 900;
}

.sidebar__name {
  font-size: 17px;
  font-weight: 900;
}

.sidebar__sub {
  margin-top: 2px;
  color: rgba(255, 255, 255, 0.58);
  font-size: 12px;
}

.sidebar :deep(.el-menu-item),
.sidebar :deep(.el-sub-menu__title) {
  height: 46px;
  margin: 4px 0;
  border-radius: 8px;
  font-weight: 800;
}

.sidebar :deep(.el-menu-item.is-active) {
  color: #0b2f2a;
  background: #f5c451;
}

.sidebar :deep(.el-sub-menu .el-menu-item) {
  min-width: auto;
  padding-left: 42px !important;
}

.sidebar__bottom {
  position: relative;
  z-index: 1;
  flex: 0 0 auto;
  margin-top: 10px;
  padding: 14px;
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 8px;
  background: rgba(255, 255, 255, 0.08);
}

.sidebar__bottom-title {
  color: #dff8ee;
  font-size: 12px;
  font-weight: 900;
}

.sidebar__chain {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 12px;
}

.sidebar__chain span {
  color: #0b2f2a;
  background: #9ae9dc;
  border-radius: 6px;
  padding: 5px 8px;
  font-size: 12px;
  font-weight: 900;
}

.sidebar__chain i {
  flex: 1;
  height: 1px;
  background: rgba(255, 255, 255, 0.28);
}
</style>
5xS<template>
  <div class="admin-layout">
    <Sidebar />
    <section class="admin-layout__main">
      <Navbar />
      <AppMain />
    </section>
  </div>
</template>

<script setup>
import Sidebar from './components/Sidebar/index.vue'
import Navbar from './components/Navbar/index.vue'
import AppMain from './components/AppMain/index.vue'
</script>

<style lang="scss" scoped>
.admin-layout {
  display: flex;
  min-width: 1180px;
  height: 100vh;
  min-height: 0;
  overflow: hidden;
  background:
    linear-gradient(90deg, rgba(11, 47, 42, 0.05) 1px, transparent 1px),
    linear-gradient(180deg, rgba(11, 47, 42, 0.04) 1px, transparent 1px),
    #f4f7ef;
  background-size: 30px 30px;
}

.admin-layout__main {
  display: flex;
  flex: 1;
  flex-direction: column;
  height: 100vh;
  min-width: 0;
  min-height: 0;
  overflow: hidden;
}
</style>
\9ximport { createApp } from 'vue'
import ElementPlus from 'element-plus'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import 'element-plus/dist/index.css'
import App from './App.vue'
import router from './router'
import store from './store'
import permission from './directive/permission'
import './permission'
import './assets/styles/index.scss'

const app = createApp(App)

app.directive('hasPermi', permission)
app.use(store)
app.use(router)
app.use(ElementPlus, { locale: zhCn })
app.mount('#app')
*txKimport router, { constantRoutes } from './router'
import { useUserStore } from './store/modules/user'
import { getToken } from './utils/auth'
import { hasPermission } from './utils/permission'

const whiteList = ['/login']

function resolvePath(parent, child) {
  if (child.startsWith('/')) return child
  if (parent === '/') return `/${child}`
  return `${parent}/${child}`
}

function firstAllowedPath(permissions) {
  for (const route of constantRoutes) {
    if (route.hidden) continue
    const children = route.children || []
    if (!children.length && hasPermission(permissions, route.meta?.permissions || [])) return route.path
    for (const child of children) {
      if (!child.hidden && hasPermission(permissions, child.meta?.permissions || [])) {
        return resolvePath(route.path, child.path)
      }
    }
  }
  return ''
}

router.beforeEach(async (to, from, next) => {
  document.title = to.meta?.title ? `${to.meta.title} - 立方和教育管理端` : '立方和教育管理端'

  const token = getToken()
  if (token) {
    if (to.path === '/login') {
      next({ path: '/index' })
      return
    }
    const userStore = useUserStore()
    try {
      const account = await userStore.loadCurrent()
      if (!account) {
        userStore.logout()
        next(`/login?redirect=${to.fullPath}`)
        return
      }
    } catch (error) {
      userStore.logout()
      next(`/login?redirect=${to.fullPath}`)
      return
    }
    if (!hasPermission(userStore.permissions, to.meta?.permissions || [])) {
      const path = firstAllowedPath(userStore.permissions)
      if (path && path !== to.path) next({ path })
      else {
        userStore.logout()
        next('/login')
      }
      return
    }
    next()
    return
  }

  if (whiteList.includes(to.path)) {
    next()
    return
  }
  next(`/login?redirect=${to.fullPath}`)
})
ix$ 100644 index.js TctqtExmimport { createRouter, createWebHashHistory } from 'vue-router'
import Layout from '@/layout/index.vue'

export const constantRoutes = [
  {
    path: '/login',
    component: () => import('@/views/login.vue'),
    hidden: true,
  },
  {
    path: '/',
    component: Layout,
    redirect: '/index',
    children: [
      {
        path: 'index',
        name: 'Dashboard',
        component: () => import('@/views/dashboard/index.vue'),
        meta: { title: '数据驾驶舱', icon: 'DataLine', affix: true, permissions: ['dashboard:overview:view'] },
      },
    ],
  },
  {
    path: '/system',
    component: Layout,
    redirect: '/system/account',
    meta: { title: '系统管理', icon: 'Setting' },
    children: [
      {
        path: 'account',
        name: 'Account',
        component: () => import('@/views/system/account/index.vue'),
        meta: { title: '用户管理', icon: 'UserFilled', permissions: ['system:user:view'] },
      },
      {
        path: 'role',
        name: 'Role',
        component: () => import('@/views/system/role/index.vue'),
        meta: { title: '角色权限', icon: 'Key', permissions: ['system:role:view'] },
      },
    ],
  },
  {
    path: '/leads',
    component: Layout,
    redirect: '/leads/index',
    meta: { title: '线索管理', icon: 'Share' },
    children: [
      {
        path: 'index',
        name: 'Leads',
        component: () => import('@/views/leads/index.vue'),
        meta: { title: '推荐线索', icon: 'Share', permissions: ['leads:referral:view'] },
      },
    ],
  },
  {
    path: '/points',
    component: Layout,
    redirect: '/points/manager',
    meta: { title: '积分管理', icon: 'Coin' },
    children: [
      {
        path: 'manager',
        name: 'PointManager',
        component: () => import('@/views/points/manager/index.vue'),
        meta: { title: '积分概览', icon: 'Coin', permissions: ['points:account:view'] },
      },
      {
        path: 'accounts',
        name: 'PointAccounts',
        component: () => import('@/views/points/accounts/index.vue'),
        meta: { title: '家长积分账户', icon: 'Coin', permissions: ['points:account:view'] },
      },
      {
        path: 'rewards',
        name: 'PointRewards',
        component: () => import('@/views/points/rewards/index.vue'),
        meta: { title: '兑换权益', icon: 'Coin', permissions: ['points:reward:edit'] },
      },
      {
        path: 'rewards/:id',
        name: 'PointRewardDetail',
        component: () => import('@/views/points/reward-detail/index.vue'),
        hidden: true,
        meta: { title: '兑换权益详情', permissions: ['points:reward:edit', 'points:redemption:view'] },
      },
      {
        path: 'redemptions',
        name: 'PointRedemptions',
        component: () => import('@/views/points/redemptions/index.vue'),
        meta: { title: '兑换审核', icon: 'Coin', permissions: ['points:redemption:view'] },
      },
      {
        path: 'vouchers',
        name: 'PointVouchers',
        component: () => import('@/views/points/vouchers/index.vue'),
        meta: { title: '券码核销', icon: 'Coin', permissions: ['points:voucher:verify'] },
      },
      {
        path: 'details',
        name: 'PointDetails',
        component: () => import('@/views/points/details/index.vue'),
        meta: { title: '积分明细', icon: 'Coin', permissions: ['points:account:view'] },
      },
    ],
  },
  {
    path: '/courses',
    component: Layout,
    redirect: '/courses/trial',
    meta: { title: '课程管理', icon: 'Calendar' },
    children: [
      {
        path: 'trial',
        name: 'TrialCourses',
        component: () => import('@/views/courses/trial/index.vue'),
        meta: { title: '试听课程', icon: 'Calendar', permissions: ['courses:trial:view'] },
      },
      {
        path: 'appointments',
        name: 'CourseAppointments',
        component: () => import('@/views/courses/appointments/index.vue'),
        meta: { title: '预约记录', icon: 'Tickets', permissions: ['courses:trial:view'] },
      },
    ],
  },
  {
    path: '/statistics',
    component: Layout,
    redirect: '/statistics/channel',
    meta: { title: '数据统计', icon: 'TrendCharts' },
    children: [
      {
        path: 'channel',
        name: 'ChannelStatistics',
        component: () => import('@/views/statistics/channel/index.vue'),
        meta: { title: '渠道转化', icon: 'Share', permissions: ['statistics:channel:view'] },
      },
    ],
  },
  {
    path: '/:pathMatch(.*)*',
    redirect: '/index',
    hidden: true,
  },
]

const router = createRouter({
  history: createWebHashHistory(),
  routes: constantRoutes,
  scrollBehavior: () => ({ top: 0 }),
})

export default router
xk export default {
  title: '立方和教育管理端',
  sideTheme: 'theme-growth',
  showTagsView: true,
}
,xF 100644 index.js C-pu^c>z340000 modules 3<qLn6? LΚ.xW import { createPinia } from 'pinia'

const store = createPinia()

export default store
Lyx# 100644 user.js ҭ]bh
>R{UYxnimport { defineStore } from 'pinia'
import { login, getInfo } from '@/api/login'
import { getAccount, getToken, removeAccount, removeToken, setAccount, setToken } from '@/utils/auth'

export const useUserStore = defineStore('user', {
  state: () => ({
    token: getToken(),
    account: getAccount(),
    permissions: getAccount()?.permissions || [],
    loaded: false,
  }),
  actions: {
    async loginByPassword(account, password) {
      const accountInfo = await login({ account, password })
      this.token = accountInfo.sessionToken
      this.account = accountInfo
      this.permissions = accountInfo.permissions || []
      this.loaded = true
      setToken(accountInfo.sessionToken)
      setAccount(accountInfo)
      return accountInfo
    },
    async loadCurrent(force = false) {
      if (!this.token) return null
      if (this.loaded && !force) return this.account
      const account = await getInfo()
      this.account = account
      this.permissions = account.permissions || []
      this.loaded = true
      setAccount(account)
      return account
    },
    setAccountInfo(account) {
      this.account = account
      this.permissions = account?.permissions || []
      this.loaded = true
      setAccount(account)
    },
    logout() {
      this.token = ''
      this.account = null
      this.permissions = []
      this.loaded = false
      removeToken()
      removeAccount()
    },
  },
})
&1xr 100644 auth.js q}iqz^'6v100644 permission.js j:C}grc=100644 request.js ed(璾Lt`1$xOconst TokenKey = 'Lfhe-Admin-Token'
const AccountKey = 'Lfhe-Admin-Account'

export function getToken() {
  return localStorage.getItem(TokenKey)
}

export function setToken(token) {
  localStorage.setItem(TokenKey, token)
}

export function removeToken() {
  localStorage.removeItem(TokenKey)
}

export function getAccount() {
  const raw = localStorage.getItem(AccountKey)
  return raw ? JSON.parse(raw) : null
}

export function setAccount(account) {
  localStorage.setItem(AccountKey, JSON.stringify(account))
}

export function removeAccount() {
  localStorage.removeItem(AccountKey)
}
Gx export function hasPermission(permissions = [], required = []) {
  if (!required || !required.length) return true
  if (permissions.includes('*:*:*')) return true
  return required.some((permission) => permissions.includes(permission))
}
YS޼2x,import axios from 'axios'
import { ElMessage } from 'element-plus'
import { getToken } from './auth'

const service = axios.create({
  baseURL: import.meta.env.VITE_APP_BASE_API || '/api',
  timeout: 10000,
})

service.interceptors.request.use((config) => {
  const token = getToken()
  if (token) config.headers.Authorization = `Bearer ${token}`
  return config
})

service.interceptors.response.use(
  (response) => {
    const body = response.data
    if (body?.ok === false) {
      ElMessage.error(body.message || '请求失败')
      return Promise.reject(new Error(body.message || '请求失败'))
    }
    return body?.data ?? body
  },
  (error) => {
    ElMessage.error(error.response?.data?.message || error.message || '网络异常')
    return Promise.reject(error)
  },
)

export default service
x 40000 courses x0UUjp>40000 dashboard P*Ӷ{tFC40000 leads q(_x
zKqwv100644 login.vue \Wl-V-40000 points :/a֡&:l40000 statistics ܔ2ؚЖyђjP40000 system 8Y,扻P>IfxG 40000 appointments W(/ ;%y40000 trial .hc 9nK%q0x% 100644 index.vue Ե[X[F~Pد)|Ѹx*<template>
  <div class="page-shell">
    <div class="page-head">
      <div>
        <div class="page-kicker">POINTS / ACCOUNTS</div>
        <div class="page-title">家长积分账户</div>
        <div class="page-subtitle">按家长、手机号、孩子信息和积分状态分页查询，积分调整统一在弹窗中完成。</div>
      </div>
      <div class="head-actions">
        <el-button @click="router.push('/points/details')">积分明细</el-button>
        <el-button color="#0b4f46" :icon="Refresh" :loading="loading" @click="query">查询</el-button>
      </div>
    </div>

    <div class="growth-grid metric-grid">
      <AppCard v-for="item in metrics" :key="item.label" class="metric-card">
        <div class="metric-card__label">{{ item.label }}</div>
        <div class="metric-card__value">{{ item.value }}</div>
        <div class="metric-card__hint">{{ item.hint }}</div>
      </AppCard>
    </div>

    <AppCard class="table-card">
      <el-form class="query-form" :model="filters" inline>
        <el-form-item label="关键词">
          <el-input v-model="filters.keyword" clearable placeholder="家长 / 手机号 / 孩子 / 账号ID" style="width: 260px" @keyup.enter="query" />
        </el-form-item>
        <el-form-item label="积分状态">
          <el-select v-model="filters.pointStatus" style="width: 160px">
            <el-option label="全部" value="all" />
            <el-option label="有可用积分" value="available" />
            <el-option label="有待到账" value="pending" />
            <el-option label="有将过期" value="expiring" />
          </el-select>
        </el-form-item>
        <el-form-item>
          <el-button color="#0b4f46" :icon="Search" @click="query">查询</el-button>
          <el-button @click="reset">重置</el-button>
        </el-form-item>
      </el-form>

      <el-table class="accounts-table" :data="result.list" v-loading="loading" row-key="user.id" :fit="true" style="width: 100%">
        <el-table-column label="家长" min-width="190">
          <template #default="{ row }">
            <div class="parent-cell">
              <div class="parent-cell__avatar">{{ row.user.name.slice(0, 1) }}</div>
              <div>
                <div class="parent-cell__name">{{ row.user.name }}</div>
                <div class="parent-cell__child">{{ row.user.child?.name || '-' }} · {{ row.user.phone || '-' }}</div>
              </div>
            </div>
          </template>
        </el-table-column>
        <el-table-column prop="points.available" label="可用积分" min-width="130" />
        <el-table-column prop="points.pending" label="待到账" min-width="130" />
        <el-table-column prop="points.frozen" label="冻结" min-width="110" />
        <el-table-column prop="points.expiring" label="将过期" min-width="130" />
        <el-table-column prop="user.id" label="账号ID" min-width="170" show-overflow-tooltip />
        <el-table-column label="操作" width="160" fixed="right">
          <template #default="{ row }">
            <el-button v-hasPermi="['points:account:adjust']" size="small" color="#0b4f46" @click="openAdjust(row)">调整</el-button>
            <el-button
              v-if="row.points.pending > 0"
              v-hasPermi="['points:account:adjust']"
              size="small"
              type="warning"
              plain
              @click="openSettle(row)"
            >
              到账
            </el-button>
          </template>
        </el-table-column>
      </el-table>

      <div class="pager">
        <el-pagination
          v-model:current-page="filters.pageNum"
          v-model:page-size="filters.pageSize"
          :page-sizes="[10, 20, 50, 100]"
          layout="total, sizes, prev, pager, next, jumper"
          :total="result.total"
          @size-change="load"
          @current-change="load"
        />
      </div>
    </AppCard>

    <el-dialog v-model="adjustVisible" title="调整积分" width="430px">
      <el-form label-width="86px">
        <el-form-item label="家长账号">
          <el-input :model-value="currentUserLabel" disabled />
        </el-form-item>
        <el-form-item label="调整账户">
          <el-radio-group v-model="adjustForm.status">
            <el-radio-button label="available">可用积分</el-radio-button>
            <el-radio-button label="pending">待到账</el-radio-button>
          </el-radio-group>
        </el-form-item>
        <el-form-item label="积分数量">
          <el-input-number v-model="adjustForm.amount" :step="50" />
        </el-form-item>
        <el-form-item label="调整原因">
          <el-input v-model="adjustForm.title" placeholder="如 到课补发 / 兑换冲正" />
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="adjustVisible = false">取消</el-button>
        <el-button color="#0b4f46" :loading="saving" @click="submitAdjust">确认调整</el-button>
      </template>
    </el-dialog>

    <el-dialog v-model="settleVisible" title="确认待到账积分" width="430px">
      <el-form label-width="86px">
        <el-form-item label="家长账号">
          <el-input :model-value="currentUserLabel" disabled />
        </el-form-item>
        <el-form-item label="待到账">
          <el-input :model-value="`${currentRow?.points?.pending || 0} 积分`" disabled />
        </el-form-item>
        <el-form-item label="到账数量">
          <el-input-number v-model="settleForm.amount" :min="1" :max="currentRow?.points?.pending || 1" :step="50" />
        </el-form-item>
        <el-form-item label="到账原因">
          <el-input v-model="settleForm.title" placeholder="如 到课确认 / 活动复核通过" />
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="settleVisible = false">取消</el-button>
        <el-button color="#0b4f46" :loading="settleSaving" @click="submitSettle">确认到账</el-button>
      </template>
    </el-dialog>
  </div>
</template>

<script setup>
import { computed, onMounted, reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Refresh, Search } from '@element-plus/icons-vue'
import AppCard from '@/components/AppCard/index.vue'
import { adjustPoints, listPointAccounts, settlePendingPoints } from '@/api/points/manager'

const router = useRouter()
const loading = ref(false)
const saving = ref(false)
const settleSaving = ref(false)
const adjustVisible = ref(false)
const settleVisible = ref(false)
const currentRow = ref(null)
const filters = reactive({
  keyword: '',
  pointStatus: 'all',
  pageNum: 1,
  pageSize: 10,
})
const result = reactive({
  list: [],
  total: 0,
  overview: {},
})
const adjustForm = reactive({
  userId: '',
  amount: 100,
  title: '后台手动调整',
  status: 'available',
})
const settleForm = reactive({
  userId: '',
  amount: 0,
  title: '手动确认到账',
})

const currentUserLabel = computed(() => {
  if (!currentRow.value) return ''
  return `${currentRow.value.user.name}（${currentRow.value.user.phone || currentRow.value.user.id}）`
})

const metrics = computed(() => [
  { label: '家长账户', value: result.overview.parents || 0, hint: '当前查询命中' },
  { label: '可用积分', value: formatNumber(result.overview.available || 0), hint: '当前结果合计' },
  { label: '待到账', value: formatNumber(result.overview.pending || 0), hint: '当前结果合计' },
  { label: '将过期', value: formatNumber(result.overview.expiring || 0), hint: '当前结果合计' },
])

function formatNumber(value) {
  return Number(value).toLocaleString('zh-CN')
}

async function load() {
  loading.value = true
  try {
    const data = await listPointAccounts(filters)
    Object.assign(result, data)
  } finally {
    loading.value = false
  }
}

function query() {
  filters.pageNum = 1
  load()
}

function reset() {
  filters.keyword = ''
  filters.pointStatus = 'all'
  filters.pageNum = 1
  load()
}

function openAdjust(row) {
  currentRow.value = row
  adjustForm.userId = row.user.id
  adjustForm.amount = 100
  adjustForm.title = '后台手动调整'
  adjustForm.status = 'available'
  adjustVisible.value = true
}

function openSettle(row) {
  currentRow.value = row
  settleForm.userId = row.user.id
  settleForm.amount = Number(row.points.pending || 0)
  settleForm.title = '手动确认到账'
  settleVisible.value = true
}

async function submitAdjust() {
  saving.value = true
  try {
    await adjustPoints({ ...adjustForm })
    ElMessage.success('积分已调整')
    adjustVisible.value = false
    await load()
  } finally {
    saving.value = false
  }
}

async function submitSettle() {
  if (!settleForm.amount || settleForm.amount <= 0) {
    ElMessage.warning('请输入到账积分数量')
    return
  }
  try {
    await ElMessageBox.confirm(
      `确认将 ${settleForm.amount} 待到账积分转为可用积分？`,
      '确认到账',
      {
        confirmButtonText: '确认到账',
        cancelButtonText: '取消',
        type: 'warning',
      },
    )
  } catch (error) {
    return
  }
  settleSaving.value = true
  try {
    await settlePendingPoints({ ...settleForm })
    ElMessage.success('积分已到账')
    settleVisible.value = false
    await load()
  } finally {
    settleSaving.value = false
  }
}

onMounted(load)
</script>

<style lang="scss" scoped>
.metric-grid {
  grid-template-columns: repeat(4, 1fr);
}

.head-actions {
  display: flex;
  gap: 10px;
}

.metric-card,
.table-card {
  padding: 20px;
}

.metric-card__label {
  color: #72817b;
  font-size: 13px;
  font-weight: 800;
}

.metric-card__value {
  margin-top: 10px;
  color: #0b2f2a;
  font-size: 30px;
  font-weight: 900;
}

.metric-card__hint {
  margin-top: 8px;
  color: #7c8a84;
  font-size: 12px;
}

.table-card {
  margin-top: 16px;
}

.query-form {
  margin-bottom: 14px;
}

.query-form :deep(.el-form-item) {
  margin-bottom: 10px;
}

.accounts-table {
  width: 100%;
}

.parent-cell {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
}

.parent-cell__avatar {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: 32px;
  height: 32px;
  border-radius: 8px;
  color: #0b2f2a;
  background: #f5c451;
  font-weight: 900;
}

.parent-cell__name {
  max-width: 136px;
  overflow: hidden;
  color: #17312d;
  font-weight: 900;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.parent-cell__child {
  max-width: 136px;
  overflow: hidden;
  margin-top: 2px;
  color: #72817b;
  font-size: 12px;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.pager {
  display: flex;
  justify-content: flex-end;
  margin-top: 16px;
}
</style>
\&xA
T9oCOURSES / APPOINTME}*预约记录 Y看家长试听预约记录，按账号配置的试听额度安排跟进与到课确认Iacourses/trial')">试听课程!6
           >"
            clearable
           b#e课程 / 校区 / 老师"
            style="width: 280px"
            @keyup.enter="query"
         :.status" style="width: 15L*状态v/已预约" value="booked" /P!到课" value="attended" /P!取消" value="cancelledEn1id"	7Eerson-cell">
              <strong>{{ row.user?.name || '-' }}</strong>
              <span>{{ row.user?.phone || '-' }}</span>	^试听课程" min-width="24Dcourse-cell">
              <strong>{{ row.course?.name || '-' }}</strong>
              <span>{{ row.course?.campus || '-' }} (/ {{ row.course?.teacher || '-' }}</span	_试听时间" min-width="15+!{{ row.course?.nextTime || '-' }}	@预约状态" width="129jel-tag :type="statusMeta(row.status).type" effect="plain" round>{{ statusMeta(row.status).label }}</el-tag	L预约时间" min-width="18+{{ formatTime(row.createdAt) }}	@3Pstatus === 'booked'courses:trial:edi.,color="#0b4f46"
              @click="attend\#确认到课$.  <span v-else class="operation-muted">-</span	9œjg=ttendAppointment, getAppointments } from '@/api/courses/trial!?&s%t".预约总数\total} 结果' },
  { label: '已预约\@booked || 0, hint: '等待到课跟进' },
  { label: '已到课\Eattended || 0, hint: '可触发到课积分' },
  { label: '已取消\cancelled || 0, hint: '无效或撤销预约' },
])

const statusMap = {
  booked: { label: '已预约', type: 'warning' },
  attended: { label: '已到课', type: 'success' },
  cancelled: { label: '已取消', type: 'info' },
}

function statusMeta(status) {
  return statusMap[status] || { label: status || '-', type: 'info' }
}

function formatTime(value) {
  if (!value) return '-'
  const date = new Date(value)
  if (Number.isNaN(date.getTime())) return String(value)
  return date.toLocaleString('zh-CN', { hour12: false }3SgetAppointmes_0async function attend(row) {"6F“${row.user?.name || row.userId}”已到课？确认后会把相关#1，并发放到课奖励。`,
      '确认到课S#1#t await attendAppointment(row.id)
O$ 已确认到课')
  await load()$iperson-cell,
.course-cell {'7gap: 5px;
}

.person-cell strong,
.course-cell strong {(''}

.person-cell span,
.course-cell span%#&	)L1
.operation-muted {
  color: #9aa7a1;
}
</style>
ѥx% 100644 index.vue {3R!C7YOY0Ix*,<template>
  <div class="page-shell">
    <div class="page-head">
      <div>
        <div class="page-kicker">COURSES / TRIAL</div>
        <div class="page-title">试听课程管理</div>
        <div class="page-subtitle">列表只展示课程信息，新增和编辑统一通过弹窗完成。</div>
      </div>
      <div class="head-actions">
        <el-button :icon="Calendar" @click="router.push('/courses/appointments')">预约记录</el-button>
        <el-button v-hasPermi="['courses:trial:add']" color="#0b4f46" :icon="Plus" @click="openCreate">新增课程</el-button>
        <el-button :icon="Search" :loading="loading" @click="query">查询</el-button>
      </div>
    </div>

    <div class="growth-grid metric-grid">
      <AppCard v-for="item in metrics" :key="item.label" class="metric-card">
        <div class="metric-card__label">{{ item.label }}</div>
        <div class="metric-card__value">{{ item.value }}</div>
        <div class="metric-card__hint">{{ item.hint }}</div>
      </AppCard>
    </div>

    <AppCard class="table-card">
      <el-form class="query-form" :model="filters" inline>
        <el-form-item label="关键词">
          <el-input v-model="filters.keyword" clearable placeholder="课程 / 老师 / 校区" style="width: 240px" @keyup.enter="query" />
        </el-form-item>
        <el-form-item label="状态">
          <el-select v-model="filters.status" style="width: 140px">
            <el-option label="全部" value="" />
            <el-option label="上架中" value="active" />
            <el-option label="已暂停" value="paused" />
          </el-select>
        </el-form-item>
        <el-form-item label="校区">
          <el-select v-model="filters.campus" clearable placeholder="全部校区" style="width: 170px">
            <el-option v-for="item in result.campusOptions" :key="item" :label="item" :value="item" />
          </el-select>
        </el-form-item>
        <el-form-item>
          <el-button color="#0b4f46" :icon="Search" @click="query">查询</el-button>
          <el-button @click="reset">重置</el-button>
        </el-form-item>
      </el-form>

      <el-table :data="result.list" v-loading="loading" row-key="id">
        <el-table-column label="课程" min-width="230">
          <template #default="{ row }">
            <div class="course-cell">
              <strong>{{ row.name }}</strong>
              <div class="tag-line">
                <el-tag v-for="tag in row.tags" :key="tag" size="small" effect="plain">{{ tag }}</el-tag>
              </div>
            </div>
          </template>
        </el-table-column>
        <el-table-column label="校区 / 老师" min-width="180">
          <template #default="{ row }">{{ row.campus || '-' }} / {{ row.teacher || '-' }}</template>
        </el-table-column>
        <el-table-column prop="nextTime" label="试听时间" min-width="170" />
        <el-table-column prop="seats" label="席位" width="90" />
        <el-table-column prop="pointsBack" label="到课积分" width="110" />
        <el-table-column prop="trialMinutes" label="时长" width="90" />
        <el-table-column label="状态" width="110">
          <template #default="{ row }">
            <el-tag :type="row.status === 'active' ? 'success' : 'info'" effect="plain" round>{{ row.status === 'active' ? '上架中' : '已暂停' }}</el-tag>
          </template>
        </el-table-column>
        <el-table-column label="操作" width="150" fixed="right">
          <template #default="{ row }">
            <el-button v-hasPermi="['courses:trial:edit']" size="small" color="#0b4f46" @click="openEdit(row)">编辑</el-button>
            <el-button v-hasPermi="['courses:trial:delete']" size="small" type="danger" plain :icon="Delete" @click="remove(row)">删除</el-button>
          </template>
        </el-table-column>
      </el-table>

      <div class="pager">
        <el-pagination
          v-model:current-page="filters.pageNum"
          v-model:page-size="filters.pageSize"
          :page-sizes="[10, 20, 50, 100]"
          layout="total, sizes, prev, pager, next, jumper"
          :total="result.total"
          @size-change="load"
          @current-change="load"
        />
      </div>
    </AppCard>

    <el-dialog v-model="dialogVisible" :title="dialogMode === 'create' ? '新增试听课程' : '编辑试听课程'" width="540px">
      <el-form class="course-dialog-form" label-width="86px">
        <el-form-item label="课程名称">
          <el-input v-model="form.name" placeholder="如 数学思维体验课" />
        </el-form-item>
        <el-form-item label="校区">
          <el-input v-model="form.campus" placeholder="如 南山校区" />
        </el-form-item>
        <el-form-item label="老师">
          <el-input v-model="form.teacher" placeholder="如 林老师" />
        </el-form-item>
        <el-form-item label="试听时间">
          <el-date-picker
            v-model="form.nextTime"
            type="datetime"
            format="YYYY-MM-DD HH:mm"
            value-format="YYYY-MM-DD HH:mm"
            placeholder="选择试听日期和时间"
            style="width: 100%"
          />
        </el-form-item>
        <el-form-item label="席位">
          <el-input-number v-model="form.seats" :min="0" :step="1" controls-position="right" />
        </el-form-item>
        <el-form-item label="到课积分">
          <el-input-number v-model="form.pointsBack" :min="0" :step="10" controls-position="right" />
        </el-form-item>
        <el-form-item label="时长">
          <el-input-number v-model="form.trialMinutes" :min="0" :step="5" controls-position="right" />
        </el-form-item>
        <el-form-item label="状态">
          <el-radio-group v-model="form.status">
            <el-radio-button label="active">上架</el-radio-button>
            <el-radio-button label="paused">暂停</el-radio-button>
          </el-radio-group>
        </el-form-item>
        <el-form-item label="标签">
          <el-input v-model="form.tagsText" placeholder="用逗号分隔，如 试听,小班" />
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="dialogVisible = false">取消</el-button>
        <el-button color="#0b4f46" :loading="saving" @click="submit">保存</el-button>
      </template>
    </el-dialog>
  </div>
</template>

<script setup>
import { computed, onMounted, reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Calendar, Delete, Plus, Search } from '@element-plus/icons-vue'
import AppCard from '@/components/AppCard/index.vue'
import { createTrialCourse, deleteTrialCourse, getTrialCourses, updateTrialCourse } from '@/api/courses/trial'

const router = useRouter()
const loading = ref(false)
const saving = ref(false)
const dialogVisible = ref(false)
const dialogMode = ref('create')
const editingId = ref('')
const filters = reactive({
  keyword: '',
  status: '',
  campus: '',
  pageNum: 1,
  pageSize: 10,
  paged: '1',
})
const result = reactive({
  list: [],
  total: 0,
  overview: {},
  campusOptions: [],
})
const form = reactive(createBlankForm())

function createBlankForm() {
  return {
    name: '',
    campus: '',
    teacher: '',
    nextTime: '',
    seats: 4,
    pointsBack: 100,
    trialMinutes: 45,
    status: 'paused',
    tagsText: '试听,小班',
  }
}

const metrics = computed(() => [
  { label: '上架课程', value: result.overview.active || 0, hint: `${result.overview.total || 0} 门当前结果` },
  { label: '可预约席位', value: result.overview.seats || 0, hint: '家长端可见名额' },
  { label: '覆盖校区', value: result.overview.campuses || 0, hint: '按校区筛选排期' },
  { label: '平均返积分', value: result.overview.avgPoints || 0, hint: '到课后待到账' },
])

async function load() {
  loading.value = true
  try {
    const data = await getTrialCourses(filters)
    Object.assign(result, data)
  } finally {
    loading.value = false
  }
}

function query() {
  filters.pageNum = 1
  load()
}

function reset() {
  filters.keyword = ''
  filters.status = ''
  filters.campus = ''
  filters.pageNum = 1
  load()
}

function openCreate() {
  dialogMode.value = 'create'
  editingId.value = ''
  Object.assign(form, createBlankForm())
  dialogVisible.value = true
}

function openEdit(row) {
  dialogMode.value = 'edit'
  editingId.value = row.id
  Object.assign(form, {
    name: row.name,
    campus: row.campus,
    teacher: row.teacher,
    nextTime: row.nextTime,
    seats: row.seats,
    pointsBack: row.pointsBack,
    trialMinutes: row.trialMinutes,
    status: row.status,
    tagsText: (row.tags || []).join(','),
  })
  dialogVisible.value = true
}

function buildPayload() {
  return {
    name: String(form.name || '').trim(),
    campus: String(form.campus || '').trim(),
    teacher: String(form.teacher || '').trim(),
    nextTime: String(form.nextTime || '').trim(),
    seats: Number(form.seats || 0),
    pointsBack: Number(form.pointsBack || 0),
    trialMinutes: Number(form.trialMinutes || 0),
    status: form.status || 'paused',
    tags: String(form.tagsText || '')
      .split(/[,，]/)
      .map((item) => item.trim())
      .filter(Boolean),
  }
}

async function submit() {
  const payload = buildPayload()
  if (!payload.name) {
    ElMessage.warning('请填写课程名称')
    return
  }
  if (!payload.nextTime) {
    ElMessage.warning('请选择试听日期和时间')
    return
  }
  saving.value = true
  try {
    if (dialogMode.value === 'create') {
      await createTrialCourse(payload)
      ElMessage.success('试听课程已新增')
    } else {
      await updateTrialCourse(editingId.value, payload)
      ElMessage.success('试听课程已保存')
    }
    dialogVisible.value = false
    await load()
  } finally {
    saving.value = false
  }
}

async function remove(row) {
  try {
    await ElMessageBox.confirm(`仅可删除没有预约记录的「${row.name}」。如已有预约，请将课程暂停以保留历史。`, '确认删除课程', {
      confirmButtonText: '确认删除',
      cancelButtonText: '取消',
      confirmButtonClass: 'el-button--danger',
      type: 'warning',
    })
  } catch (error) {
    return
  }
  await deleteTrialCourse(row.id)
  ElMessage.success('试听课程已删除')
  await load()
}

onMounted(load)
</script>

<style lang="scss" scoped>
.metric-grid {
  grid-template-columns: repeat(4, 1fr);
}

.metric-card,
.table-card {
  padding: 20px;
}

.head-actions {
  display: flex;
  gap: 10px;
}

.metric-card__label {
  color: #72817b;
  font-size: 13px;
  font-weight: 800;
}

.metric-card__value {
  margin-top: 10px;
  color: #0b2f2a;
  font-size: 30px;
  font-weight: 900;
}

.metric-card__hint {
  margin-top: 8px;
  color: #7c8a84;
  font-size: 12px;
}

.table-card {
  margin-top: 16px;
}

.query-form {
  margin-bottom: 14px;
}

.query-form :deep(.el-form-item) {
  margin-bottom: 10px;
}

.course-cell strong {
  color: #17312d;
  font-weight: 900;
}

.tag-line {
  display: flex;
  gap: 6px;
  margin-top: 8px;
}

.course-dialog-form {
  padding-right: 10px;
}

.pager {
  display: flex;
  justify-content: flex-end;
  margin-top: 16px;
}
</style>
 x% 100644 index.vue }DrEGO AK/x7<template>
  <div class="page-shell">
    <div class="page-head">
      <div>
        <div class="page-kicker">GROWTH LEDGER</div>
        <div class="page-title">推广积分驾驶舱</div>
        <div class="page-subtitle">聚合账号、渠道、线索、积分发放，给管理者看清每一次推荐带来的真实转化。</div>
      </div>
      <el-button color="#0b4f46" :icon="Refresh" :loading="loading" @click="load">刷新数据</el-button>
    </div>

    <div class="growth-grid dashboard-grid">
      <AppCard v-for="metric in metrics" :key="metric.label" class="metric-card">
        <div class="metric-card__label">{{ metric.label }}</div>
        <div class="metric-card__value">{{ metric.value }}</div>
        <div class="metric-card__hint">{{ metric.hint }}</div>
      </AppCard>
    </div>

    <AppCard class="trend-panel">
      <div class="trend-panel__head">
        <div>
          <div class="section-title">数据增长趋势</div>
          <div class="section-subtitle">新增家长与推荐线索每日趋势</div>
        </div>
        <div class="trend-filters">
          <el-radio-group v-model="trendPreset" size="small" @change="selectPreset">
            <el-radio-button value="7">近 7 天</el-radio-button>
            <el-radio-button value="30">近 30 天</el-radio-button>
            <el-radio-button value="90">近 90 天</el-radio-button>
          </el-radio-group>
          <el-date-picker
            v-model="trendRange"
            type="daterange"
            value-format="YYYY-MM-DD"
            start-placeholder="开始日期"
            end-placeholder="结束日期"
            :clearable="false"
            :disabled-date="disableFutureDate"
            @change="applyCustomRange"
          />
        </div>
      </div>

      <div class="trend-grid" v-loading="loading">
        <section class="trend-chart-block">
          <div class="trend-chart-block__head">
            <div>
              <div class="trend-chart-block__title">每日增长用户数</div>
              <div class="trend-chart-block__meta">所选时段新增家长</div>
            </div>
            <strong>{{ trendUserTotal }}</strong>
          </div>
          <div ref="userTrendEl" class="trend-chart" />
        </section>
        <section class="trend-chart-block trend-chart-block--lead">
          <div class="trend-chart-block__head">
            <div>
              <div class="trend-chart-block__title">推荐线索趋势</div>
              <div class="trend-chart-block__meta">所选时段新增线索</div>
            </div>
            <strong>{{ trendLeadTotal }}</strong>
          </div>
          <div ref="leadTrendEl" class="trend-chart" />
        </section>
      </div>
    </AppCard>

    <div class="growth-grid dashboard-main">
      <AppCard dark class="chain-card">
        <div class="chain-card__head">
          <div>
            <div class="chain-card__kicker">推荐链路</div>
            <div class="chain-card__title">朋友圈 / 微信推荐后的积分流向</div>
          </div>
          <el-tag effect="dark" color="#f5c451">实时口径</el-tag>
        </div>
        <div class="chain-card__steps">
          <div v-for="step in chainSteps" :key="step.title" class="chain-step">
            <span>{{ step.title }}</span>
            <strong>{{ step.points }}</strong>
            <small>{{ step.desc }}</small>
          </div>
        </div>
      </AppCard>

      <AppCard class="channel-card">
        <div class="section-head">
          <div>
            <div class="section-title">渠道转化排行</div>
            <div class="section-subtitle">按推荐来源对比到课和报名效率</div>
          </div>
        </div>
        <div class="channel-list">
          <div v-for="item in overview.channelStats" :key="item.channel" class="channel-row">
            <div class="channel-row__head">
              <span>{{ item.channel }}</span>
              <strong>{{ conversion(item) }}%</strong>
            </div>
            <el-progress :percentage="conversion(item)" :stroke-width="12" color="#17bebb" :show-text="false" />
            <div class="channel-row__meta">
              <span>线索 {{ item.leads }}</span>
              <span>到课 {{ item.attended }}</span>
              <span>报名 {{ item.purchased }}</span>
            </div>
          </div>
        </div>
      </AppCard>
    </div>
  </div>
</template>

<script setup>
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
import { Refresh } from '@element-plus/icons-vue'
import * as echarts from 'echarts/core'
import { LineChart } from 'echarts/charts'
import { GridComponent, TooltipComponent } from 'echarts/components'
import { CanvasRenderer } from 'echarts/renderers'
import AppCard from '@/components/AppCard/index.vue'
import { getPcOverview } from '@/api/statistics/dashboard'

echarts.use([LineChart, GridComponent, TooltipComponent, CanvasRenderer])

const loading = ref(false)
const trendPreset = ref('30')
const trendRange = ref(createDateRange(30))
const userTrendEl = ref(null)
const leadTrendEl = ref(null)
let userTrendChart
let leadTrendChart
let chartResizeObserver
const overview = reactive({
  overview: {},
  accounts: [],
  roleMatrix: [],
  channelStats: [],
  referralRules: [],
  trend: {
    dates: [],
    users: [],
    leads: [],
  },
})

const chainStepMeta = {
  registered: { title: '注册', desc: '新家长通过邀请码完成登录' },
  booked: { title: '预约', desc: '被推荐者预约试听课程' },
  attended: { title: '到课', desc: '确认到课后双方到账' },
  purchased: { title: '报名', desc: '正价课成交奖励' },
}

const chainSteps = computed(() => {
  const rules = overview.referralRules?.length
    ? overview.referralRules
    : [
        { event: 'registered', title: '新家长注册', inviter: 20, invitee: 20 },
        { event: 'booked', title: '预约试听', inviter: 50, invitee: 50 },
        { event: 'attended', title: '确认到课', inviter: 150, invitee: 100 },
        { event: 'purchased', title: '报名正价课', inviter: 800, invitee: 500 },
      ]
  return rules.map((rule) => ({
    title: chainStepMeta[rule.event]?.title || rule.title,
    points: `推荐人 +${rule.inviter} / 新家长 +${rule.invitee}`,
    desc: chainStepMeta[rule.event]?.desc || rule.title,
  }))
})

const metrics = computed(() => [
  { label: '管理账号', value: overview.overview.accounts || 0, hint: `${overview.overview.activeAccounts || 0} 个启用中` },
  { label: '推荐线索', value: overview.overview.leads || 0, hint: '覆盖朋友圈、好友、推荐码' },
  { label: '报名转化', value: `${overview.overview.conversion || 0}%`, hint: '按报名 / 线索计算' },
  { label: '已发放积分', value: formatNumber(overview.overview.pointsCost || 0), hint: '推荐、预约、到课等正向积分合计' },
])
const trendUserTotal = computed(() => overview.trend.users.reduce((sum, value) => sum + Number(value || 0), 0))
const trendLeadTotal = computed(() => overview.trend.leads.reduce((sum, value) => sum + Number(value || 0), 0))

function formatDateKey(date) {
  const year = date.getFullYear()
  const month = String(date.getMonth() + 1).padStart(2, '0')
  const day = String(date.getDate()).padStart(2, '0')
  return `${year}-${month}-${day}`
}

function createDateRange(days) {
  const end = new Date()
  const start = new Date(end)
  start.setDate(start.getDate() - Math.max(0, Number(days || 30) - 1))
  return [formatDateKey(start), formatDateKey(end)]
}

function disableFutureDate(date) {
  const today = new Date()
  today.setHours(23, 59, 59, 999)
  return date > today
}

function selectPreset(value) {
  trendRange.value = createDateRange(Number(value))
  load()
}

function applyCustomRange(value) {
  if (!value?.length) return
  trendPreset.value = ''
  load()
}

function formatNumber(value) {
  return Number(value || 0).toLocaleString('zh-CN')
}

function conversion(item) {
  if (!item.leads) return 0
  return Math.round((item.purchased / item.leads) * 100)
}

function chartOption(seriesName, data, color, areaColor) {
  const dates = overview.trend.dates || []
  return {
    animationDuration: 420,
    aria: { enabled: true },
    tooltip: {
      trigger: 'axis',
      backgroundColor: '#17312d',
      borderWidth: 0,
      padding: [9, 12],
      textStyle: { color: '#ffffff', fontSize: 12 },
      formatter(params) {
        const item = params?.[0]
        return `${item?.axisValue || ''}<br/>${seriesName}：<strong>${item?.value || 0}</strong>`
      },
    },
    grid: { left: 42, right: 18, top: 20, bottom: 32 },
    xAxis: {
      type: 'category',
      boundaryGap: false,
      data: dates,
      axisLine: { lineStyle: { color: '#d7e1d8' } },
      axisTick: { show: false },
      axisLabel: {
        color: '#72817b',
        hideOverlap: true,
        formatter(value) { return String(value).slice(5) },
      },
    },
    yAxis: {
      type: 'value',
      minInterval: 1,
      axisLine: { show: false },
      axisTick: { show: false },
      axisLabel: { color: '#72817b' },
      splitLine: { lineStyle: { color: '#e7eee7', type: 'dashed' } },
    },
    series: [{
      name: seriesName,
      type: 'line',
      data,
      smooth: 0.28,
      symbol: 'circle',
      symbolSize: 7,
      showSymbol: dates.length <= 31,
      lineStyle: { width: 3, color },
      itemStyle: { color, borderColor: '#ffffff', borderWidth: 2 },
      areaStyle: { color: areaColor },
      emphasis: { focus: 'series' },
    }],
  }
}

function ensureCharts() {
  if (userTrendEl.value && !userTrendChart) userTrendChart = echarts.init(userTrendEl.value)
  if (leadTrendEl.value && !leadTrendChart) leadTrendChart = echarts.init(leadTrendEl.value)
  if (!chartResizeObserver && typeof ResizeObserver !== 'undefined') {
    chartResizeObserver = new ResizeObserver(() => {
      userTrendChart?.resize()
      leadTrendChart?.resize()
    })
    if (userTrendEl.value) chartResizeObserver.observe(userTrendEl.value)
    if (leadTrendEl.value) chartResizeObserver.observe(leadTrendEl.value)
  }
}

async function renderTrendCharts() {
  await nextTick()
  ensureCharts()
  userTrendChart?.setOption(chartOption('新增家长', overview.trend.users || [], '#0b6f63', 'rgba(23, 190, 187, 0.14)'), true)
  leadTrendChart?.setOption(chartOption('新增线索', overview.trend.leads || [], '#e86543', 'rgba(255, 122, 89, 0.14)'), true)
}

async function load() {
  loading.value = true
  try {
    const [startDate, endDate] = trendRange.value || []
    const data = await getPcOverview({ startDate, endDate })
    Object.assign(overview, data)
    await renderTrendCharts()
  } finally {
    loading.value = false
  }
}

onMounted(load)
onBeforeUnmount(() => {
  chartResizeObserver?.disconnect()
  userTrendChart?.dispose()
  leadTrendChart?.dispose()
})
</script>

<style lang="scss" scoped>
.dashboard-grid {
  grid-template-columns: repeat(4, 1fr);
}

.metric-card {
  padding: 18px;
}

.metric-card__label {
  color: #72817b;
  font-size: 13px;
  font-weight: 800;
}

.metric-card__value {
  margin-top: 10px;
  color: #0b2f2a;
  font-size: 30px;
  font-weight: 900;
}

.metric-card__hint {
  margin-top: 8px;
  color: #7c8a84;
  font-size: 12px;
}

.dashboard-main {
  grid-template-columns: 1.2fr 0.8fr;
  margin-top: 16px;
}

.trend-panel {
  margin-top: 16px;
  padding: 22px;
}

.trend-panel__head,
.trend-chart-block__head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
}

.trend-filters {
  display: flex;
  align-items: center;
  gap: 10px;
}

.trend-filters :deep(.el-date-editor) {
  width: 260px;
}

.trend-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  margin-top: 20px;
}

.trend-chart-block {
  min-width: 0;
  padding-right: 22px;
}

.trend-chart-block--lead {
  padding-right: 0;
  padding-left: 22px;
  border-left: 1px solid #e2eae2;
}

.trend-chart-block__title {
  color: #17312d;
  font-size: 15px;
  font-weight: 900;
}

.trend-chart-block__meta {
  margin-top: 4px;
  color: #72817b;
  font-size: 12px;
}

.trend-chart-block__head strong {
  color: #0b2f2a;
  font-size: 26px;
  line-height: 1;
}

.trend-chart-block--lead .trend-chart-block__head strong {
  color: #e86543;
}

.trend-chart {
  width: 100%;
  height: 280px;
  margin-top: 10px;
}

.chain-card,
.channel-card {
  padding: 22px;
}

.chain-card__head,
.section-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
}

.chain-card__kicker {
  color: #9ae9dc;
  font-size: 12px;
  font-weight: 900;
}

.chain-card__title,
.section-title {
  margin-top: 6px;
  font-size: 20px;
  font-weight: 900;
}

.section-subtitle {
  margin-top: 4px;
  color: #72817b;
  font-size: 13px;
}

.chain-card__steps {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 12px;
  margin-top: 28px;
}

.chain-step {
  min-height: 142px;
  padding: 18px;
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 8px;
  background: rgba(255, 255, 255, 0.08);
}

.chain-step span,
.chain-step small {
  display: block;
}

.chain-step span {
  color: #9ae9dc;
  font-size: 13px;
  font-weight: 900;
}

.chain-step strong {
  display: block;
  margin: 14px 0 10px;
  color: #f5c451;
  font-size: 22px;
  font-weight: 900;
  line-height: 1.25;
}

.chain-step small {
  color: rgba(255, 255, 255, 0.66);
  line-height: 1.5;
}

.channel-list {
  display: grid;
  gap: 18px;
  margin-top: 18px;
}

.channel-row__head,
.channel-row__meta {
  display: flex;
  justify-content: space-between;
  gap: 12px;
}

.channel-row__head {
  margin-bottom: 8px;
  color: #17312d;
  font-size: 14px;
  font-weight: 900;
}

.channel-row__head strong {
  color: #ff7a59;
}

.channel-row__meta {
  margin-top: 8px;
  color: #72817b;
  font-size: 12px;
}

@media (max-width: 1380px) {
  .trend-panel__head {
    align-items: stretch;
    flex-direction: column;
  }

  .trend-filters {
    justify-content: space-between;
  }
}

</style>
a֥x% 100644 index.vue ӍbK;j{TxKT7oLEADS / REFERRAL'推荐线索 subtitle">跟进推荐来的新家长，从注册、预约、到课到确认正价课报名，闭环触发对应积分奖励。L*load">刷新</el-buttonH=!来源 / 渠道" style="width: 28R阶段)stage" style="width: 15Lv-for="stage in stageOptions" :key="stage" :label="stage === '全部' ? '全部阶段' : stage" :value="stage" />
          </eWn1id"	家长" min-width="18Eerson-cell">
              <strong>{{ row.parent }}</strong>
              <span>{{ row.channel || '-' }}</span>
            </	O荐来源" min-width="16Dhsource-cell">
              <strong>{{ row.source || '-' }}</strong>
              <span>推荐人</span	_阶段" width="129Pel-tag :type="stageType(row.stage)" effect="plain" round>{{ row.stage }}</el-tag	L风险" width="119el-tag :type="row.risk === '正常' ? 'success' : 'warning'" effect="plain" round>{{ row.risk || '正常' }}</el-tag>
         	A下一步" min-width="21Pstage !== '已报名'leads:referral:advance-Z:type="nextStage(row) === '已报名' ? 'danger' : 'primary'"
              @click="advanc[${{ actionLabel(row) }}$<  <span v-else class="operation-muted">已完成报名</span	9</el-table>
    </AppCardi*confirmLeadPurchase, getLeads, updateLeadStage } from '@/api/leads'

const stageFlow = ['待跟进', '已注册', '已预约', G'已到课', '已报名']
const stageOptions = ['全部', ...stageFlow]>"&stage: '全部'O72前线索', value: result.total || result.list.length, hint: '当前查询条件下的线索数' },
  { label: '已预约', value: countReached('已预约'), hint: '包含已到课和已报名' },
  { label: '已到课', value: countReached('已到课'), hint: '可进入报名确认' },
  { label: '已报名', value: countReached('已报名'), hint: '已触发报名奖励' },
])

function countReached(stage) {
  const rank = stageFlow.indexOf(stage)
  return result.list.filter((item) => stageFlow.indexOf(item.stage) >= rank).length
}

function nextStage(row) {
  const index = stageFlow.indexOf(row.stage)
  return stageFlow[Math.min(index + 1, stageFlow.length - 1)] || '已注册'
}

function actionLabel(row) {
  return nextStage(row) === '已报名' ? '确认报名正价课' : `推进到${nextStage(row)}`
}

function stageType(stage) {
  const map = {
    待跟进: 'info',
    已注册: 'primary',
    已预约: 'warning',
    已到课: 'success',
    已报名: 'danger',
  }
  return map[stage] 	|| 'info'4RgetLeado>stage = '全部'
  load()
}

async function advance(row) {
  const next = nextStage(row)
  const isPurchase = next === '已报'"/isPurchase
        ? `确认“${row.parent}”已报名正价课？确认后会发放报名阶段推荐奖励，并更新推漏斗。`
        : `将“${row.parent}”从“${row.stage}”推进到“${next}”？`,
      isPurchase ? '确认报名价课' : '推进线索S#&+isPurchase ? '确认报名' : '确认推进#5isPurchase ? 'warning' : 'info#7if (isPurchase) await confirmLeadPurchase(row.id)
  else await updateLeadStage(row.id, next)
  ElMessage.success(isPurchase ? 'G已确认报名并发放奖励' : `已推进到${next}`)
  await load()$v~%person-cell,
.source-cell {'7gap: 5px;
}

.person-cell strong,
.source-cell strong {(''}

.person-cell span,
.source-cell span%#&	/operation-muted {
  color: #9aa7a1;
}
</style>
F.x;<template>
  <main class="login-page">
    <section class="login-stage" :style="{ '--login-stage-image': `url(${loginStageImage})` }">
      <div class="login-stage__brand">
        <div class="login-stage__mark">和</div>
        <div>
          <div class="login-stage__name">立方和教育</div>
          <div class="login-stage__sub">推广积分 · 账号权限 · 数据统计</div>
        </div>
      </div>
      <div class="login-stage__chain">
        <span>家长分享</span>
        <i />
        <span>推荐到课</span>
        <i />
        <span>积分兑换</span>
        <i />
        <span>权限审计</span>
      </div>
    </section>

    <section class="login-panel">
      <div class="login-panel__head">
        <div class="page-kicker">PC ADMIN</div>
        <div class="page-title">账号密码登录</div>
        <div class="page-subtitle">PC 端独立管理账号权限、角色矩阵与渠道统计。</div>
      </div>
      <el-form class="login-form" @submit.prevent>
        <el-form-item>
          <el-input v-model="form.account" size="large" placeholder="账号 / 手机号" />
        </el-form-item>
        <el-form-item>
          <el-input v-model="form.password" size="large" type="password" show-password placeholder="密码" @keyup.enter="handleLogin" />
        </el-form-item>
        <el-button class="login-button" color="#0b4f46" size="large" :loading="loading" @click="handleLogin">登录管理端</el-button>
      </el-form>
    </section>
  </main>
</template>

<script setup>
import { ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import loginStageImage from '@/assets/images/login-learning-studio.webp'
import { useUserStore } from '@/store/modules/user'

const router = useRouter()
const route = useRoute()
const userStore = useUserStore()
const loading = ref(false)
const form = ref({
  account: '',
  password: '',
})

async function handleLogin() {
  if (!form.value.account || !form.value.password) {
    ElMessage.warning('请输入账号和密码')
    return
  }
  loading.value = true
  try {
    const account = await userStore.loginByPassword(form.value.account, form.value.password)
    ElMessage.success(`欢迎回来，${account.name}`)
    router.push(route.query.redirect || '/index')
  } finally {
    loading.value = false
  }
}
</script>

<style lang="scss" scoped>
.login-page {
  display: grid;
  grid-template-columns: 1.08fr 0.92fr;
  min-width: 1180px;
  min-height: 100vh;
  background: #f4f7ef;
}

.login-stage {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  padding: 42px 52px;
  overflow: hidden;
  isolation: isolate;
  color: #ffffff;
  background: #0b2f2a var(--login-stage-image) 58% center / cover no-repeat;
}

.login-stage::before {
  position: absolute;
  inset: 0;
  z-index: -1;
  background: rgba(5, 35, 29, 0.46);
  content: "";
}

.login-stage__brand {
  display: flex;
  align-items: center;
  gap: 14px;
  position: relative;
  z-index: 1;
}

.login-stage__mark {
  display: grid;
  place-items: center;
  width: 52px;
  height: 52px;
  border-radius: 8px;
  color: #0b2f2a;
  background: #f5c451;
  font-size: 28px;
  font-weight: 900;
}

.login-stage__name {
  font-size: 22px;
  font-weight: 900;
  text-shadow: 0 1px 12px rgba(0, 0, 0, 0.28);
}

.login-stage__sub {
  margin-top: 4px;
  color: rgba(255, 255, 255, 0.82);
  font-size: 13px;
  text-shadow: 0 1px 10px rgba(0, 0, 0, 0.32);
}

.login-stage__chain {
  position: relative;
  z-index: 1;
  display: grid;
  grid-template-columns: auto 1fr auto 1fr auto 1fr auto;
  align-items: center;
  gap: 10px;
}

.login-stage__chain span {
  padding: 10px 14px;
  border-radius: 8px;
  color: #0b2f2a;
  background: rgba(235, 250, 246, 0.9);
  box-shadow: 0 8px 24px rgba(3, 28, 24, 0.14);
  backdrop-filter: blur(6px);
  font-size: 13px;
  font-weight: 900;
}

.login-stage__chain i {
  height: 1px;
  background: rgba(255, 255, 255, 0.7);
}

.login-panel {
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: 48px 72px;
}

.login-panel__head {
  margin-bottom: 24px;
}

.login-form {
  display: grid;
  gap: 2px;
  margin-bottom: 18px;
}

.login-button {
  width: 100%;
}

</style>
bx 40000 accounts O q4_2(
40000 details T#pk\Z"40000 manager 46y"m8>kӹ40000 redemptions ,P/+)40000 reward-detail CpRka'5t40000 rewards ,P/5ɷû=N40000 vouchers xrFH wSnz?ex% 100644 index.vue fFg= YʞdSXgVx% 100644 index.vue |d-GȑK%	ZVx$TBxDETAIL(积分明细 P键词、账号、状态和日期查询流水，筛选动作统一通过接口Cnaccounts')">家长积分账户!;4:icon="Download" @click="exportRows">导出当前页0I事项4R账号ID ?filters.userId" clearable placeholder="可选" style="width: 17S.status" style="width: 14LY可用5(T:0日期">
          <el-date-picker
            v-model="dateRange"
            type="daterange"
            value-format="YYYY-MM-DD"
            start-placeholder="开始日期"
            end-placeholder="结束日期"
            style="width: 260px"

         1ɓn1id"	!#createdAt" label="日期" width="12)Name" label="家长" width="14%#title" label="事项" min-width="24积分" width="119span :class="['amount', row.amount < 0 && 'amount--minus']">{{ row.amount > 0 ? '+' : '' }}{{ row.amount }}</span>
          </	D status" label="状态" width="129Ael-tag effect="plain" round>{{ statusLabel(row.status) }}</el-tag	R'userId" label="账号 ID" width="170" /JAppCard class="expiry-card">
      <div class="table-head">
        <div>
(section-title">积分过期批次</div>
=section-subtitle">{{ result.expiryRule?.description }}</div>
	R<el-tag effect="plain">{{ result.expiryRule?.validDays || 180 }} 天有效</el-tagQ!expiry-grid">
        <div v-for="item in result.expirySchedules" :key="item.id" class="expiry-item">
          <span>{{ item.umserName }}</span>
          <strong>{{ item.points }} 积分</strong>
          <em>{{ item.expireAt }}</em>
	IA DownloadhhlistPointTransaction]dateRange = ref([].:userId: '',
  status: 'all',
  startDate: '',
  endDate: '0f(  expiryRule: {},
  expirySchedules: [],2_筛选流水', value: result.total || 0, hint: '当前条件命中' },
  { label: '发放积分'Gincome || 0), hint: '正向奖励与补发' },
  { label: '消耗积分*(ense || 0), hint: '兑换与冲正消耗净额'L需后续确认Zfunction statusLabel(status) {
  return status === 'pending' ? '待到账' : '可用'
}

function syncDateRange() {
  const [start, end] = dateRange.value || []
  filters.startDate = start || ''
  filters.endDate = end || ''
}

async function load() {
  syncDateRange()O@TransactionuserId = ''
  filters.s_$startDate = ''
  filters.endDate = 'kdateRange.value = []
  load()
}

function exportRows() {
  const rows = result.list
  if (!rows.length) {
    ElMessage.warning('当前页没有可导出的明细')
    return
  }
  const headers = ['日期', '家长', '账号ID', '事项', '积分', '态']
  const body = rows.map((item) => [item.createdAt, item.userName, item.userId, item.title, item.amount, statusLabel(item.status)])
  const csv = [headers, ...body]
    .map((row) => row.map((cell) => `"${String(cell ?? '').replaceAll('"', '""')}"`).join(','))
    .join('\n')
  const blob = new Blob([`\ufeff${csv}`], { type: 'text/csv;charset=utf-8;' })
  const url = URL.createObjectURL(blob)
  const link = document.createElement('a')
  link.href = url
  link.download = `积分明细_${new Date().KtoISOString().slice(0, 10)}.csv`
  link.click()
  URL.revokeObjectURL(url)
O$当前页明细已导出')$;M%8%4~%	,
.expiry%/	,
.expiry&
table-heado'#Vflex-start;
  justify-content: space-between;
  margin-bottom: 16px;
}

.expiry-grid {'"%'3, 1fr);
  gap: 12px;
}

.expiry-item {'gap: 6px;
  padding: 14px;
  border: 1px solid #dbe7dc;
  border-radius: 8px;
  background: #f7faf3;
}

.expiry-item span {
  c(#P}

.expiry-item strong {
  color: #ff7a59;
  font-size: 18px;
}

.expiry-item em%#.2px;
  font-style: normal;
}

.section-title {(& 19J&"section-subtitle {
  margin-top: 5d)$"3px;
}

.amount {
  color: #0b4f46L& amount--minus {
  color: #ff7a59)[|{֥x% 100644 index.vue !V{;DX"bSӋ#_¹x,<template>
  <div class="page-shell">
    <div class="page-head">
      <div>
        <div class="page-kicker">POINTS / OVERVIEW</div>
        <div class="page-title">积分概览</div>
        <div class="page-subtitle">把积分账户、流水、兑换权益、审核和核销拆成独立工作台，避免大数据量页面混在一起。</div>
      </div>
      <div class="head-actions">
        <el-button @click="router.push('/points/accounts')">家长积分账户</el-button>
        <el-button @click="router.push('/points/rewards')">兑换权益</el-button>
        <el-button color="#0b4f46" :icon="Refresh" :loading="loading" @click="load">刷新概览</el-button>
      </div>
    </div>

    <div class="growth-grid metric-grid">
      <AppCard v-for="item in metrics" :key="item.label" class="metric-card">
        <div class="metric-card__label">{{ item.label }}</div>
        <div class="metric-card__value">{{ item.value }}</div>
        <div class="metric-card__hint">{{ item.hint }}</div>
      </AppCard>
    </div>

    <div class="workspace-grid">
      <AppCard v-for="entry in entries" :key="entry.path" class="entry-card" @click="router.push(entry.path)">
        <div class="entry-card__icon">
          <el-icon><component :is="entry.icon" /></el-icon>
        </div>
        <div>
          <div class="entry-card__title">{{ entry.title }}</div>
          <div class="entry-card__desc">{{ entry.desc }}</div>
        </div>
      </AppCard>
    </div>

    <div class="points-layout">
      <AppCard class="rule-card" dark>
        <div class="rule-card__head">
          <div>
            <div class="rule-card__title">推荐奖励规则</div>
            <div class="rule-card__sub">朋友圈 / 微信推荐后的积分流向</div>
          </div>
          <el-button v-hasPermi="['points:rule:edit', 'points:account:adjust']" size="small" color="#f5c451" :icon="Edit" @click="openRuleDialog">编辑规则</el-button>
        </div>
        <div v-for="rule in manager.rules" :key="rule.event" class="rule-row">
          <span>{{ rule.title }}</span>
          <strong>推荐人 +{{ rule.inviter }} / 被推荐人 +{{ rule.invitee }}</strong>
        </div>
      </AppCard>

      <AppCard class="expiry-card">
        <div class="expiry-card__head">
          <div>
            <div class="section-title">积分过期说明</div>
            <div class="section-subtitle">{{ manager.expiryRule?.description }}</div>
          </div>
          <div class="expiry-card__days">
            <strong>{{ manager.expiryRule?.validDays || 180 }}</strong>
            <span>天有效</span>
          </div>
        </div>
        <div class="expiry-note">{{ manager.expiryRule?.reminder }}</div>
      </AppCard>
    </div>

    <el-dialog v-model="ruleDialogVisible" title="编辑推荐积分流向" width="680px">
      <div class="rule-editor-list">
        <div v-for="row in ruleForm.rows" :key="row.event" class="rule-editor-row">
          <div class="rule-editor-row__meta">
            <strong>{{ row.title || eventName(row.event) }}</strong>
            <span>{{ eventHint(row.event) }}</span>
          </div>
          <el-input v-model="row.title" placeholder="规则名称" />
          <el-input-number v-model="row.inviter" :min="0" :step="10" controls-position="right" />
          <el-input-number v-model="row.invitee" :min="0" :step="10" controls-position="right" />
        </div>
      </div>
      <div class="rule-editor-legend">
        <span></span>
        <span>规则名称</span>
        <span>推荐人获得</span>
        <span>新家长获得</span>
      </div>
      <template #footer>
        <el-button @click="ruleDialogVisible = false">取消</el-button>
        <el-button color="#0b4f46" :loading="ruleSaving" @click="saveRules">保存规则</el-button>
      </template>
    </el-dialog>
  </div>
</template>

<script setup>
import { computed, onMounted, reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { Check, Coin, Edit, Refresh, Tickets } from '@element-plus/icons-vue'
import AppCard from '@/components/AppCard/index.vue'
import { getPointManager, updateReferralRule } from '@/api/points/manager'

const router = useRouter()
const loading = ref(false)
const ruleSaving = ref(false)
const ruleDialogVisible = ref(false)
const manager = reactive({
  overview: {},
  expiryRule: {},
  rules: [],
})
const ruleForm = reactive({
  rows: [],
})

const entries = [
  { title: '家长积分账户', desc: '分页查询家长积分余额，支持弹窗调整积分。', path: '/points/accounts', icon: Coin },
  { title: '积分明细', desc: '按家长、状态、日期和关键词查询流水并导出。', path: '/points/details', icon: Tickets },
  { title: '兑换权益', desc: '维护可兑换权益、库存和详情说明。', path: '/points/rewards', icon: Tickets },
  { title: '兑换审核', desc: '审核家长兑换申请，通过后生成券码。', path: '/points/redemptions', icon: Check },
  { title: '券码核销', desc: '按券码或列表完成学习权益核销。', path: '/points/vouchers', icon: Check },
]

const metrics = computed(() => [
  { label: '可用积分池', value: formatNumber(manager.overview.available || 0), hint: `${manager.overview.parents || 0} 个家长账户` },
  { label: '待到账', value: formatNumber(manager.overview.pending || 0), hint: '到课确认后可转入' },
  { label: '将过期', value: formatNumber(manager.overview.expiring || 0), hint: manager.overview.nextExpireAt ? `最早 ${manager.overview.nextExpireAt}` : '需运营提醒兑换' },
  { label: '待处理兑换', value: manager.overview.pendingRedemptions || 0, hint: `${manager.overview.issuedVouchers || 0} 张券待核销` },
])

function formatNumber(value) {
  return Number(value).toLocaleString('zh-CN')
}

function eventName(event) {
  const names = {
    registered: '新家长注册',
    booked: '预约试听',
    attended: '确认到课',
    purchased: '报名正价课',
  }
  return names[event] || event
}

function eventHint(event) {
  const hints = {
    registered: '新用户通过邀请码完成登录后发放',
    booked: '被推荐人预约试听课程后发放',
    attended: '校区确认到课后发放',
    purchased: '报名正价课成交后发放',
  }
  return hints[event] || '推荐链路奖励阶段'
}

function openRuleDialog() {
  ruleForm.rows = (manager.rules || []).map((rule) => ({
    event: rule.event,
    title: rule.title,
    inviter: Number(rule.inviter || 0),
    invitee: Number(rule.invitee || 0),
  }))
  ruleDialogVisible.value = true
}

async function saveRules() {
  const invalid = ruleForm.rows.find((row) => !String(row.title || '').trim())
  if (invalid) {
    ElMessage.warning('请填写完整的规则名称')
    return
  }
  ruleSaving.value = true
  try {
    await Promise.all(ruleForm.rows.map((row) => updateReferralRule(row.event, {
      title: String(row.title || '').trim(),
      inviter: Number(row.inviter || 0),
      invitee: Number(row.invitee || 0),
    })))
    ElMessage.success('推荐积分规则已保存')
    ruleDialogVisible.value = false
    await load()
  } finally {
    ruleSaving.value = false
  }
}

async function load() {
  loading.value = true
  try {
    const data = await getPointManager()
    Object.assign(manager, data)
  } finally {
    loading.value = false
  }
}

onMounted(load)
</script>

<style lang="scss" scoped>
.metric-grid {
  grid-template-columns: repeat(4, 1fr);
}

.head-actions {
  display: flex;
  gap: 10px;
}

.metric-card,
.entry-card,
.rule-card,
.expiry-card {
  padding: 20px;
}

.metric-card__label {
  color: #72817b;
  font-size: 13px;
  font-weight: 800;
}

.metric-card__value {
  margin-top: 10px;
  color: #0b2f2a;
  font-size: 30px;
  font-weight: 900;
}

.metric-card__hint {
  margin-top: 8px;
  color: #7c8a84;
  font-size: 12px;
}

.workspace-grid {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 16px;
  margin-top: 16px;
}

.entry-card {
  display: grid;
  grid-template-columns: 42px minmax(0, 1fr);
  gap: 14px;
  cursor: pointer;
  transition: transform 0.18s ease, border-color 0.18s ease;
}

.entry-card:hover {
  border-color: #9ae9dc;
  transform: translateY(-2px);
}

.entry-card__icon {
  display: grid;
  place-items: center;
  width: 42px;
  height: 42px;
  border-radius: 8px;
  color: #0b2f2a;
  background: #f5c451;
  font-size: 22px;
}

.entry-card__title {
  color: #0b2f2a;
  font-size: 16px;
  font-weight: 900;
}

.entry-card__desc {
  margin-top: 6px;
  color: #72817b;
  font-size: 12px;
  line-height: 1.45;
}

.points-layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 320px;
  align-items: stretch;
  gap: 16px;
  margin-top: 16px;
}

.rule-card,
.expiry-card {
  height: 100%;
}

.rule-card__title {
  color: #9ae9dc;
  font-size: 13px;
  font-weight: 900;
}

.rule-card__head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 14px;
}

.rule-card__sub {
  margin-top: 6px;
  color: rgba(255, 255, 255, 0.58);
  font-size: 12px;
}

.rule-row {
  display: grid;
  gap: 6px;
  margin-top: 16px;
  padding: 14px;
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 8px;
  background: rgba(255, 255, 255, 0.08);
}

.rule-row span {
  color: #ffffff;
  font-weight: 900;
}

.rule-row strong {
  color: #f5c451;
  font-size: 13px;
}

.section-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  margin-bottom: 16px;
}

.section-title {
  color: #0b2f2a;
  font-size: 19px;
  font-weight: 900;
}

.section-subtitle {
  margin-top: 5px;
  color: #72817b;
  font-size: 13px;
  line-height: 1.5;
}

.expiry-card {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  padding: 16px;
}

.expiry-card__head {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 72px;
  gap: 14px;
}

.expiry-card__days {
  display: grid;
  place-items: center;
  width: 72px;
  height: 72px;
  border-radius: 8px;
  color: #0b2f2a;
  background: #f5c451;
}

.expiry-card__days strong,
.expiry-card__days span {
  display: block;
  line-height: 1;
}

.expiry-card__days strong {
  font-size: 24px;
  font-weight: 900;
}

.expiry-card__days span {
  margin-top: 5px;
  font-size: 12px;
  font-weight: 900;
}

.expiry-note {
  margin-top: 12px;
  padding: 12px 14px;
  border-radius: 8px;
  color: #0b4f46;
  background: #dff8ee;
  font-size: 13px;
  font-weight: 800;
}

.rule-editor-list {
  display: grid;
  gap: 12px;
}

.rule-editor-row {
  display: grid;
  grid-template-columns: minmax(150px, 1fr) minmax(150px, 1fr) 130px 130px;
  align-items: center;
  gap: 12px;
  padding: 12px;
  border: 1px solid #dfe8dc;
  border-radius: 8px;
  background: #f7faf4;
}

.rule-editor-row__meta {
  display: grid;
  gap: 4px;
}

.rule-editor-row__meta strong {
  color: #0b2f2a;
  font-size: 14px;
  font-weight: 900;
}

.rule-editor-row__meta span {
  color: #72817b;
  font-size: 12px;
  line-height: 1.4;
}

.rule-editor-legend {
  display: grid;
  grid-template-columns: minmax(150px, 1fr) minmax(150px, 1fr) 130px 130px;
  gap: 12px;
  margin-top: 12px;
  padding: 0 12px;
  color: #72817b;
  font-size: 12px;
  font-weight: 800;
}
</style>
Px% 100644 index.vue -4$:9a*bE|`_O_x+H<template>
  <div class="page-shell">
    <div class="page-head">
      <div>
        <div class="page-kicker">POINTS / VOUCHER</div>
        <div class="page-title">券码核销</div>
        <div class="page-subtitle">审核通过后的学习权益会生成券码，校区前台或运营可在这里完成核销留痕。</div>
      </div>
      <div class="head-actions">
        <el-button @click="router.push('/points/redemptions')">兑换审核</el-button>
        <el-button color="#0b4f46" :icon="Refresh" :loading="loading" @click="load">刷新</el-button>
      </div>
    </div>

    <AppCard class="verify-panel" dark>
      <div>
        <div class="verify-panel__kicker">VERIFY DESK</div>
        <div class="verify-panel__title">输入券码完成核销</div>
        <div class="verify-panel__sub">核销后状态会变为已核销，家长端同步看到权益已使用。</div>
      </div>
      <div class="verify-box">
        <el-input v-model="quickCode" size="large" placeholder="输入或粘贴券码，如 LFH-XXXX-XXXX" clearable @keyup.enter="openVerifyByCode" />
        <el-button size="large" color="#f5c451" :icon="Check" @click="openVerifyByCode">核销</el-button>
      </div>
    </AppCard>

    <div class="growth-grid metric-grid">
      <AppCard v-for="item in metrics" :key="item.label" class="metric-card">
        <div class="metric-card__label">{{ item.label }}</div>
        <div class="metric-card__value">{{ item.value }}</div>
        <div class="metric-card__hint">{{ item.hint }}</div>
      </AppCard>
    </div>

    <AppCard class="voucher-card">
      <div class="filter-strip">
        <el-input v-model="filters.keyword" :prefix-icon="Search" placeholder="搜索券码、家长、手机号或权益" clearable @keyup.enter="query" />
        <el-select v-model="filters.voucherStatus" placeholder="券码状态">
          <el-option label="全部券码" value="all" />
          <el-option label="待核销" value="issued" />
          <el-option label="已核销" value="used" />
          <el-option label="已作废" value="void" />
          <el-option label="已过期" value="expired" />
        </el-select>
        <el-button color="#0b4f46" :icon="Search" @click="query">查询</el-button>
        <el-button @click="reset">重置</el-button>
        <el-button @click="exportCsv">导出</el-button>
      </div>

      <el-table :data="result.list" v-loading="loading" row-key="id">
        <el-table-column label="券码" min-width="180">
          <template #default="{ row }">
            <el-tag v-if="row.voucherCode" class="code-tag" effect="dark">{{ row.voucherCode }}</el-tag>
            <span v-else class="muted">未发券</span>
          </template>
        </el-table-column>
        <el-table-column label="权益" min-width="220">
          <template #default="{ row }">
            <div class="reward-cell">
              <strong>{{ row.reward?.name || row.itemId }}</strong>
              <span>{{ row.reward?.type || '-' }} · {{ row.costPoints }} 积分</span>
            </div>
          </template>
        </el-table-column>
        <el-table-column label="家长" min-width="160">
          <template #default="{ row }">
            <div class="parent-cell">
              <strong>{{ row.user?.name || row.userId }}</strong>
              <span>{{ row.user?.phone || '-' }}</span>
            </div>
          </template>
        </el-table-column>
        <el-table-column label="状态" width="120">
          <template #default="{ row }">
            <el-tag :type="voucherTag(row.voucherStatus)" effect="plain" round>{{ row.voucherStatusLabel }}</el-tag>
          </template>
        </el-table-column>
        <el-table-column prop="expireAt" label="有效期至" width="160" />
        <el-table-column prop="usedAt" label="核销时间" width="160">
          <template #default="{ row }">{{ row.usedAt || '-' }}</template>
        </el-table-column>
        <el-table-column label="操作" width="120" fixed="right">
          <template #default="{ row }">
            <el-button v-if="row.voucherStatus === 'issued'" v-hasPermi="['points:voucher:verify']" size="small" color="#0b4f46" @click="openVerify(row)">核销</el-button>
            <span v-else class="muted">不可核销</span>
          </template>
        </el-table-column>
      </el-table>
      <div class="pager">
        <el-pagination
          v-model:current-page="filters.pageNum"
          v-model:page-size="filters.pageSize"
          :page-sizes="[10, 20, 50, 100]"
          layout="total, sizes, prev, pager, next, jumper"
          :total="result.total"
          @size-change="load"
          @current-change="load"
        />
      </div>
    </AppCard>

    <el-dialog v-model="verifyVisible" title="确认核销券码" width="430px">
      <div class="verify-summary">
        <div>
          <span>券码</span>
          <strong>{{ verifyForm.code }}</strong>
        </div>
        <div v-if="currentRow">
          <span>权益</span>
          <strong>{{ currentRow.reward?.name || currentRow.itemId }}</strong>
        </div>
      </div>
      <el-form label-width="76px">
        <el-form-item label="核销备注">
          <el-input v-model="verifyForm.note" type="textarea" :rows="3" placeholder="如：前台核销 / 老师确认交付" />
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="verifyVisible = false">取消</el-button>
        <el-button color="#0b4f46" :loading="saving" @click="submitVerify">确认核销</el-button>
      </template>
    </el-dialog>
  </div>
</template>

<script setup>
import { computed, onMounted, reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Check, Refresh, Search } from '@element-plus/icons-vue'
import AppCard from '@/components/AppCard/index.vue'
import { listRedemptions, verifyVoucher } from '@/api/points/redemption'

const router = useRouter()
const loading = ref(false)
const saving = ref(false)
const verifyVisible = ref(false)
const quickCode = ref('')
const currentRow = ref(null)
const filters = reactive({
  keyword: '',
  voucherStatus: 'issued',
  pageNum: 1,
  pageSize: 10,
})
const verifyForm = reactive({
  id: '',
  code: '',
  note: '现场核销',
})
const result = reactive({
  list: [],
  total: 0,
  overview: {},
})

const metrics = computed(() => [
  { label: '待核销券码', value: result.overview.issuedVouchers || 0, hint: '已发券但未使用' },
  { label: '已核销', value: result.overview.used || 0, hint: '完成学习权益交付' },
  { label: '已驳回/作废', value: result.overview.rejected || 0, hint: '已退还或不可使用' },
  { label: '筛选结果', value: result.total || 0, hint: '当前券码列表' },
])

async function load() {
  loading.value = true
  try {
    const data = await listRedemptions(filters)
    Object.assign(result, data)
  } finally {
    loading.value = false
  }
}

function query() {
  filters.pageNum = 1
  load()
}

function reset() {
  filters.keyword = ''
  filters.voucherStatus = 'issued'
  filters.pageNum = 1
  load()
}

function voucherTag(status) {
  const map = {
    issued: 'success',
    used: 'info',
    void: 'danger',
    expired: 'warning',
    not_issued: '',
  }
  return map[status] || ''
}

function openVerify(row) {
  currentRow.value = row
  verifyForm.id = row.id
  verifyForm.code = row.voucherCode
  verifyForm.note = '现场核销'
  verifyVisible.value = true
}

function openVerifyByCode() {
  const code = quickCode.value.trim().toUpperCase()
  if (!code) {
    ElMessage.warning('请输入券码')
    return
  }
  currentRow.value = result.list.find((item) => item.voucherCode === code) || null
  verifyForm.id = ''
  verifyForm.code = code
  verifyForm.note = '现场核销'
  verifyVisible.value = true
}

async function submitVerify() {
  await ElMessageBox.confirm(`确认核销券码 ${verifyForm.code}？核销后不可重复使用。`, '核销确认', {
    confirmButtonText: '确认核销',
    cancelButtonText: '取消',
    type: 'warning',
  })
  saving.value = true
  try {
    await verifyVoucher({
      id: verifyForm.id,
      code: verifyForm.code,
      note: verifyForm.note,
    })
    ElMessage.success('券码已核销')
    verifyVisible.value = false
    quickCode.value = ''
    await load()
  } finally {
    saving.value = false
  }
}

function exportCsv() {
  const rows = [
    ['券码', '权益', '类型', '家长', '手机号', '状态', '有效期至', '核销时间'],
    ...result.list.map((item) => [
      item.voucherCode || '',
      item.reward?.name || item.itemId,
      item.reward?.type || '',
      item.user?.name || item.userId,
      item.user?.phone || '',
      item.voucherStatusLabel,
      item.expireAt || '',
      item.usedAt || '',
    ]),
  ]
  const csv = rows.map((row) => row.map((cell) => `"${String(cell).replace(/"/g, '""')}"`).join(',')).join('\n')
  const blob = new Blob([`\uFEFF${csv}`], { type: 'text/csv;charset=utf-8;' })
  const link = document.createElement('a')
  link.href = URL.createObjectURL(blob)
  link.download = '券码核销.csv'
  link.click()
  URL.revokeObjectURL(link.href)
}

onMounted(load)
</script>

<style lang="scss" scoped>
.head-actions {
  display: flex;
  gap: 10px;
}

.verify-panel {
  display: grid;
  grid-template-columns: minmax(0, 0.95fr) minmax(360px, 1fr);
  align-items: center;
  gap: 24px;
  padding: 24px;
}

.verify-panel__kicker {
  color: #9ae9dc;
  font-size: 12px;
  font-weight: 900;
}

.verify-panel__title {
  margin-top: 8px;
  color: #ffffff;
  font-size: 24px;
  font-weight: 900;
}

.verify-panel__sub {
  margin-top: 8px;
  color: rgba(255, 255, 255, 0.66);
  font-size: 13px;
}

.verify-box {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 112px;
  gap: 10px;
}

.metric-grid {
  grid-template-columns: repeat(4, 1fr);
  margin-top: 16px;
}

.metric-card,
.voucher-card {
  padding: 20px;
}

.metric-card__label {
  color: #72817b;
  font-size: 13px;
  font-weight: 800;
}

.metric-card__value {
  margin-top: 10px;
  color: #0b2f2a;
  font-size: 30px;
  font-weight: 900;
}

.metric-card__hint {
  margin-top: 8px;
  color: #7c8a84;
  font-size: 12px;
}

.voucher-card {
  margin-top: 16px;
}

.filter-strip {
  display: grid;
  grid-template-columns: minmax(300px, 1fr) 150px auto auto auto;
  gap: 10px;
  margin-bottom: 16px;
}

.pager {
  display: flex;
  justify-content: flex-end;
  margin-top: 16px;
}

.code-tag {
  letter-spacing: 0;
}

.reward-cell,
.parent-cell {
  display: grid;
  gap: 5px;
}

.reward-cell strong,
.parent-cell strong {
  color: #17312d;
  font-weight: 900;
}

.reward-cell span,
.parent-cell span,
.muted {
  color: #72817b;
  font-size: 12px;
}

.verify-summary {
  display: grid;
  gap: 10px;
  margin-bottom: 16px;
}

.verify-summary div {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 12px 14px;
  border-radius: 8px;
  background: #eef8f4;
}

.verify-summary span {
  color: #72817b;
  font-size: 12px;
  font-weight: 800;
}

.verify-summary strong {
  color: #0b4f46;
}
</style>
2ExIWQx
REDEMPTION换审核g长提交学习权益兑换后，先由运营审核；通过后系统生成券码，进入待核销池?hvouchers')">券码
	[_review-cardQe#长、手机号、权益或券码H"status" placeholder="审核状态"
部状怓s1审核" value="pending_review$发券" value="approv&驳回" value="reject&7K<	*type" placeholder="权益类型" clearable部类型t-!课程权益" value="课程权益!!服务权益" value="服务权益!抵扣券" value="抵扣券!测评" value="测评aQ换人" min-width="179(T__avatar">{{ (row.user?.name || '家').slice(0, 1) }}</div>
              <div>
    #__name"%	div>
    #__child"div>
  ,6Z兑换
gXstatusTag(row.s$s+a
	7@
|审核通过后生成
R$prop="createdAt" label="提交时间*
9G	gv-if="row.status === 'pending_review'">
              <el-button v-hasPermi="['points:redemption:review9!Check" @click="approve(row)">通过</el-button>
              <el-button v-hasPermi="['points:redemption:review']" size="small"= :icon="Close" @click="openReject(row)">驳回</el-button>
      <el-button v-else%size="small"vouchers')">去!	已处理
?(rejectVisible" title="驳回兑换申请el-alert title="驳回后会自动退还本次兑换消耗的积分，并将券码作废。" type="warning" show-icon :closable.="false" />
      <el-form class="reject-form"2驳回原因">
  `rejectForm.reason/'资料不完整 / 权益暂不可兑换breject<gReject">确认驳回0	Closeq4approveRedemption, listRedemptions, rejectRedemption@yrejectC&status: 'pending_review',
  type: 'allM*#rejectForm = reactive({
  reason: '|审核Qpending_review || 0, hint: '需要运营确认后发券' },
  { label: '已发券approved || 0, hint: `${L#} 张待核销`F完成e6条件下的申请数(0status = 'pending_review'
  filters.type = 'all'x,status"Ipending_review: 'warning',
    approved: 'success',
    rejected: 'dangerLexpired: 'info%async function approve(row(通过“${row.reward?.name || row.itemId}”兑换申请？通过后会生成券码。`, '审核确认', {
    confirmButtonTe通过并发券?await approveRedemption(row.id, { validDays: 30 })
  ElMessage.success('已通过审核并生成券码')
  await load()
}

funcopenReject(row) {$ow
  rejectForm.reason = ''
  reject_*Reject() {
  if (!currentRow.value) return)VrejectRedemption(currentRow.value.id, { reason: rejectForm.reason || '审核未通过'\ 9驳回并退还积分')
    rejectVisible.value = false oQ换人', '手机号', '权益', '类型', '积分', '状态', '券码', '提交!8!"D!GcostPoints,
      item.sm"!	createdAt"5换审核#p&7}
Y$3&card,
.review'%review.(`260px, 1fr) 150px(parent-cell*-"gap: 10px;
}

.parent-cell__avatar@*0place-items: center;
  width: 36px;
  height: 36*%color: #0b2f2a;
  background: #f5c451)parent-cell__name,
.reward-cell+) parent-cell__child,
.reward)>reward^))&)&ject-form {)	</style>
ex% 100644 index.vue N[v%Fյ۹n#7ދx(<template>
  <div class="page-shell">
    <div class="page-head">
      <div>
        <div class="page-kicker">POINTS / REWARD DETAIL</div>
        <div class="page-title">兑换权益详情</div>
        <div class="page-subtitle">查看权益说明、库存状态和最近兑换记录。</div>
      </div>
      <div class="head-actions">
        <el-button @click="router.back()">返回</el-button>
        <el-button color="#0b4f46" :icon="Refresh" :loading="loading" @click="load">刷新</el-button>
      </div>
    </div>

    <div class="detail-grid">
      <AppCard class="detail-card">
        <div class="detail-card__type">{{ detail.reward.type || '-' }}</div>
        <div class="detail-card__name">{{ detail.reward.name || '-' }}</div>
        <div class="detail-card__desc">{{ detail.reward.description || '暂无详情说明' }}</div>
        <div class="detail-stats">
          <div>
            <span>所需积分</span>
            <strong>{{ detail.reward.cost || 0 }}</strong>
          </div>
          <div>
            <span>库存</span>
            <strong>{{ detail.reward.stock || 0 }}</strong>
          </div>
          <div>
            <span>兑换状态</span>
            <strong>{{ detail.reward.stock > 0 ? '可兑换' : '已售罄' }}</strong>
          </div>
        </div>
      </AppCard>

      <AppCard class="side-card" dark>
        <div class="side-card__title">使用提示</div>
        <div class="side-card__line">库存为 0 时，小程序端会阻止家长继续兑换。</div>
        <div class="side-card__line">审核通过后会生成券码，券码在核销页完成交付确认。</div>
      </AppCard>
    </div>

    <AppCard class="table-card">
      <div class="table-head">
        <div>
          <div class="section-title">最近兑换</div>
          <div class="section-subtitle">最多展示最近 10 条兑换申请</div>
        </div>
      </div>
      <el-table :data="detail.redemptions" v-loading="loading" row-key="id">
        <el-table-column label="家长" width="150">
          <template #default="{ row }">{{ row.user?.name || row.userId }}</template>
        </el-table-column>
        <el-table-column label="状态" width="120">
          <template #default="{ row }">
            <el-tag effect="plain" round>{{ row.statusLabel }}</el-tag>
          </template>
        </el-table-column>
        <el-table-column label="券码" min-width="180">
          <template #default="{ row }">{{ row.voucherCode || '-' }}</template>
        </el-table-column>
        <el-table-column prop="createdAt" label="提交时间" width="160" />
        <el-table-column prop="expireAt" label="有效期至" width="160" />
      </el-table>
    </AppCard>
  </div>
</template>

<script setup>
import { onMounted, reactive, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { Refresh } from '@element-plus/icons-vue'
import AppCard from '@/components/AppCard/index.vue'
import { getRewardDetail } from '@/api/points/manager'

const route = useRoute()
const router = useRouter()
const loading = ref(false)
const detail = reactive({
  reward: {},
  redemptions: [],
})

async function load() {
  loading.value = true
  try {
    const data = await getRewardDetail(route.params.id)
    Object.assign(detail, data)
  } finally {
    loading.value = false
  }
}

onMounted(load)
</script>

<style lang="scss" scoped>
.head-actions {
  display: flex;
  gap: 10px;
}

.detail-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 360px;
  gap: 16px;
}

.detail-card,
.side-card,
.table-card {
  padding: 22px;
}

.detail-card__type {
  color: #ff7a59;
  font-size: 12px;
  font-weight: 900;
}

.detail-card__name {
  margin-top: 10px;
  color: #0b2f2a;
  font-size: 28px;
  font-weight: 900;
}

.detail-card__desc {
  margin-top: 14px;
  color: #50615a;
  font-size: 14px;
  line-height: 1.7;
}

.detail-stats {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 12px;
  margin-top: 22px;
}

.detail-stats div {
  padding: 16px;
  border-radius: 8px;
  background: #eef8f4;
}

.detail-stats span,
.detail-stats strong {
  display: block;
}

.detail-stats span {
  color: #72817b;
  font-size: 12px;
  font-weight: 800;
}

.detail-stats strong {
  margin-top: 8px;
  color: #0b4f46;
  font-size: 20px;
  font-weight: 900;
}

.side-card__title {
  color: #9ae9dc;
  font-size: 13px;
  font-weight: 900;
}

.side-card__line {
  margin-top: 14px;
  padding: 14px;
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 8px;
  color: rgba(255, 255, 255, 0.78);
  background: rgba(255, 255, 255, 0.08);
  font-size: 13px;
  line-height: 1.55;
}

.table-card {
  margin-top: 16px;
}

.table-head {
  margin-bottom: 16px;
}

.section-title {
  color: #0b2f2a;
  font-size: 19px;
  font-weight: 900;
}

.section-subtitle {
  margin-top: 5px;
  color: #72817b;
  font-size: 13px;
}
</style>
W?f x% 100644 index.vue qp[:%DsTV];x~TGxREWARD)换权益 subtitle">维护学习权益的所需积分、库存和详情说明；库存为 0 时家长端不可兑换。</div>
      </div^6Lreward:edit']" color="#0b4f46" :icon="Plus" @click="openCreate">新增权益
redemptions')">兑换审核!h权益名称 / 类型 / 说明e型)type" style="width: 15L*类型v/$课程权益" value="课程权益" /P $服务权益" value="服务权益" /P 抵扣券" value="抵扣券" /P 测评" value="测评EI0	存)stockStatus" style="width: 14L]存" value="in_stock" /P 无库存" value="sold_outEn1id"	益" min-width="24Dreward-cell">
              <strong>{{ row.name }}</strong>
              <span>{{ row.description || '未填写详情说明' }}</span	etype" label="类型" width="12%$cost" label="所需积分" width="12库存" width="129`el-tag :type="row.stock > 0 ? 'success' : 'danger'" effect="plain" round>{{ row.stock }}</el-tag	L兑换状态" width="139el-tag :type="row.stock > 0 ? 'success' : 'info'" round>{{ row.stock > 0 ? '可兑换' : '已售罄' }}</el-tag>
          </te	<8QEsize="small" @click="router.push(`/points/rewards/${row.id}`)">详情
reward:edi*.Edit(row)">编辑	/editVisible" :title="dialogMode === 'create' ? '新增兑换权益' : '编辑兑换权益'" width="560px">
      <el-form label,权益名称 1editForm.name" placeholder="请输入权益名称;型!editForm.typ Z$课程权益" value="课程权益" /P $服务权益" value="服务权益" /P 抵扣券" value="抵扣券" /P 测评" value="测评EI0积分;editForm.cost" :min="0" :step="50" controls-position="right:库存;editForm.stock" :min="0" :step="1" controls-position="right:详情说明 editForm.description" type="textarea" :rows="4" placeholder="说明兑换范围、核销方式、有效期或限制条件" />
 \editRJEdit">保存0A Plushh'createReward, listRewards, updateRewardvedit1dialogMode = ref('edit')
const currentId = ref(''.type: 'all',
  stock${editForm = reactive({
  name: '',
  type: '课程权益',
  cost: 0,
  stock: 0,
  description: '',
})

function blankRewardFornm() {
  return {
    name: '',
    type: '课程权益',
    cost: 0,
    stock: 1,
    description: '',
  }
}!/权益数量\total}1	有库存\AinStock || 0, hint: '家长端可兑换' },
  { label: '无库存\soldOut || 0, hint: '家长端不可兑换' },
  { label: '当前页', value: result.list.length, hint: `总计 ${result.total || 0} 条` },
])6TRewardtype = 'all'
  filters.stock^>Create() {
  dialogMode.value = 'create'
  currentId.value = ''
  Object.assign(editForm, blankRewardForm())
  editVisible.valu_ Edit(row) {
  dialogMode.value = 'edit'
  currentId.value = row.id
  editForm.name = row.name
  editForm.type = row.type
  editgForm.cost = row.cost
  editForm.stock = row.stock
  editForm.description = row.description || ''
  edit)!-Edi[!(Qif (dialogMode.value === 'create') {
      await createReward({ ...editForm })
  M$b兑换权益已新增')
    } else {
      await updateReward(currentId.value, { ...editForm })
  M$&兑换权益已保存')
    }
    edit$:"$ereward-cell {'"gap: 5px;
}

.reward-cell strong {('}

.reward-cell span {@)(r)(bڥx% 100644 index.vue ,3bxXTB[CNx" 40000 channel j/7jcL8׺ x% 100644 index.vue ];>!tIJףZQӹx&<template>
  <div class="page-shell">
    <div class="page-head">
      <div>
        <div class="page-kicker">STATISTICS / CHANNEL</div>
        <div class="page-title">渠道转化统计</div>
        <div class="page-subtitle">对比朋友圈海报、微信好友、H5 链接和顾问二维码的推荐质量。</div>
      </div>
      <el-button color="#0b4f46" :icon="Refresh" :loading="loading" @click="load">刷新统计</el-button>
    </div>

    <div class="channel-layout">
      <AppCard class="summary-card" dark>
        <div class="summary-card__title">渠道总览</div>
        <div class="summary-card__number">{{ totalLeads }}</div>
        <div class="summary-card__sub">累计推荐线索</div>
        <div class="summary-card__meter">
          <span>到课 {{ totalAttended }}</span>
          <span>报名 {{ totalPurchased }}</span>
        </div>
      </AppCard>

      <AppCard class="table-card">
        <el-table :data="channels" v-loading="loading" row-key="channel">
          <el-table-column prop="channel" label="渠道" min-width="180" />
          <el-table-column prop="leads" label="线索数" width="110" />
          <el-table-column prop="attended" label="到课" width="110" />
          <el-table-column prop="purchased" label="报名" width="110" />
          <el-table-column label="到课率" min-width="190">
            <template #default="{ row }">
              <el-progress :percentage="attendRate(row)" :stroke-width="10" color="#17bebb" />
            </template>
          </el-table-column>
          <el-table-column label="成交率" min-width="190">
            <template #default="{ row }">
              <el-progress :percentage="purchaseRate(row)" :stroke-width="10" color="#ff7a59" />
            </template>
          </el-table-column>
        </el-table>
      </AppCard>
    </div>
  </div>
</template>

<script setup>
import { computed, onMounted, ref } from 'vue'
import { Refresh } from '@element-plus/icons-vue'
import AppCard from '@/components/AppCard/index.vue'
import { getPcOverview } from '@/api/statistics/dashboard'

const loading = ref(false)
const channels = ref([])

const totalLeads = computed(() => channels.value.reduce((sum, item) => sum + item.leads, 0))
const totalAttended = computed(() => channels.value.reduce((sum, item) => sum + item.attended, 0))
const totalPurchased = computed(() => channels.value.reduce((sum, item) => sum + item.purchased, 0))

function attendRate(row) {
  if (!row.leads) return 0
  return Math.round((row.attended / row.leads) * 100)
}

function purchaseRate(row) {
  if (!row.leads) return 0
  return Math.round((row.purchased / row.leads) * 100)
}

async function load() {
  loading.value = true
  try {
    const data = await getPcOverview()
    channels.value = data.channelStats || []
  } finally {
    loading.value = false
  }
}

onMounted(load)
</script>

<style lang="scss" scoped>
.channel-layout {
  display: grid;
  grid-template-columns: 310px 1fr;
  gap: 16px;
}

.summary-card,
.table-card {
  padding: 22px;
}

.summary-card__title {
  color: #9ae9dc;
  font-size: 13px;
  font-weight: 900;
}

.summary-card__number {
  margin-top: 26px;
  color: #f5c451;
  font-size: 58px;
  font-weight: 900;
}

.summary-card__sub {
  color: rgba(255, 255, 255, 0.68);
  font-size: 13px;
}

.summary-card__meter {
  display: grid;
  gap: 10px;
  margin-top: 34px;
}

.summary-card__meter span {
  padding: 12px;
  border-radius: 8px;
  color: #ffffff;
  background: rgba(255, 255, 255, 0.1);
  font-weight: 900;
}
</style>
xA 40000 account eu
nÅ40000 role Ck%
ۉC<x% 100644 index.vue ?+ݽOPbS5c 
xW<template>
  <div class="page-shell">
    <div class="page-head">
      <div>
        <div class="page-kicker">SYSTEM / USER</div>
        <div class="page-title">用户管理</div>
        <div class="page-subtitle">统一核对手机号、微信身份、邀请关系与账号权限。</div>
      </div>
      <div class="head-actions">
        <el-input v-model="filters.keyword" :prefix-icon="Search" clearable placeholder="姓名 / 手机 / 账号 / 微信" @keyup.enter="query" />
        <el-select v-model="filters.role" placeholder="角色">
          <el-option label="全部角色" value="all" />
          <el-option v-for="role in roles" :key="role.value" :label="role.label" :value="role.value" />
        </el-select>
        <el-select v-model="filters.wechatBound" placeholder="微信绑定">
          <el-option label="全部微信状态" value="all" />
          <el-option label="已绑定微信" value="bound" />
          <el-option label="未绑定微信" value="unbound" />
        </el-select>
        <el-select v-model="filters.status" placeholder="状态">
          <el-option label="全部" value="all" />
          <el-option label="启用" value="active" />
          <el-option label="停用" value="disabled" />
        </el-select>
        <el-button color="#0b4f46" :icon="Search" :loading="loading" @click="query">查询</el-button>
        <el-button @click="reset">重置</el-button>
        <el-button v-hasPermi="['system:user:add']" color="#0b4f46" :icon="Plus" @click="openCreate">新增用户</el-button>
      </div>
    </div>

    <AppCard class="table-card">
      <el-table :data="accounts" v-loading="loading" row-key="id">
        <el-table-column prop="name" label="用户" min-width="170">
          <template #default="{ row }">
            <div class="account-cell">
              <el-avatar v-if="row.wechatAvatar" :size="38" :src="row.wechatAvatar" shape="square" />
              <div v-else class="account-cell__avatar">{{ (row.name || '-').slice(0, 1) }}</div>
              <div>
                <div class="account-cell__name">{{ row.name }}</div>
                <div class="account-cell__id">{{ row.username || row.id }}</div>
              </div>
            </div>
          </template>
        </el-table-column>
        <el-table-column prop="phone" label="手机号" width="125" />
        <el-table-column min-width="190">
          <template #header>
            <div class="wechat-column-head">
              <span>微信账号</span>
              <el-tooltip content="微信接口不提供用户自己设置的微信号；这里显示微信昵称和系统唯一标识。" placement="top">
                <el-icon><InfoFilled /></el-icon>
              </el-tooltip>
            </div>
          </template>
          <template #default="{ row }">
            <div v-if="isWechatBound(row)" class="wechat-cell">
              <div class="wechat-cell__main">
                <span class="wechat-cell__dot"></span>
                <strong>{{ row.wechatNickname || '已绑定微信' }}</strong>
              </div>
              <span class="wechat-cell__id">{{ wechatIdentityPreview(row) }}</span>
            </div>
            <div v-else class="wechat-empty">
              <span class="wechat-empty__dot"></span>
              <span>未绑定</span>
            </div>
          </template>
        </el-table-column>
        <el-table-column prop="roleName" label="角色" width="110" />
        <el-table-column label="试听次数" width="130" align="center">
          <template #default="{ row }">
            <div class="quota-cell">
              <strong>{{ row.trialBookingRemaining ?? row.trialBookingLimit ?? 1 }}</strong>
              <span>剩余 / {{ row.trialBookingLimit ?? 1 }}</span>
            </div>
          </template>
        </el-table-column>
        <el-table-column label="邀请关系" min-width="170">
          <template #default="{ row }">
            <div class="invite-cell">
              <span v-if="row.role !== 'parent'" class="invite-cell__muted">不适用</span>
              <template v-else>
                <span v-if="row.invitedBy">被 {{ row.invitedBy.name }} 邀请</span>
                <span v-else class="invite-cell__muted">未记录邀请人</span>
                <el-tag v-if="row.invitedCount" type="success" effect="plain" round>邀请 {{ row.invitedCount }} 人</el-tag>
              </template>
            </div>
          </template>
        </el-table-column>
        <el-table-column label="状态" width="80">
          <template #default="{ row }">
            <StatusTag :status="row.status" />
          </template>
        </el-table-column>
        <el-table-column label="操作" width="210" fixed="right">
          <template #default="{ row }">
            <el-button link type="primary" @click="openDetail(row)">详情</el-button>
            <el-button v-hasPermi="['system:user:edit']" link type="primary" @click="openEdit(row)">编辑</el-button>
            <el-button
              v-hasPermi="['system:user:status']"
              link
              :type="row.status === 'active' ? 'warning' : 'success'"
              @click="toggleStatus(row)"
            >
              {{ row.status === 'active' ? '停用' : '启用' }}
            </el-button>
            <el-button v-hasPermi="['system:user:delete']" link type="danger" @click="remove(row)">删除</el-button>
          </template>
        </el-table-column>
      </el-table>
      <div class="pager">
        <el-pagination
          v-model:current-page="filters.pageNum"
          v-model:page-size="filters.pageSize"
          :page-sizes="[10, 20, 50, 100]"
          layout="total, sizes, prev, pager, next, jumper"
          :total="total"
          @size-change="load"
          @current-change="load"
        />
      </div>
    </AppCard>

    <el-dialog v-model="dialogVisible" :title="dialogTitle" width="720px">
      <el-form v-if="dialogMode !== 'detail'" :model="form" label-width="100px">
        <el-form-item label="姓名">
          <el-input v-model="form.name" placeholder="请输入用户姓名" />
        </el-form-item>
        <el-form-item label="手机号">
          <el-input v-model="form.phone" maxlength="11" placeholder="请输入手机号" />
        </el-form-item>
        <el-form-item label="登录账号">
          <el-input v-model="form.username" placeholder="默认使用手机号" />
        </el-form-item>
        <el-form-item label="登录密码">
          <el-input v-model="form.password" type="password" show-password :placeholder="dialogMode === 'create' ? '可为空' : '不修改请留空'" />
        </el-form-item>
        <el-form-item label="角色">
          <el-select v-model="form.role" :disabled="!canAssignRole || dialogMode === 'detail'" style="width: 100%">
            <el-option v-for="role in roles" :key="role.value" :label="role.label" :value="role.value" />
          </el-select>
        </el-form-item>
        <el-form-item label="状态">
          <el-radio-group v-model="form.status" :disabled="!canChangeStatus || dialogMode === 'detail'">
            <el-radio-button label="active">启用</el-radio-button>
            <el-radio-button label="disabled">停用</el-radio-button>
          </el-radio-group>
        </el-form-item>
        <el-form-item label="试听总额度">
          <div class="quota-field">
            <el-input-number v-model="form.trialBookingLimit" :min="0" :max="99" :step="1" step-strictly />
            <span>次</span>
          </div>
        </el-form-item>
        <template v-if="form.role === 'parent'">
          <el-form-item label="孩子姓名">
            <el-input v-model="form.childName" placeholder="选填" />
          </el-form-item>
          <el-form-item label="孩子年级">
            <el-input v-model="form.childGrade" placeholder="选填" />
          </el-form-item>
          <el-form-item label="关注点">
            <el-input v-model="form.childFocus" placeholder="选填" />
          </el-form-item>
        </template>
      </el-form>
      <template v-if="dialogMode === 'detail'">
        <div class="detail-summary">
          <el-avatar v-if="currentRow?.wechatAvatar" :size="52" :src="currentRow.wechatAvatar" shape="square" />
          <div v-else class="detail-summary__avatar">{{ (currentRow?.name || '-').slice(0, 1) }}</div>
          <div class="detail-summary__main">
            <strong>{{ currentRow?.name }}</strong>
            <span>{{ currentRow?.username || currentRow?.id }}</span>
          </div>
          <el-tag effect="plain">{{ currentRow?.roleName }}</el-tag>
          <StatusTag :status="currentRow?.status" />
        </div>
        <el-divider>登录身份</el-divider>
        <div class="identity-detail">
          <div class="identity-detail__item">
            <span>手机号身份</span>
            <strong>{{ currentRow?.phoneBound ? '已绑定' : '未绑定' }}</strong>
            <small>{{ currentRow?.phone || '-' }}<template v-if="currentRow?.phoneBoundAt"> · {{ currentRow.phoneBoundAt }}</template></small>
          </div>
          <div class="identity-detail__item">
            <span>微信身份</span>
            <strong>{{ isWechatBound(currentRow) ? (currentRow?.wechatNickname || '已绑定') : '未绑定' }}</strong>
            <small v-if="currentRow?.wechatBoundAt">绑定时间 {{ currentRow.wechatBoundAt }}</small>
            <small v-else>暂无绑定时间</small>
          </div>
          <div class="identity-detail__item">
            <span>最近登录</span>
            <strong>{{ currentRow?.lastLoginAt || '暂无记录' }}</strong>
            <small>登录来源 {{ currentRow?.loginProvider || '-' }}</small>
          </div>
          <div class="identity-detail__item">
            <span>权限范围</span>
            <strong>{{ currentRow?.roleName || '家长' }}</strong>
            <small>{{ permissionScopeText(currentRow) }}</small>
          </div>
          <div class="identity-detail__item">
            <span>剩余试听次数</span>
            <strong>{{ currentRow?.trialBookingRemaining ?? currentRow?.trialBookingLimit ?? 1 }} 次</strong>
            <small>已使用 {{ currentRow?.trialBookingUsed ?? 0 }} 次 / 总额度 {{ currentRow?.trialBookingLimit ?? 1 }} 次</small>
          </div>
          <div class="identity-detail__item identity-detail__item--wide">
            <span>OpenID（小程序唯一标识）</span>
            <code>{{ currentRow?.wechatOpenid || '-' }}</code>
          </div>
          <div class="identity-detail__item identity-detail__item--wide">
            <span>UnionID（微信开放平台统一标识）</span>
            <code>{{ currentRow?.wechatUnionid || '-' }}</code>
          </div>
        </div>
        <el-divider>邀请关系</el-divider>
        <div class="invite-detail">
          <div class="invite-detail__block">
            <div class="invite-detail__label">被谁邀请</div>
            <div v-if="currentRow?.invitedBy" class="invite-person">
              <strong>{{ currentRow.invitedBy.name }}</strong>
              <span>{{ currentRow.invitedBy.phone || currentRow.invitedBy.id }}</span>
            </div>
            <div v-else class="invite-empty">暂无邀请人记录</div>
          </div>
          <div class="invite-detail__block">
            <div class="invite-detail__label">邀请的新用户</div>
            <div v-if="currentRow?.invitedUsers?.length" class="invite-list">
              <div v-for="item in currentRow.invitedUsers" :key="item.id || item.phone" class="invite-person">
                <strong>{{ item.name }}</strong>
                <span>{{ item.phone || item.id }} · {{ item.status || '-' }}</span>
              </div>
            </div>
            <div v-else class="invite-empty">暂无邀请新用户</div>
          </div>
        </div>
      </template>
      <template #footer>
        <el-button @click="dialogVisible = false">{{ dialogMode === 'detail' ? '关闭' : '取消' }}</el-button>
        <el-button v-if="dialogMode !== 'detail'" color="#0b4f46" :loading="submitting" @click="submit">保存</el-button>
      </template>
    </el-dialog>
  </div>
</template>

<script setup>
import { computed, onMounted, reactive, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { InfoFilled, Plus, Search } from '@element-plus/icons-vue'
import AppCard from '@/components/AppCard/index.vue'
import StatusTag from '@/components/StatusTag/index.vue'
import { changeAccountStatus, createAccount, deleteAccount, listAccounts, updateAccount } from '@/api/system/account'
import { listRoles } from '@/api/system/role'
import { useUserStore } from '@/store/modules/user'
import { hasPermission } from '@/utils/permission'

const userStore = useUserStore()
const loading = ref(false)
const submitting = ref(false)
const dialogVisible = ref(false)
const dialogMode = ref('create')
const editingId = ref('')
const currentRow = ref(null)
const accounts = ref([])
const total = ref(0)
const roleMatrix = ref([])
const form = reactive(blankForm())
const filters = reactive({
  keyword: '',
  role: 'all',
  wechatBound: 'all',
  status: 'all',
  pageNum: 1,
  pageSize: 10,
  paged: '1',
})

const roles = computed(() => {
  const adminRoles = roleMatrix.value.map((role) => ({ label: role.name, value: role.role }))
  return [{ label: '家长', value: 'parent' }, ...adminRoles]
})
const canAssignRole = computed(() => hasPermission(userStore.permissions, ['system:user:role']))
const canChangeStatus = computed(() => hasPermission(userStore.permissions, ['system:user:status']))
const dialogTitle = computed(() => {
  if (dialogMode.value === 'create') return '新增用户'
  if (dialogMode.value === 'detail') return '用户详情'
  return '编辑用户'
})

function blankForm() {
  return {
    name: '',
    phone: '',
    username: '',
    password: '',
    role: 'parent',
    status: 'active',
    trialBookingLimit: 1,
    childName: '',
    childGrade: '',
    childFocus: '',
  }
}

function fillForm(row = {}) {
  Object.assign(form, {
    name: row.name || '',
    phone: row.phone || '',
    username: row.username || row.phone || '',
    password: '',
    role: row.role || 'parent',
    status: row.status || 'active',
    trialBookingLimit: Number(row.trialBookingLimit ?? 1),
    childName: row.child?.name || '',
    childGrade: row.child?.grade || '',
    childFocus: row.child?.focus || '',
  })
}

function permissionScopeText(row = {}) {
  const permissions = row.permissions || []
  if (permissions.includes('*:*:*')) return '可使用全部管理功能'
  if (row.role === 'parent' || !permissions.length) return '使用家长端功能'
  return `已配置 ${permissions.length} 项管理权限`
}

function isWechatBound(row) {
  return Boolean(row?.wechatOpenid || row?.wechatUnionid)
}

function maskIdentity(value) {
  const text = String(value || '')
  if (!text) return ''
  if (text.length <= 12) return text
  return `${text.slice(0, 6)}...${text.slice(-4)}`
}

function wechatIdentityPreview(row) {
  if (row?.wechatOpenid) return `OpenID ${maskIdentity(row.wechatOpenid)}`
  return `UnionID ${maskIdentity(row?.wechatUnionid)}`
}

async function load() {
  loading.value = true
  try {
    const [accountData, roleData] = await Promise.all([listAccounts(filters), listRoles()])
    accounts.value = accountData.list || []
    total.value = accountData.total || 0
    roleMatrix.value = roleData.roles || []
  } finally {
    loading.value = false
  }
}

function query() {
  filters.pageNum = 1
  load()
}

function reset() {
  filters.keyword = ''
  filters.role = 'all'
  filters.wechatBound = 'all'
  filters.status = 'all'
  filters.pageNum = 1
  load()
}

function openCreate() {
  dialogMode.value = 'create'
  editingId.value = ''
  currentRow.value = null
  Object.assign(form, blankForm())
  dialogVisible.value = true
}

function openDetail(row) {
  dialogMode.value = 'detail'
  editingId.value = row.id
  currentRow.value = row
  fillForm(row)
  dialogVisible.value = true
}

function openEdit(row) {
  dialogMode.value = 'edit'
  editingId.value = row.id
  currentRow.value = row
  fillForm(row)
  dialogVisible.value = true
}

async function submit() {
  if (!form.name.trim()) {
    ElMessage.warning('请填写姓名')
    return
  }
  if (!/^1\d{10}$/.test(form.phone)) {
    ElMessage.warning('请填写正确手机号')
    return
  }
  if (!Number.isInteger(Number(form.trialBookingLimit)) || form.trialBookingLimit < 0 || form.trialBookingLimit > 99) {
    ElMessage.warning('试听额度必须是 0 到 99 之间的整数')
    return
  }
  submitting.value = true
  try {
    const payload = {
      ...form,
      username: form.username || form.phone,
    }
    if (!canAssignRole.value) payload.role = accounts.value.find((item) => item.id === editingId.value)?.role || form.role
    if (!canChangeStatus.value) payload.status = accounts.value.find((item) => item.id === editingId.value)?.status || form.status
    if (dialogMode.value === 'create') {
      await createAccount(payload)
      ElMessage.success('用户已新增')
    } else {
      await updateAccount(editingId.value, payload)
      ElMessage.success('用户已保存')
    }
    dialogVisible.value = false
    await load()
  } finally {
    submitting.value = false
  }
}

async function toggleStatus(row) {
  const next = row.status === 'active' ? 'disabled' : 'active'
  await ElMessageBox.confirm(`确认${next === 'active' ? '启用' : '停用'}用户“${row.name}”？`, '状态确认', {
    type: 'warning',
    confirmButtonText: '确认',
    cancelButtonText: '取消',
  })
  await changeAccountStatus(row.id, next)
  ElMessage.success('账号状态已更新')
  await load()
}

async function remove(row) {
  await ElMessageBox.confirm(`仅可删除尚未产生业务记录的用户“${row.name}”。已有推荐、积分、预约或兑换记录时，请停用账号。`, '删除确认', {
    type: 'warning',
    confirmButtonText: '删除',
    cancelButtonText: '取消',
  })
  await deleteAccount(row.id)
  ElMessage.success('用户已删除')
  await load()
}

onMounted(load)
</script>

<style lang="scss" scoped>
.head-actions {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 12px;
}

.head-actions .el-input {
  width: 260px;
}

.head-actions .el-select {
  width: 130px;
}

.table-card {
  padding: 18px;
}

.pager {
  display: flex;
  justify-content: flex-end;
  margin-top: 16px;
}

.account-cell {
  display: flex;
  align-items: center;
  gap: 10px;
}

.account-cell__avatar {
  display: grid;
  place-items: center;
  width: 38px;
  height: 38px;
  border-radius: 8px;
  color: #0b2f2a;
  background: #f5c451;
  font-weight: 900;
}

.account-cell__name {
  color: #17312d;
  font-weight: 900;
}

.account-cell__id {
  margin-top: 2px;
  color: #72817b;
  font-size: 12px;
}

.quota-field {
  display: flex;
  align-items: center;
  gap: 12px;
}

.quota-field span {
  color: #72817b;
  font-size: 12px;
}

.quota-cell {
  display: flex;
  align-items: baseline;
  justify-content: center;
  gap: 5px;
  white-space: nowrap;
}

.quota-cell strong {
  color: #0b4f46;
  font-size: 16px;
}

.quota-cell span {
  color: #72817b;
  font-size: 11px;
}

.wechat-column-head {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}

.wechat-column-head .el-icon {
  color: #7c8a84;
  cursor: help;
}

.wechat-cell,
.wechat-empty {
  display: flex;
  align-items: flex-start;
  gap: 5px;
}

.wechat-cell {
  flex-direction: column;
}

.wechat-cell__main {
  display: flex;
  align-items: center;
  gap: 7px;
  color: #17312d;
  font-size: 13px;
}

.wechat-cell__dot,
.wechat-empty__dot {
  width: 8px;
  height: 8px;
  flex: 0 0 8px;
  border-radius: 50%;
}

.wechat-cell__dot {
  background: #20a162;
  box-shadow: 0 0 0 3px #e5f5ec;
}

.wechat-empty {
  align-items: center;
  color: #8a9993;
  font-size: 13px;
}

.wechat-empty__dot {
  background: #c8d1cd;
}

.wechat-cell__id {
  color: #72817b;
  font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
  font-size: 11px;
}

.invite-cell {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 6px;
  color: #17312d;
  font-weight: 800;
}

.invite-cell__muted {
  color: #8a9993;
  font-weight: 700;
}

.invite-detail {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 14px;
}

.identity-detail {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 12px;
}

.detail-summary {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 14px;
  border: 1px solid #dbe7dc;
  background: #fbfdf8;
}

.detail-summary__avatar {
  display: grid;
  place-items: center;
  width: 52px;
  height: 52px;
  flex: 0 0 52px;
  border-radius: 8px;
  color: #0b2f2a;
  background: #f5c451;
  font-size: 22px;
  font-weight: 900;
}

.detail-summary__main {
  min-width: 0;
  flex: 1;
}

.detail-summary__main strong,
.detail-summary__main span {
  display: block;
}

.detail-summary__main strong {
  color: #17312d;
  font-size: 17px;
}

.detail-summary__main span {
  margin-top: 4px;
  color: #72817b;
  font-size: 12px;
}

.identity-detail__item {
  min-width: 0;
  padding: 13px 14px;
  border-left: 3px solid #17bebb;
  background: #f5f9f4;
}

.identity-detail__item--wide {
  grid-column: span 2;
  border-left-color: #f5c451;
}

.identity-detail__item span,
.identity-detail__item small,
.identity-detail__item code {
  display: block;
}

.identity-detail__item span {
  color: #72817b;
  font-size: 12px;
  font-weight: 800;
}

.identity-detail__item strong {
  display: block;
  margin-top: 6px;
  color: #17312d;
}

.identity-detail__item small,
.identity-detail__item code {
  margin-top: 5px;
  color: #66756f;
  font-size: 12px;
}

.identity-detail__item code {
  overflow-wrap: anywhere;
  white-space: normal;
}

.invite-detail__block {
  min-height: 112px;
  padding: 14px;
  border: 1px solid #dbe7dc;
  border-radius: 8px;
  background: #f8faf5;
}

.invite-detail__label {
  margin-bottom: 10px;
  color: #72817b;
  font-size: 12px;
  font-weight: 900;
}

.invite-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
  max-height: 190px;
  overflow-y: auto;
}

.invite-person {
  display: flex;
  flex-direction: column;
  gap: 4px;
  color: #17312d;
}

.invite-person strong {
  font-size: 14px;
}

.invite-person span,
.invite-empty {
  color: #72817b;
  font-size: 12px;
}
</style>
x% 100644 index.vue XCBJką/U xb<template>
  <div class="page-shell">
    <div class="page-head">
      <div>
        <div class="page-kicker">SYSTEM / ROLE</div>
        <div class="page-title">角色权限</div>
        <div class="page-subtitle">配置不同角色可查看的页面和可执行的操作。</div>
      </div>
      <el-button color="#0b4f46" :icon="Refresh" :loading="loading" @click="load">刷新权限</el-button>
    </div>

    <div class="role-layout">
      <AppCard class="role-list-card">
        <div
          v-for="role in roles"
          :key="role.role"
          :class="['role-item', selectedRole?.role === role.role && 'role-item--active']"
          @click="selectRole(role)"
        >
          <div>
            <div class="role-item__name">{{ role.name }}</div>
          </div>
          <el-tag v-if="role.permissions.includes('*:*:*')" type="warning" round>全部权限</el-tag>
          <el-tag v-else round>{{ role.permissions.length }} 项</el-tag>
        </div>
      </AppCard>

      <AppCard class="permission-card">
        <template v-if="selectedRole">
          <div class="permission-card__head">
            <div>
              <div class="permission-card__title">{{ selectedRole.name }}</div>
              <div class="permission-card__sub">页面访问和业务操作权限</div>
            </div>
            <div class="permission-actions">
              <el-button :icon="Refresh" :disabled="!canModifySelectedRole" @click="resetChecked">重置</el-button>
              <el-button color="#0b4f46" :disabled="!canModifySelectedRole" :loading="saving" @click="save">保存权限</el-button>
            </div>
          </div>

          <el-alert
            v-if="!canEdit"
            title="当前账号仅可查看角色权限。"
            type="info"
            show-icon
            :closable="false"
          />
          <el-alert
            v-else-if="selectedRole.role === 'super_admin'"
            title="超级管理员固定拥有全部管理权限，无需调整。"
            type="warning"
            show-icon
            :closable="false"
          />

          <el-tree
            ref="permissionTreeRef"
            :class="['permission-tree', !canModifySelectedRole && 'permission-tree--readonly']"
            :data="displayPermissionTree"
            node-key="id"
            show-checkbox
            default-expand-all
            :props="{ label: 'label', children: 'children', disabled: 'disabled' }"
            :check-strictly="false"
          >
            <template #default="{ data }">
              <div class="tree-node">
                <span>{{ data.label }}</span>
              </div>
            </template>
          </el-tree>
        </template>
        <el-empty v-else description="请选择角色" />
      </AppCard>
    </div>
  </div>
</template>

<script setup>
import { computed, nextTick, onMounted, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { Refresh } from '@element-plus/icons-vue'
import AppCard from '@/components/AppCard/index.vue'
import { listRoles, updateRolePermissions } from '@/api/system/role'
import { useUserStore } from '@/store/modules/user'
import { hasPermission } from '@/utils/permission'

const userStore = useUserStore()
const loading = ref(false)
const saving = ref(false)
const roles = ref([])
const permissionTree = ref([])
const selectedRoleCode = ref('')
const permissionTreeRef = ref()
const selectedRole = computed(() => roles.value.find((item) => item.role === selectedRoleCode.value))
const canEdit = computed(() => hasPermission(userStore.permissions, ['system:role:edit']))
const canModifySelectedRole = computed(() => canEdit.value && selectedRole.value?.role !== 'super_admin')
const displayPermissionTree = computed(() => markTreeDisabled(permissionTree.value, !canModifySelectedRole.value))

function markTreeDisabled(nodes = [], disabled = false) {
  return nodes.map((node) => ({
    ...node,
    disabled,
    children: markTreeDisabled(node.children || [], disabled),
  }))
}

async function load() {
  loading.value = true
  try {
    const data = await listRoles()
    roles.value = data.roles || []
    permissionTree.value = data.permissionTree || []
    if (!selectedRoleCode.value && roles.value.length) selectedRoleCode.value = roles.value[0].role
    await nextTick()
    resetChecked()
  } finally {
    loading.value = false
  }
}

async function selectRole(role) {
  selectedRoleCode.value = role.role
  await nextTick()
  resetChecked()
}

function resetChecked() {
  if (!permissionTreeRef.value || !selectedRole.value) return
  const keys = selectedRole.value.permissions.includes('*:*:*') ? allPermissionKeys(permissionTree.value) : selectedRole.value.permissions
  permissionTreeRef.value.setCheckedKeys(keys)
}

function allPermissionKeys(nodes = []) {
  return nodes.flatMap((node) => {
    const current = node.permission ? [node.permission] : []
    return current.concat(allPermissionKeys(node.children || []))
  })
}

async function save() {
  if (!selectedRole.value || !canModifySelectedRole.value) return
  saving.value = true
  try {
    const checked = permissionTreeRef.value.getCheckedKeys(true)
    const permissions = selectedRole.value.role === 'super_admin' ? ['*:*:*'] : checked
    const role = await updateRolePermissions(selectedRole.value.role, permissions)
    const index = roles.value.findIndex((item) => item.role === role.role)
    if (index >= 0) roles.value[index] = role
    ElMessage.success('角色权限已保存')
    await nextTick()
    resetChecked()
  } finally {
    saving.value = false
  }
}

onMounted(load)
</script>

<style lang="scss" scoped>
.role-layout {
  display: grid;
  grid-template-columns: 320px minmax(0, 1fr);
  gap: 16px;
}

.role-list-card,
.permission-card {
  padding: 18px;
}

.role-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 16px;
  border: 1px solid #e2e8df;
  border-radius: 8px;
  cursor: pointer;
}

.role-item + .role-item {
  margin-top: 12px;
}

.role-item--active {
  border-color: #9ae9dc;
  background: #e8f6f1;
}

.role-item__name {
  color: #0b2f2a;
  font-size: 18px;
  font-weight: 900;
}

.permission-card__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 18px;
  margin-bottom: 18px;
}

.permission-card__title {
  color: #0b2f2a;
  font-size: 22px;
  font-weight: 900;
}

.permission-card__sub {
  margin-top: 6px;
  color: #72817b;
  font-size: 13px;
}

.permission-actions {
  display: flex;
  gap: 10px;
}

.permission-tree {
  margin-top: 16px;
}

.permission-tree--readonly {
  padding: 12px;
  border: 1px solid #e2e7e4;
  border-radius: 8px;
  background: #f5f7f5;
}

.tree-node {
  display: flex;
  align-items: center;
  gap: 10px;
}
</style>
ص'xuimport { fileURLToPath, URL } from 'node:url'
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig(({ mode }) => {
  const env = loadEnv(mode, process.cwd())
  return {
    plugins: [vue()],
    resolve: {
      alias: {
        '@': fileURLToPath(new URL('./src', import.meta.url)),
      },
    },
    server: {
      host: '127.0.0.1',
      port: Number(env.VITE_APP_PORT || 5174),
      open: false,
      proxy: {
        '/api': {
          target: env.VITE_APP_PROXY_API || 'http://127.0.0.1:3100',
          changeOrigin: true,
        },
      },
    },
  }
})
vxg 100644 build-server-package.ps1 	MD"fil6100755 build-server-package.sh >tZ視E|*jxVparam(
  [string]$NodePath = "C:\Users\ruixininfo\AppData\Local\nodejs\npm.cmd"
)

$ErrorActionPreference = "Stop"

$root = Resolve-Path (Join-Path $PSScriptRoot "..")
$backend = Join-Path $root "backend"
$pcAdmin = Join-Path $root "pc-admin"
$release = Join-Path $root "release"
$serverPackage = Join-Path $release "server"
$pcStatic = Join-Path $serverPackage "public\pc-admin"

$serverPackageFull = [System.IO.Path]::GetFullPath($serverPackage)
$releaseFull = [System.IO.Path]::GetFullPath($release)
if (-not $serverPackageFull.StartsWith($releaseFull, [System.StringComparison]::OrdinalIgnoreCase)) {
  throw "Refusing to clean package path outside release directory: $serverPackageFull"
}

if (Test-Path $serverPackage) {
  Remove-Item -LiteralPath $serverPackage -Recurse -Force
}
New-Item -ItemType Directory -Force -Path $pcStatic | Out-Null

Push-Location $pcAdmin
try {
  & $NodePath run build
} finally {
  Pop-Location
}

Copy-Item -Path (Join-Path $backend "src") -Destination (Join-Path $serverPackage "src") -Recurse
Copy-Item -Path (Join-Path $backend "sql") -Destination (Join-Path $serverPackage "sql") -Recurse
Copy-Item -Path (Join-Path $backend "package.json") -Destination $serverPackage
Copy-Item -Path (Join-Path $backend "package-lock.json") -Destination $serverPackage
Copy-Item -Path (Join-Path $backend ".env.example") -Destination $serverPackage
Copy-Item -Path (Join-Path $pcAdmin "dist\*") -Destination $pcStatic -Recurse

$uploads = Join-Path $serverPackage "uploads"
New-Item -ItemType Directory -Force -Path $uploads | Out-Null

Write-Host "Server package ready:" $serverPackage
Write-Host "Upload this directory, then run npm ci --omit=dev and npm start on the server."
EHxu#!/usr/bin/env bash
set -euo pipefail

ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)"
BACKEND_DIR="$ROOT_DIR/backend"
PC_ADMIN_DIR="$ROOT_DIR/pc-admin"
RELEASE_DIR="$ROOT_DIR/release"
SERVER_PACKAGE="$RELEASE_DIR/server"
PC_STATIC="$SERVER_PACKAGE/public/pc-admin"
NPM_BIN="${NPM_BIN:-npm}"

mkdir -p "$RELEASE_DIR"
RELEASE_FULL="$(cd "$RELEASE_DIR" && pwd -P)"
SERVER_PACKAGE_FULL="$RELEASE_FULL/server"

case "$SERVER_PACKAGE_FULL" in
  "$RELEASE_FULL"/*) ;;
  *)
    echo "Refusing to clean package path outside release directory: $SERVER_PACKAGE_FULL" >&2
    exit 1
    ;;
esac

rm -rf "$SERVER_PACKAGE_FULL"
mkdir -p "$PC_STATIC"

(
  cd "$PC_ADMIN_DIR"
  "$NPM_BIN" run build
)

cp -R "$BACKEND_DIR/src" "$SERVER_PACKAGE/src"
cp -R "$BACKEND_DIR/sql" "$SERVER_PACKAGE/sql"
cp "$BACKEND_DIR/package.json" "$SERVER_PACKAGE/"
cp "$BACKEND_DIR/package-lock.json" "$SERVER_PACKAGE/"
cp "$BACKEND_DIR/.env.example" "$SERVER_PACKAGE/"
cp -R "$PC_ADMIN_DIR/dist/." "$PC_STATIC/"

mkdir -p "$SERVER_PACKAGE/uploads"

echo "Server package ready: $SERVER_PACKAGE"
echo "Upload this directory, then run npm ci --omit=dev and npm start on the server."
O#Qmݢ~#z*t