commit b7e5d1964face5d54ee49994ca4fd37dc5b7f8da Author: query-database Date: Wed Mar 25 15:46:20 2026 +0800 chore: initial import diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 0000000..e69de29 diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..64cfe72 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,25 @@ +{ + "root": true, + "env": { + "browser": true, + "es2020": true, + "node": true + }, + "extends": [ + "eslint:recommended", + "plugin:vue/vue3-recommended", + "plugin:@typescript-eslint/recommended" + ], + "parser": "vue-eslint-parser", + "parserOptions": { + "parser": "@typescript-eslint/parser", + "ecmaVersion": 2020, + "sourceType": "module" + }, + "rules": { + "vue/multi-word-component-names": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }] + } +} + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7d246ee --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +.pnpm-store +api/data +*.db +.env +.env.* +.trae +.vercel + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? +.vite diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..98d45bf --- /dev/null +++ b/.npmrc @@ -0,0 +1,5 @@ +registry=https://registry.npmjs.org/ +store-dir=.pnpm-store +package-import-method=copy +node-linker=hoisted + diff --git a/.vercelignore b/.vercelignore new file mode 100644 index 0000000..d057c48 --- /dev/null +++ b/.vercelignore @@ -0,0 +1,5 @@ +.pnpm-store +node_modules +api +dist +**/*.db diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..a7cea0b --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["Vue.volar"] +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..39af6f5 --- /dev/null +++ b/README.md @@ -0,0 +1,69 @@ +# MySQL 查询练习网站 + +一个面向新手/一般/进阶的 MySQL 查询练习网站: +- 登录注册 +- 首次引导(填姓名/选模块/经验/选择模拟库或导入库) +- 题库分级、在线运行 SQL、判题与进度保存 +- 支持模拟数据库(内置电商库/人事库)与导入自定义数据库(上传初始化 SQL) + +## 运行方式(本地开发) + +### 1) 启动 MySQL(Docker) +在项目根目录执行: +```bash +docker compose up -d +``` + +默认连接信息: +- Host: `127.0.0.1` +- Port: `3306` +- User: `root` +- Password: `root` + +### 2) 启动后端(Go + Gin) +打开一个终端: +```bash +cd api +go run . +``` + +后端默认端口:`http://localhost:8080` + +可选环境变量: +- `PORT`(默认 `8080`) +- `JWT_SECRET`(默认 `dev-secret`) +- `SQLITE_PATH`(默认 `./data/app.db`) +- `MYSQL_HOST` `MYSQL_PORT` `MYSQL_USER` `MYSQL_PASSWORD` + +### 3) 启动前端(Vite + Vue3) +打开另一个终端: +```bash +pnpm install +pnpm run dev +``` + +前端会通过 Vite 代理把 `/api` 转发到 `http://localhost:8080`。 + +## 部署说明(生产) + +当前仓库支持: +- 前端部署到 Vercel(静态站点) +- 后端+MySQL 用 Docker Compose 部署到一台服务器(或任意支持 Docker 的环境) + +### 1) 后端+MySQL(Docker Compose) +在服务器上: +```bash +docker compose -f docker-compose.prod.yml up -d +``` + +默认会把后端暴露到 `http://<你的服务器>:8080`。 + +### 2) 前端(Vercel) +- 通过 Vercel 部署本仓库根目录(Vite 构建输出为 `dist`,配置见 `vercel.json`) +- 在 Vercel 项目环境变量中设置: + - `VITE_API_BASE_URL` = `http://<你的服务器>:8080` + +## 常用命令 +- `pnpm run check`:TypeScript 类型检查 +- `pnpm run lint`:ESLint 检查 +- `cd api && go test ./...`:后端单元测试 diff --git a/api/Dockerfile b/api/Dockerfile new file mode 100644 index 0000000..6516eab --- /dev/null +++ b/api/Dockerfile @@ -0,0 +1,23 @@ +FROM golang:1.22-alpine AS build + +WORKDIR /src +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /out/api . + + +FROM alpine:3.20 + +WORKDIR /app +RUN adduser -D -u 10001 app +USER app + +COPY --from=build /out/api /app/api + +EXPOSE 8080 +ENV PORT=8080 + +CMD ["/app/api"] + diff --git a/api/go.mod b/api/go.mod new file mode 100644 index 0000000..a940f2c --- /dev/null +++ b/api/go.mod @@ -0,0 +1,52 @@ +module query-database/api + +go 1.23.0 + +require ( + github.com/gin-contrib/cors v1.7.6 + github.com/gin-gonic/gin v1.10.1 + github.com/go-sql-driver/mysql v1.9.0 + github.com/golang-jwt/jwt/v5 v5.2.1 + github.com/google/uuid v1.6.0 + golang.org/x/crypto v0.39.0 + modernc.org/sqlite v1.33.1 +) + +require ( + filippo.io/edwards25519 v1.1.0 // indirect + github.com/bytedance/sonic v1.13.3 // indirect + github.com/bytedance/sonic/loader v0.2.4 // indirect + github.com/cloudwego/base64x v0.1.5 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/gabriel-vasile/mimetype v1.4.9 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.26.0 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.10 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.3.0 // indirect + golang.org/x/arch v0.18.0 // indirect + golang.org/x/net v0.41.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/text v0.26.0 // indirect + google.golang.org/protobuf v1.36.6 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect + modernc.org/libc v1.55.3 // indirect + modernc.org/mathutil v1.6.0 // indirect + modernc.org/memory v1.8.0 // indirect + modernc.org/strutil v1.2.0 // indirect + modernc.org/token v1.1.0 // indirect +) diff --git a/api/go.sum b/api/go.sum new file mode 100644 index 0000000..e845841 --- /dev/null +++ b/api/go.sum @@ -0,0 +1,142 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0= +github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY= +github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= +github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= +github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= +github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY= +github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk= +github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= +github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= +github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= +github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= +github.com/go-sql-driver/mysql v1.9.0 h1:Y0zIbQXhQKmQgTp44Y1dp3wTXcn804QoTptLZT1vtvo= +github.com/go-sql-driver/mysql v1.9.0/go.mod h1:pDetrLJeA3oMujJuvXc8RJoasr589B6A9fwzD3QMrqw= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= +github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= +github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc= +golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= +golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= +golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= +golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= +golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= +golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ= +modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= +modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y= +modernc.org/ccgo/v4 v4.19.2/go.mod h1:ysS3mxiMV38XGRTTcgo0DQTeTmAO4oCmJl1nX9VFI3s= +modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= +modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= +modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw= +modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU= +modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI= +modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4= +modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U= +modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w= +modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= +modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= +modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= +modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= +modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= +modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc= +modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss= +modernc.org/sqlite v1.33.1 h1:trb6Z3YYoeM9eDL1O8do81kP+0ejv+YzgyFo+Gwy0nM= +modernc.org/sqlite v1.33.1/go.mod h1:pXV2xHxhzXZsgT/RtTFAPY6JJDEvOTcTdwADQCCWD4k= +modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= +modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= diff --git a/api/internal/auth/auth.go b/api/internal/auth/auth.go new file mode 100644 index 0000000..1ceb44d --- /dev/null +++ b/api/internal/auth/auth.go @@ -0,0 +1,87 @@ +package auth + +import ( + "crypto/subtle" + "database/sql" + "net/http" + "strings" + "time" + + "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt/v5" +) + +type Auth struct { + secret []byte +} + +func New(secret string) *Auth { + return &Auth{secret: []byte(secret)} +} + +func (a *Auth) Issue(userID string) (string, error) { + t := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ + "sub": userID, + "iat": time.Now().Unix(), + "exp": time.Now().Add(7 * 24 * time.Hour).Unix(), + }) + return t.SignedString(a.secret) +} + +func (a *Auth) parseToken(token string) (string, error) { + parsed, err := jwt.Parse(token, func(t *jwt.Token) (any, error) { + if t.Method.Alg() != jwt.SigningMethodHS256.Alg() { + return nil, jwt.ErrSignatureInvalid + } + return a.secret, nil + }) + if err != nil { + return "", err + } + if !parsed.Valid { + return "", jwt.ErrTokenInvalidClaims + } + claims, ok := parsed.Claims.(jwt.MapClaims) + if !ok { + return "", jwt.ErrTokenInvalidClaims + } + sub, ok := claims["sub"].(string) + if !ok || sub == "" { + return "", jwt.ErrTokenInvalidClaims + } + return sub, nil +} + +func (a *Auth) RequireAuth(sqlite *sql.DB) gin.HandlerFunc { + return func(c *gin.Context) { + hdr := c.GetHeader("Authorization") + if hdr == "" || !strings.HasPrefix(hdr, "Bearer ") { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"message": "未登录"}) + return + } + token := strings.TrimSpace(strings.TrimPrefix(hdr, "Bearer ")) + userID, err := a.parseToken(token) + if err != nil { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"message": "Token 无效"}) + return + } + + var exists int + if err := sqlite.QueryRow(`SELECT COUNT(1) FROM users WHERE id = ?`, userID).Scan(&exists); err != nil { + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"message": "服务异常"}) + return + } + if subtle.ConstantTimeEq(int32(exists), 1) != 1 { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"message": "账号不存在"}) + return + } + c.Set("userID", userID) + c.Next() + } +} + +func UserID(c *gin.Context) string { + v, _ := c.Get("userID") + s, _ := v.(string) + return s +} diff --git a/api/internal/config/config.go b/api/internal/config/config.go new file mode 100644 index 0000000..b43021c --- /dev/null +++ b/api/internal/config/config.go @@ -0,0 +1,52 @@ +package config + +import "os" + +type Config struct { + Port string + JWTSecret string + SQLitePath string + + MySQLHost string + MySQLPort string + MySQLUser string + MySQLPassword string + + CORSOrigins string + CORSAllowAll string +} + +func Load() Config { + cfg := Config{ + Port: os.Getenv("PORT"), + JWTSecret: os.Getenv("JWT_SECRET"), + SQLitePath: os.Getenv("SQLITE_PATH"), + + MySQLHost: os.Getenv("MYSQL_HOST"), + MySQLPort: os.Getenv("MYSQL_PORT"), + MySQLUser: os.Getenv("MYSQL_USER"), + MySQLPassword: os.Getenv("MYSQL_PASSWORD"), + + CORSOrigins: os.Getenv("CORS_ORIGINS"), + CORSAllowAll: os.Getenv("CORS_ALLOW_ALL"), + } + if cfg.JWTSecret == "" { + cfg.JWTSecret = "dev-secret" + } + if cfg.SQLitePath == "" { + cfg.SQLitePath = "./data/app.db" + } + if cfg.MySQLHost == "" { + cfg.MySQLHost = "127.0.0.1" + } + if cfg.MySQLPort == "" { + cfg.MySQLPort = "3306" + } + if cfg.MySQLUser == "" { + cfg.MySQLUser = "root" + } + if cfg.MySQLPassword == "" { + cfg.MySQLPassword = "root" + } + return cfg +} diff --git a/api/internal/db/mysql.go b/api/internal/db/mysql.go new file mode 100644 index 0000000..1252c25 --- /dev/null +++ b/api/internal/db/mysql.go @@ -0,0 +1,25 @@ +package db + +import ( + "database/sql" + "fmt" + "time" + + _ "github.com/go-sql-driver/mysql" + + "query-database/api/internal/config" +) + +func OpenMySQL(cfg config.Config) (*sql.DB, error) { + dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/?parseTime=true&multiStatements=true&charset=utf8mb4", cfg.MySQLUser, cfg.MySQLPassword, cfg.MySQLHost, cfg.MySQLPort) + db, err := sql.Open("mysql", dsn) + if err != nil { + return nil, err + } + db.SetMaxOpenConns(10) + db.SetConnMaxLifetime(5 * time.Minute) + if err := db.Ping(); err != nil { + return nil, err + } + return db, nil +} diff --git a/api/internal/db/sqlite.go b/api/internal/db/sqlite.go new file mode 100644 index 0000000..dcffc8a --- /dev/null +++ b/api/internal/db/sqlite.go @@ -0,0 +1,173 @@ +package db + +import ( + "database/sql" + "os" + "path/filepath" + "time" + + _ "modernc.org/sqlite" + + "github.com/google/uuid" +) + +func OpenSQLite(path string) (*sql.DB, error) { + dir := filepath.Dir(path) + if dir != "." { + _ = os.MkdirAll(dir, 0o755) + } + db, err := sql.Open("sqlite", path) + if err != nil { + return nil, err + } + db.SetMaxOpenConns(1) + db.SetConnMaxLifetime(10 * time.Minute) + if err := db.Ping(); err != nil { + return nil, err + } + return db, nil +} + +func MigrateSQLite(db *sql.DB) error { + stmts := []string{ + `CREATE TABLE IF NOT EXISTS users ( + id TEXT PRIMARY KEY, + email TEXT NOT NULL UNIQUE, + password_hash TEXT NOT NULL, + name TEXT NOT NULL DEFAULT '', + module_key TEXT NOT NULL DEFAULT 'shop', + experience_level TEXT NOT NULL DEFAULT 'beginner', + onboarding_completed INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL + );`, + `CREATE TABLE IF NOT EXISTS user_databases ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + name TEXT NOT NULL, + source TEXT NOT NULL, + schema_name TEXT NOT NULL, + is_active INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL + );`, + `CREATE INDEX IF NOT EXISTS idx_user_databases_user_id ON user_databases(user_id);`, + `CREATE TABLE IF NOT EXISTS exercises ( + id TEXT PRIMARY KEY, + title TEXT NOT NULL, + level TEXT NOT NULL, + prompt TEXT NOT NULL, + answer_sql TEXT NOT NULL, + database_key TEXT NOT NULL, + created_at TEXT NOT NULL + );`, + `CREATE INDEX IF NOT EXISTS idx_exercises_level ON exercises(level);`, + `CREATE TABLE IF NOT EXISTS progress ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + exercise_id TEXT NOT NULL, + draft_sql TEXT NOT NULL DEFAULT '', + is_solved INTEGER NOT NULL DEFAULT 0, + updated_at TEXT NOT NULL + );`, + `CREATE UNIQUE INDEX IF NOT EXISTS uniq_progress_user_exercise ON progress(user_id, exercise_id);`, + } + + for _, s := range stmts { + if _, err := db.Exec(s); err != nil { + return err + } + } + return nil +} + +func SeedSQLite(db *sql.DB) error { + var cnt int + if err := db.QueryRow(`SELECT COUNT(1) FROM exercises`).Scan(&cnt); err != nil { + return err + } + if cnt > 0 { + return nil + } + + now := time.Now().UTC().Format(time.RFC3339) + seed := []struct { + Title string + Level string + Prompt string + AnswerSQL string + DatabaseKey string + }{ + { + Title: "新手 1:查询所有商品名称与价格", + Level: "beginner", + Prompt: "在电商库中查询 products 表,返回 name 与 price 两列。", + AnswerSQL: "SELECT name, price FROM products ORDER BY id;", + DatabaseKey: "shop", + }, + { + Title: "新手 2:筛选价格大于 100 的商品", + Level: "beginner", + Prompt: "在电商库中查询 price > 100 的商品,返回 id, name, price,并按 price 从高到低排序。", + AnswerSQL: "SELECT id, name, price FROM products WHERE price > 100 ORDER BY price DESC, id ASC;", + DatabaseKey: "shop", + }, + { + Title: "一般 1:统计每个用户的订单数", + Level: "normal", + Prompt: "在电商库中统计每个用户的订单数量,返回 user_id 与 order_count,并按 order_count 从高到低排序。", + AnswerSQL: "SELECT user_id, COUNT(*) AS order_count FROM orders GROUP BY user_id ORDER BY order_count DESC, user_id ASC;", + DatabaseKey: "shop", + }, + { + Title: "一般 2:查询每个订单的总金额", + Level: "normal", + Prompt: "在电商库中,计算每个订单的总金额(sum(quantity * unit_price)),返回 order_id 与 total_amount,按 order_id 排序。", + AnswerSQL: "SELECT order_id, SUM(quantity * unit_price) AS total_amount FROM order_items GROUP BY order_id ORDER BY order_id ASC;", + DatabaseKey: "shop", + }, + { + Title: "进阶 1:找出下单金额最高的用户", + Level: "advanced", + Prompt: "在电商库中,计算每个用户的下单总金额,找出总金额最高的用户,返回 user_id 与 total_amount。", + AnswerSQL: "SELECT o.user_id, SUM(oi.quantity * oi.unit_price) AS total_amount FROM orders o JOIN order_items oi ON oi.order_id = o.id GROUP BY o.user_id ORDER BY total_amount DESC, o.user_id ASC LIMIT 1;", + DatabaseKey: "shop", + }, + { + Title: "新手 3:查询所有员工姓名与部门", + Level: "beginner", + Prompt: "在人事库中查询 employees 与 departments,返回 employee_name 与 department_name,并按 employee_id 排序。", + AnswerSQL: "SELECT e.name AS employee_name, d.name AS department_name FROM employees e JOIN departments d ON d.id = e.department_id ORDER BY e.id ASC;", + DatabaseKey: "hr", + }, + { + Title: "一般 3:统计每个部门员工数", + Level: "normal", + Prompt: "在人事库中统计每个部门的员工数,返回 department_name 与 employee_count,按 employee_count 从高到低排序。", + AnswerSQL: "SELECT d.name AS department_name, COUNT(e.id) AS employee_count FROM departments d LEFT JOIN employees e ON e.department_id = d.id GROUP BY d.id ORDER BY employee_count DESC, d.id ASC;", + DatabaseKey: "hr", + }, + { + Title: "进阶 2:找出每个部门工资最高的员工", + Level: "advanced", + Prompt: "在人事库中,找出每个部门工资最高的员工,返回 department_name, employee_name, salary。", + AnswerSQL: "SELECT d.name AS department_name, e.name AS employee_name, e.salary FROM departments d JOIN employees e ON e.department_id = d.id WHERE e.salary = (SELECT MAX(salary) FROM employees e2 WHERE e2.department_id = d.id) ORDER BY d.id ASC, e.id ASC;", + DatabaseKey: "hr", + }, + } + + for _, s := range seed { + id := uuid.NewString() + if _, err := db.Exec( + `INSERT INTO exercises (id, title, level, prompt, answer_sql, database_key, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)`, + id, + s.Title, + s.Level, + s.Prompt, + s.AnswerSQL, + s.DatabaseKey, + now, + ); err != nil { + return err + } + } + return nil +} diff --git a/api/internal/handlers/auth.go b/api/internal/handlers/auth.go new file mode 100644 index 0000000..b0caf6c --- /dev/null +++ b/api/internal/handlers/auth.go @@ -0,0 +1,92 @@ +package handlers + +import ( + "database/sql" + "net/http" + "strings" + "time" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "golang.org/x/crypto/bcrypt" +) + +type authReq struct { + Email string `json:"email"` + Password string `json:"password"` +} + +func (h *Handlers) Register(c *gin.Context) { + var req authReq + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"message": "参数错误"}) + return + } + email := strings.TrimSpace(strings.ToLower(req.Email)) + if email == "" || len(req.Password) < 6 { + c.JSON(http.StatusBadRequest, gin.H{"message": "邮箱或密码不合法"}) + return + } + + hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"message": "服务异常"}) + return + } + + id := uuid.NewString() + now := time.Now().UTC().Format(time.RFC3339) + _, err = h.sqlite.Exec( + `INSERT INTO users (id, email, password_hash, name, module_key, experience_level, onboarding_completed, created_at) + VALUES (?, ?, ?, '', 'shop', 'beginner', 0, ?)`, + id, + email, + string(hash), + now, + ) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"message": "该邮箱已注册"}) + return + } + token, err := h.auth.Issue(id) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"message": "服务异常"}) + return + } + c.JSON(http.StatusOK, gin.H{"token": token}) +} + +func (h *Handlers) Login(c *gin.Context) { + var req authReq + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"message": "参数错误"}) + return + } + email := strings.TrimSpace(strings.ToLower(req.Email)) + if email == "" { + c.JSON(http.StatusBadRequest, gin.H{"message": "邮箱或密码不合法"}) + return + } + + var id string + var hash string + err := h.sqlite.QueryRow(`SELECT id, password_hash FROM users WHERE email = ?`, email).Scan(&id, &hash) + if err != nil { + if err == sql.ErrNoRows { + c.JSON(http.StatusUnauthorized, gin.H{"message": "账号或密码错误"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"message": "服务异常"}) + return + } + if bcrypt.CompareHashAndPassword([]byte(hash), []byte(req.Password)) != nil { + c.JSON(http.StatusUnauthorized, gin.H{"message": "账号或密码错误"}) + return + } + token, err := h.auth.Issue(id) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"message": "服务异常"}) + return + } + c.JSON(http.StatusOK, gin.H{"token": token}) +} diff --git a/api/internal/handlers/databases.go b/api/internal/handlers/databases.go new file mode 100644 index 0000000..cecdea7 --- /dev/null +++ b/api/internal/handlers/databases.go @@ -0,0 +1,296 @@ +package handlers + +import ( + "bytes" + "context" + "database/sql" + "io" + "net/http" + "path/filepath" + "regexp" + "strings" + "time" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" + + "query-database/api/internal/auth" + "query-database/api/internal/mockdata" +) + +type userDatabaseItem struct { + ID string `json:"id"` + Name string `json:"name"` + Source string `json:"source"` + IsActive bool `json:"isActive"` +} + +func (h *Handlers) ListMockDatabases(c *gin.Context) { + ms := mockdata.List() + out := make([]gin.H, 0, len(ms)) + for _, m := range ms { + out = append(out, gin.H{"key": m.Key, "name": m.Name, "description": m.Description}) + } + c.JSON(http.StatusOK, out) +} + +func (h *Handlers) ListUserDatabases(c *gin.Context) { + userID := auth.UserID(c) + rows, err := h.sqlite.Query( + `SELECT id, name, source, is_active FROM user_databases WHERE user_id = ? ORDER BY created_at DESC`, + userID, + ) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"message": "服务异常"}) + return + } + defer rows.Close() + out := make([]userDatabaseItem, 0) + for rows.Next() { + var id, name, source string + var active int + if err := rows.Scan(&id, &name, &source, &active); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"message": "服务异常"}) + return + } + out = append(out, userDatabaseItem{ID: id, Name: name, Source: source, IsActive: active == 1}) + } + c.JSON(http.StatusOK, out) +} + +type activateReq struct { + ID string `json:"id"` +} + +func (h *Handlers) ActivateUserDatabase(c *gin.Context) { + userID := auth.UserID(c) + var req activateReq + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"message": "参数错误"}) + return + } + if strings.TrimSpace(req.ID) == "" { + c.JSON(http.StatusBadRequest, gin.H{"message": "id 不能为空"}) + return + } + tx, err := h.sqlite.Begin() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"message": "服务异常"}) + return + } + defer tx.Rollback() + if _, err := tx.Exec(`UPDATE user_databases SET is_active = 0 WHERE user_id = ?`, userID); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"message": "服务异常"}) + return + } + res, err := tx.Exec(`UPDATE user_databases SET is_active = 1 WHERE id = ? AND user_id = ?`, req.ID, userID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"message": "服务异常"}) + return + } + n, _ := res.RowsAffected() + if n == 0 { + c.JSON(http.StatusNotFound, gin.H{"message": "数据库不存在"}) + return + } + if err := tx.Commit(); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"message": "服务异常"}) + return + } + c.Status(http.StatusNoContent) +} + +type activateMockReq struct { + Key string `json:"key"` +} + +func (h *Handlers) ActivateMockDatabase(c *gin.Context) { + userID := auth.UserID(c) + var req activateMockReq + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"message": "参数错误"}) + return + } + key := strings.TrimSpace(req.Key) + if key != "shop" && key != "hr" { + c.JSON(http.StatusBadRequest, gin.H{"message": "key 不合法"}) + return + } + if err := h.activateMockForUser(userID, key, time.Now().UTC().Format(time.RFC3339)); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"message": "设置失败"}) + return + } + c.Status(http.StatusNoContent) +} + +func (h *Handlers) activateMockForUser(userID string, key string, now string) error { + ms := mockdata.List() + var picked *mockdata.MockDatabase + for i := range ms { + if ms[i].Key == key { + picked = &ms[i] + break + } + } + if picked == nil { + return sql.ErrNoRows + } + tx, err := h.sqlite.Begin() + if err != nil { + return err + } + defer tx.Rollback() + + if _, err := tx.Exec(`UPDATE user_databases SET is_active = 0 WHERE user_id = ?`, userID); err != nil { + return err + } + var existingID string + err = tx.QueryRow( + `SELECT id FROM user_databases WHERE user_id = ? AND source = 'mock' AND schema_name = ? LIMIT 1`, + userID, + picked.SchemaName, + ).Scan(&existingID) + if err != nil { + if err == sql.ErrNoRows { + newID := uuid.NewString() + if _, err := tx.Exec( + `INSERT INTO user_databases (id, user_id, name, source, schema_name, is_active, created_at) VALUES (?, ?, ?, 'mock', ?, 1, ?)`, + newID, + userID, + picked.Name, + picked.SchemaName, + now, + ); err != nil { + return err + } + } else { + return err + } + } else { + if _, err := tx.Exec(`UPDATE user_databases SET is_active = 1 WHERE id = ?`, existingID); err != nil { + return err + } + } + + return tx.Commit() +} + +func (h *Handlers) ImportDatabase(c *gin.Context) { + userID := auth.UserID(c) + name := strings.TrimSpace(c.PostForm("name")) + if name == "" { + name = "我的数据库" + } + file, hdr, err := c.Request.FormFile("file") + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"message": "请选择文件"}) + return + } + defer file.Close() + + if strings.ToLower(filepath.Ext(hdr.Filename)) != ".sql" { + c.JSON(http.StatusBadRequest, gin.H{"message": "仅支持 .sql 文件"}) + return + } + + buf := new(bytes.Buffer) + if _, err := io.CopyN(buf, file, 2<<20); err != nil && err != io.EOF { + c.JSON(http.StatusBadRequest, gin.H{"message": "读取失败"}) + return + } + sqlText := buf.String() + if strings.TrimSpace(sqlText) == "" { + c.JSON(http.StatusBadRequest, gin.H{"message": "SQL 为空"}) + return + } + + schema := makeSchemaName(userID) + if err := createSchemaAndInit(h.mysql, schema, sqlText); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"message": "导入失败:" + err.Error()}) + return + } + + now := time.Now().UTC().Format(time.RFC3339) + tx, err := h.sqlite.Begin() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"message": "服务异常"}) + return + } + defer tx.Rollback() + if _, err := tx.Exec(`UPDATE user_databases SET is_active = 0 WHERE user_id = ?`, userID); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"message": "服务异常"}) + return + } + id := uuid.NewString() + if _, err := tx.Exec( + `INSERT INTO user_databases (id, user_id, name, source, schema_name, is_active, created_at) + VALUES (?, ?, ?, 'imported', ?, 1, ?)`, + id, + userID, + name, + schema, + now, + ); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"message": "保存失败"}) + return + } + if err := tx.Commit(); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"message": "服务异常"}) + return + } + c.Status(http.StatusNoContent) +} + +var reSchemaSan = regexp.MustCompile(`[^a-z0-9_]+`) + +func makeSchemaName(userID string) string { + base := strings.ToLower(userID) + base = strings.ReplaceAll(base, "-", "_") + base = reSchemaSan.ReplaceAllString(base, "") + if len(base) > 12 { + base = base[:12] + } + return "udb_" + base + "_" + strings.ReplaceAll(uuid.NewString(), "-", "")[:8] +} + +func quoteIdent(s string) string { + return "`" + strings.ReplaceAll(s, "`", "``") + "`" +} + +func createSchemaAndInit(mysql *sql.DB, schema string, initSQL string) error { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + conn, err := mysql.Conn(ctx) + if err != nil { + return err + } + defer conn.Close() + + if _, err := conn.ExecContext(ctx, "CREATE DATABASE IF NOT EXISTS "+quoteIdent(schema)+" CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci"); err != nil { + return err + } + if _, err := conn.ExecContext(ctx, "USE "+quoteIdent(schema)); err != nil { + return err + } + stmts := splitStatements(initSQL) + for _, s := range stmts { + if _, err := conn.ExecContext(ctx, s); err != nil { + return err + } + } + return nil +} + +func splitStatements(sqlText string) []string { + parts := strings.Split(sqlText, ";") + out := make([]string, 0, len(parts)) + for _, p := range parts { + s := strings.TrimSpace(p) + if s == "" { + continue + } + out = append(out, s) + } + return out +} diff --git a/api/internal/handlers/exercises.go b/api/internal/handlers/exercises.go new file mode 100644 index 0000000..47df075 --- /dev/null +++ b/api/internal/handlers/exercises.go @@ -0,0 +1,99 @@ +package handlers + +import ( + "net/http" + "strings" + + "github.com/gin-gonic/gin" + + "query-database/api/internal/auth" +) + +type exerciseSummary struct { + ID string `json:"id"` + Title string `json:"title"` + Level string `json:"level"` + Database string `json:"databaseKey"` + IsSolved bool `json:"isSolved"` +} + +func (h *Handlers) ListExercises(c *gin.Context) { + userID := auth.UserID(c) + level := strings.TrimSpace(c.Query("level")) + if level == "" { + level = "beginner" + } + if level != "beginner" && level != "normal" && level != "advanced" { + c.JSON(http.StatusBadRequest, gin.H{"message": "level 不合法"}) + return + } + + moduleKey := strings.TrimSpace(c.Query("moduleKey")) + if moduleKey == "" { + _ = h.sqlite.QueryRow(`SELECT module_key FROM users WHERE id = ?`, userID).Scan(&moduleKey) + } + if moduleKey != "shop" && moduleKey != "hr" { + c.JSON(http.StatusBadRequest, gin.H{"message": "moduleKey 不合法"}) + return + } + + rows, err := h.sqlite.Query( + `SELECT e.id, e.title, e.level, e.database_key, + COALESCE(p.is_solved, 0) AS is_solved + FROM exercises e + LEFT JOIN progress p ON p.exercise_id = e.id AND p.user_id = ? + WHERE e.level = ? AND e.database_key = ? + ORDER BY e.created_at ASC`, + userID, + level, + moduleKey, + ) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"message": "服务异常"}) + return + } + defer rows.Close() + + out := make([]exerciseSummary, 0) + for rows.Next() { + var id, title, lvl, dbKey string + var solved int + if err := rows.Scan(&id, &title, &lvl, &dbKey, &solved); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"message": "服务异常"}) + return + } + out = append(out, exerciseSummary{ID: id, Title: title, Level: lvl, Database: dbKey, IsSolved: solved == 1}) + } + c.JSON(http.StatusOK, out) +} + +type exerciseDetail struct { + ID string `json:"id"` + Title string `json:"title"` + Level string `json:"level"` + Prompt string `json:"prompt"` + Database string `json:"databaseKey"` + IsSolved bool `json:"isSolved"` + DraftSQL string `json:"draftSql"` +} + +func (h *Handlers) GetExercise(c *gin.Context) { + userID := auth.UserID(c) + id := c.Param("id") + var title, level, prompt, dbKey string + if err := h.sqlite.QueryRow(`SELECT title, level, prompt, database_key FROM exercises WHERE id = ?`, id).Scan(&title, &level, &prompt, &dbKey); err != nil { + c.JSON(http.StatusNotFound, gin.H{"message": "题目不存在"}) + return + } + var draft string + var solved int + _ = h.sqlite.QueryRow( + `SELECT draft_sql, is_solved FROM progress WHERE user_id = ? AND exercise_id = ?`, + userID, + id, + ).Scan(&draft, &solved) + + c.JSON(http.StatusOK, exerciseDetail{ + ID: id, Title: title, Level: level, Prompt: prompt, Database: dbKey, IsSolved: solved == 1, DraftSQL: draft, + }) +} diff --git a/api/internal/handlers/handlers.go b/api/internal/handlers/handlers.go new file mode 100644 index 0000000..5f6ab14 --- /dev/null +++ b/api/internal/handlers/handlers.go @@ -0,0 +1,26 @@ +package handlers + +import ( + "database/sql" + + "query-database/api/internal/auth" + "query-database/api/internal/config" +) + +type Deps struct { + Cfg config.Config + Auth *auth.Auth + SQLite *sql.DB + MySQL *sql.DB +} + +type Handlers struct { + cfg config.Config + auth *auth.Auth + sqlite *sql.DB + mysql *sql.DB +} + +func New(d Deps) *Handlers { + return &Handlers{cfg: d.Cfg, auth: d.Auth, sqlite: d.SQLite, mysql: d.MySQL} +} diff --git a/api/internal/handlers/me.go b/api/internal/handlers/me.go new file mode 100644 index 0000000..7edd4c3 --- /dev/null +++ b/api/internal/handlers/me.go @@ -0,0 +1,81 @@ +package handlers + +import ( + "database/sql" + "net/http" + "time" + + "github.com/gin-gonic/gin" + + "query-database/api/internal/auth" +) + +func (h *Handlers) Me(c *gin.Context) { + userID := auth.UserID(c) + var email, name, moduleKey, exp string + var onboarding int + if err := h.sqlite.QueryRow( + `SELECT email, name, module_key, experience_level, onboarding_completed FROM users WHERE id = ?`, + userID, + ).Scan(&email, &name, &moduleKey, &exp, &onboarding); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"message": "服务异常"}) + return + } + + active, err := h.getActiveDatabase(userID) + if err != nil { + if onboarding == 1 { + now := time.Now().UTC().Format(time.RFC3339) + _ = h.activateMockForUser(userID, moduleKey, now) + active, _ = h.getActiveDatabase(userID) + } + } + + var activeDBID any = nil + var activeDB any = nil + if active != nil { + activeDBID = active.ID + activeDB = gin.H{ + "id": active.ID, + "name": active.Name, + "source": active.Source, + "schemaName": active.SchemaName, + } + } + + c.JSON(http.StatusOK, gin.H{ + "id": userID, + "email": email, + "name": name, + "moduleKey": moduleKey, + "experienceLevel": exp, + "onboardingCompleted": onboarding == 1, + "activeDatabaseId": activeDBID, + "activeDatabase": activeDB, + }) +} + +type activeDatabase struct { + ID string + Name string + Source string + SchemaName string +} + +func (h *Handlers) getActiveDatabase(userID string) (*activeDatabase, error) { + row := h.sqlite.QueryRow( + `SELECT id, name, source, schema_name + FROM user_databases + WHERE user_id = ? AND is_active = 1 + LIMIT 1`, + userID, + ) + var id, name, source, schema string + if err := row.Scan(&id, &name, &source, &schema); err != nil { + if err == sql.ErrNoRows { + return nil, err + } + return nil, err + } + return &activeDatabase{ID: id, Name: name, Source: source, SchemaName: schema}, nil +} diff --git a/api/internal/handlers/module.go b/api/internal/handlers/module.go new file mode 100644 index 0000000..8a9b5e4 --- /dev/null +++ b/api/internal/handlers/module.go @@ -0,0 +1,19 @@ +package handlers + +import "query-database/api/internal/mockdata" + +type moduleDatabase struct { + Key string + Name string + SchemaName string +} + +func moduleDatabaseByKey(key string) (*moduleDatabase, bool) { + ms := mockdata.List() + for i := range ms { + if ms[i].Key == key { + return &moduleDatabase{Key: ms[i].Key, Name: ms[i].Name, SchemaName: ms[i].SchemaName}, true + } + } + return nil, false +} diff --git a/api/internal/handlers/module_switch.go b/api/internal/handlers/module_switch.go new file mode 100644 index 0000000..1d2e29f --- /dev/null +++ b/api/internal/handlers/module_switch.go @@ -0,0 +1,94 @@ +package handlers + +import ( + "database/sql" + "net/http" + "strings" + "time" + + "github.com/gin-gonic/gin" + + "query-database/api/internal/auth" +) + +type switchModuleReq struct { + ModuleKey string `json:"moduleKey"` + ActivateRecommendedMock *bool `json:"activateRecommendedMock"` +} + +func (h *Handlers) SwitchModule(c *gin.Context) { + userID := auth.UserID(c) + var req switchModuleReq + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"message": "参数错误"}) + return + } + + newKey := strings.TrimSpace(req.ModuleKey) + if newKey != "shop" && newKey != "hr" { + c.JSON(http.StatusBadRequest, gin.H{"message": "模块不合法"}) + return + } + + var prevKey string + if err := h.sqlite.QueryRow(`SELECT module_key FROM users WHERE id = ?`, userID).Scan(&prevKey); err != nil { + if err == sql.ErrNoRows { + c.JSON(http.StatusUnauthorized, gin.H{"message": "未登录"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"message": "服务异常"}) + return + } + + before, _ := h.getActiveDatabase(userID) + + if _, err := h.sqlite.Exec(`UPDATE users SET module_key = ? WHERE id = ?`, newKey, userID); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"message": "服务异常"}) + return + } + + shouldActivate := false + if req.ActivateRecommendedMock != nil { + shouldActivate = *req.ActivateRecommendedMock + } else { + if before == nil || before.Source == "mock" { + shouldActivate = true + } + } + + didActivate := false + if shouldActivate { + now := time.Now().UTC().Format(time.RFC3339) + if err := h.activateMockForUser(userID, newKey, now); err != nil { + _, _ = h.sqlite.Exec(`UPDATE users SET module_key = ? WHERE id = ?`, prevKey, userID) + c.JSON(http.StatusInternalServerError, gin.H{"message": "设置数据库失败"}) + return + } + didActivate = true + } + + after, _ := h.getActiveDatabase(userID) + md, _ := moduleDatabaseByKey(newKey) + + var beforeDB any = nil + if before != nil { + beforeDB = gin.H{"id": before.ID, "name": before.Name, "source": before.Source, "schemaName": before.SchemaName} + } + var afterDB any = nil + if after != nil { + afterDB = gin.H{"id": after.ID, "name": after.Name, "source": after.Source, "schemaName": after.SchemaName} + } + + out := gin.H{ + "previousModuleKey": prevKey, + "moduleKey": newKey, + "didActivateRecommendedMock": didActivate, + "activeDatabaseBefore": beforeDB, + "activeDatabaseAfter": afterDB, + } + if md != nil { + out["recommendedMock"] = gin.H{"key": md.Key, "name": md.Name, "schemaName": md.SchemaName} + } + + c.JSON(http.StatusOK, out) +} diff --git a/api/internal/handlers/onboarding.go b/api/internal/handlers/onboarding.go new file mode 100644 index 0000000..6f5c8dc --- /dev/null +++ b/api/internal/handlers/onboarding.go @@ -0,0 +1,74 @@ +package handlers + +import ( + "net/http" + "strings" + "time" + + "github.com/gin-gonic/gin" + + "query-database/api/internal/auth" +) + +type onboardingReq struct { + Name string `json:"name"` + ModuleKey string `json:"moduleKey"` + ExperienceLevel string `json:"experienceLevel"` + DBMode string `json:"dbMode"` + MockDBKey *string `json:"mockDbKey"` +} + +func (h *Handlers) CompleteOnboarding(c *gin.Context) { + userID := auth.UserID(c) + var req onboardingReq + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"message": "参数错误"}) + return + } + name := strings.TrimSpace(req.Name) + if name == "" { + c.JSON(http.StatusBadRequest, gin.H{"message": "请填写姓名"}) + return + } + if req.ModuleKey != "shop" && req.ModuleKey != "hr" { + c.JSON(http.StatusBadRequest, gin.H{"message": "模块不合法"}) + return + } + if req.ExperienceLevel != "beginner" && req.ExperienceLevel != "normal" && req.ExperienceLevel != "advanced" { + c.JSON(http.StatusBadRequest, gin.H{"message": "经验不合法"}) + return + } + if req.DBMode != "mock" && req.DBMode != "import" { + c.JSON(http.StatusBadRequest, gin.H{"message": "数据库模式不合法"}) + return + } + + _, err := h.sqlite.Exec( + `UPDATE users SET name = ?, module_key = ?, experience_level = ?, onboarding_completed = 1 WHERE id = ?`, + name, + req.ModuleKey, + req.ExperienceLevel, + userID, + ) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"message": "服务异常"}) + return + } + + if req.DBMode == "mock" { + key := req.ModuleKey + if req.MockDBKey != nil { + key = *req.MockDBKey + } + if key != "shop" && key != "hr" { + c.JSON(http.StatusBadRequest, gin.H{"message": "模拟库不合法"}) + return + } + if err := h.activateMockForUser(userID, key, time.Now().UTC().Format(time.RFC3339)); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"message": "设置数据库失败"}) + return + } + } + + c.Status(http.StatusNoContent) +} diff --git a/api/internal/handlers/progress.go b/api/internal/handlers/progress.go new file mode 100644 index 0000000..7ac9942 --- /dev/null +++ b/api/internal/handlers/progress.go @@ -0,0 +1,58 @@ +package handlers + +import ( + "net/http" + "strings" + "time" + + "github.com/gin-gonic/gin" + + "query-database/api/internal/auth" +) + +type upsertProgressReq struct { + ExerciseID string `json:"exerciseId"` + DraftSQL string `json:"draftSql"` + IsSolved bool `json:"isSolved"` +} + +func (h *Handlers) UpsertProgress(c *gin.Context) { + userID := auth.UserID(c) + var req upsertProgressReq + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"message": "参数错误"}) + return + } + if strings.TrimSpace(req.ExerciseID) == "" { + c.JSON(http.StatusBadRequest, gin.H{"message": "exerciseId 不能为空"}) + return + } + now := time.Now().UTC().Format(time.RFC3339) + solved := 0 + if req.IsSolved { + solved = 1 + } + _, err := h.sqlite.Exec( + `INSERT INTO progress (id, user_id, exercise_id, draft_sql, is_solved, updated_at) + VALUES (?, ?, ?, ?, ?, ?) + ON CONFLICT(user_id, exercise_id) DO UPDATE SET + draft_sql = excluded.draft_sql, + is_solved = CASE WHEN progress.is_solved = 1 THEN 1 ELSE excluded.is_solved END, + updated_at = excluded.updated_at`, + uuidOrDeterministic(userID, req.ExerciseID), + userID, + req.ExerciseID, + req.DraftSQL, + solved, + now, + ) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"message": "保存失败"}) + return + } + c.Status(http.StatusNoContent) +} + +func uuidOrDeterministic(userID string, exerciseID string) string { + return userID + ":" + exerciseID +} diff --git a/api/internal/handlers/sql.go b/api/internal/handlers/sql.go new file mode 100644 index 0000000..08a3a78 --- /dev/null +++ b/api/internal/handlers/sql.go @@ -0,0 +1,191 @@ +package handlers + +import ( + "context" + "database/sql" + "net/http" + "strings" + "time" + + "github.com/gin-gonic/gin" + + "query-database/api/internal/auth" + "query-database/api/internal/judge" +) + +type executeReq struct { + ExerciseID string `json:"exerciseId"` + SQL string `json:"sql"` +} + +type executeResp struct { + Ok bool `json:"ok"` + DurationMs int64 `json:"durationMs"` + Columns []string `json:"columns"` + Rows []map[string]any `json:"rows"` + Verdict string `json:"verdict"` + Hint string `json:"hint"` +} + +func (h *Handlers) ExecuteSQL(c *gin.Context) { + userID := auth.UserID(c) + var req executeReq + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"message": "参数错误"}) + return + } + sqlText := strings.TrimSpace(req.SQL) + if req.ExerciseID == "" || sqlText == "" { + c.JSON(http.StatusBadRequest, gin.H{"message": "exerciseId 或 sql 不能为空"}) + return + } + if err := validateUserQuery(sqlText); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"message": err.Error()}) + return + } + + active, err := h.getActiveDatabase(userID) + if err != nil || active == nil { + c.JSON(http.StatusBadRequest, gin.H{"message": "请先在数据库管理页激活一个数据库"}) + return + } + + var answerSQL string + var dbKey string + if err := h.sqlite.QueryRow(`SELECT answer_sql, database_key FROM exercises WHERE id = ?`, req.ExerciseID).Scan(&answerSQL, &dbKey); err != nil { + c.JSON(http.StatusNotFound, gin.H{"message": "题目不存在"}) + return + } + mod, ok := moduleDatabaseByKey(dbKey) + if !ok { + c.JSON(http.StatusInternalServerError, gin.H{"message": "题目数据库配置错误"}) + return + } + + if active.Source != "mock" || active.SchemaName != mod.SchemaName { + c.JSON(http.StatusOK, executeResp{ + Ok: false, + DurationMs: 0, + Columns: []string{}, + Rows: []map[string]any{}, + Verdict: "fail", + Hint: "当前激活数据库与本题不匹配,请切换到:" + mod.Name, + }) + return + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + conn, err := h.mysql.Conn(ctx) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"message": "MySQL 连接失败"}) + return + } + defer conn.Close() + + if _, err := conn.ExecContext(ctx, "USE "+quoteIdent(active.SchemaName)); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"message": "数据库不可用"}) + return + } + + started := time.Now() + userRes, err := runQuery(ctx, conn, sqlText) + dur := time.Since(started).Milliseconds() + if err != nil { + c.JSON(http.StatusOK, executeResp{Ok: false, DurationMs: dur, Verdict: "fail", Hint: "SQL 执行失败:" + err.Error(), Columns: []string{}, Rows: []map[string]any{}}) + return + } + + ansRes, err := runQuery(ctx, conn, answerSQL) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"message": "标准答案执行失败"}) + return + } + + v := judge.Compare(ansRes, userRes) + verdict := "fail" + if v.Pass { + verdict = "pass" + } + + c.JSON(http.StatusOK, executeResp{ + Ok: true, + DurationMs: dur, + Columns: userRes.Columns, + Rows: toRowMaps(userRes), + Verdict: verdict, + Hint: v.Hint, + }) +} + +func validateUserQuery(sqlText string) error { + parts := splitStatements(sqlText) + if len(parts) != 1 { + return errString("一次只能执行一条语句") + } + s := strings.ToLower(strings.TrimSpace(parts[0])) + if strings.HasPrefix(s, "select ") || s == "select" { + return nil + } + if strings.HasPrefix(s, "with ") { + return nil + } + if strings.HasPrefix(s, "show ") || strings.HasPrefix(s, "describe ") || strings.HasPrefix(s, "explain ") { + return nil + } + return errString("仅允许 SELECT/WITH/SHOW/DESCRIBE/EXPLAIN") +} + +type errString string + +func (e errString) Error() string { return string(e) } + +func runQuery(ctx context.Context, conn *sql.Conn, sqlText string) (judge.QueryResult, error) { + rows, err := conn.QueryContext(ctx, sqlText) + if err != nil { + return judge.QueryResult{}, err + } + defer rows.Close() + + cols, err := rows.Columns() + if err != nil { + return judge.QueryResult{}, err + } + + out := judge.QueryResult{Columns: cols, Rows: make([][]any, 0)} + for rows.Next() { + values := make([]any, len(cols)) + ptrs := make([]any, len(cols)) + for i := range values { + ptrs[i] = &values[i] + } + if err := rows.Scan(ptrs...); err != nil { + return judge.QueryResult{}, err + } + for i := range values { + if b, ok := values[i].([]byte); ok { + values[i] = string(b) + } + } + out.Rows = append(out.Rows, values) + if len(out.Rows) >= 200 { + break + } + } + return out, nil +} + +func toRowMaps(r judge.QueryResult) []map[string]any { + out := make([]map[string]any, 0, len(r.Rows)) + for _, row := range r.Rows { + m := make(map[string]any, len(r.Columns)) + for i, c := range r.Columns { + if i < len(row) { + m[c] = row[i] + } + } + out = append(out, m) + } + return out +} diff --git a/api/internal/handlers/types.go b/api/internal/handlers/types.go new file mode 100644 index 0000000..afbb976 --- /dev/null +++ b/api/internal/handlers/types.go @@ -0,0 +1,9 @@ +package handlers + +import ( + "strings" +) + +func normalizeEmail(s string) string { + return strings.TrimSpace(strings.ToLower(s)) +} diff --git a/api/internal/judge/judge.go b/api/internal/judge/judge.go new file mode 100644 index 0000000..dd38799 --- /dev/null +++ b/api/internal/judge/judge.go @@ -0,0 +1,38 @@ +package judge + +import "fmt" + +type QueryResult struct { + Columns []string + Rows [][]any +} + +type Verdict struct { + Pass bool + Hint string +} + +func Compare(expected QueryResult, actual QueryResult) Verdict { + if len(expected.Columns) != len(actual.Columns) { + return Verdict{Pass: false, Hint: "列数量不一致"} + } + for i := range expected.Columns { + if expected.Columns[i] != actual.Columns[i] { + return Verdict{Pass: false, Hint: "列顺序或列名不一致"} + } + } + if len(expected.Rows) != len(actual.Rows) { + return Verdict{Pass: false, Hint: "返回行数不一致"} + } + for r := range expected.Rows { + if len(expected.Rows[r]) != len(actual.Rows[r]) { + return Verdict{Pass: false, Hint: "返回列数不一致"} + } + for c := range expected.Rows[r] { + if fmt.Sprint(expected.Rows[r][c]) != fmt.Sprint(actual.Rows[r][c]) { + return Verdict{Pass: false, Hint: "结果内容不一致"} + } + } + } + return Verdict{Pass: true, Hint: "结果正确"} +} diff --git a/api/internal/judge/judge_test.go b/api/internal/judge/judge_test.go new file mode 100644 index 0000000..5ea6817 --- /dev/null +++ b/api/internal/judge/judge_test.go @@ -0,0 +1,30 @@ +package judge + +import "testing" + +func TestCompare_Pass(t *testing.T) { + exp := QueryResult{Columns: []string{"id"}, Rows: [][]any{{1}, {2}}} + act := QueryResult{Columns: []string{"id"}, Rows: [][]any{{1}, {2}}} + v := Compare(exp, act) + if !v.Pass { + t.Fatalf("expected pass") + } +} + +func TestCompare_ColumnsMismatch(t *testing.T) { + exp := QueryResult{Columns: []string{"id"}, Rows: [][]any{{1}}} + act := QueryResult{Columns: []string{"ID"}, Rows: [][]any{{1}}} + v := Compare(exp, act) + if v.Pass { + t.Fatalf("expected fail") + } +} + +func TestCompare_RowCountMismatch(t *testing.T) { + exp := QueryResult{Columns: []string{"id"}, Rows: [][]any{{1}, {2}}} + act := QueryResult{Columns: []string{"id"}, Rows: [][]any{{1}}} + v := Compare(exp, act) + if v.Pass { + t.Fatalf("expected fail") + } +} diff --git a/api/internal/mockdata/mockdata.go b/api/internal/mockdata/mockdata.go new file mode 100644 index 0000000..8136d9f --- /dev/null +++ b/api/internal/mockdata/mockdata.go @@ -0,0 +1,127 @@ +package mockdata + +import ( + "database/sql" +) + +type MockDatabase struct { + Key string `json:"key"` + Name string `json:"name"` + Description string `json:"description"` + SchemaName string + InitSQL string +} + +func List() []MockDatabase { + return []MockDatabase{ + { + Key: "shop", + Name: "电商库", + Description: "商品/订单/用户", + SchemaName: "mock_shop", + InitSQL: `CREATE TABLE IF NOT EXISTS users ( + id INT PRIMARY KEY, + name VARCHAR(64) NOT NULL +); + +CREATE TABLE IF NOT EXISTS products ( + id INT PRIMARY KEY, + name VARCHAR(128) NOT NULL, + price DECIMAL(10,2) NOT NULL +); + +CREATE TABLE IF NOT EXISTS orders ( + id INT PRIMARY KEY, + user_id INT NOT NULL, + created_at DATETIME NOT NULL +); + +CREATE TABLE IF NOT EXISTS order_items ( + id INT PRIMARY KEY, + order_id INT NOT NULL, + product_id INT NOT NULL, + quantity INT NOT NULL, + unit_price DECIMAL(10,2) NOT NULL +); + +DELETE FROM users; +DELETE FROM products; +DELETE FROM orders; +DELETE FROM order_items; + +INSERT INTO users (id, name) VALUES + (1, 'Alice'), + (2, 'Bob'), + (3, 'Carol'); + +INSERT INTO products (id, name, price) VALUES + (1, 'Keyboard', 199.00), + (2, 'Mouse', 49.00), + (3, 'Monitor', 899.00), + (4, 'USB-C Cable', 19.00), + (5, 'Laptop Stand', 129.00); + +INSERT INTO orders (id, user_id, created_at) VALUES + (1, 1, '2025-01-01 10:00:00'), + (2, 1, '2025-01-05 12:00:00'), + (3, 2, '2025-02-10 09:30:00'); + +INSERT INTO order_items (id, order_id, product_id, quantity, unit_price) VALUES + (1, 1, 1, 1, 199.00), + (2, 1, 2, 2, 49.00), + (3, 2, 3, 1, 899.00), + (4, 2, 4, 3, 19.00), + (5, 3, 2, 1, 49.00), + (6, 3, 5, 1, 129.00); +`, + }, + { + Key: "hr", + Name: "人事库", + Description: "部门/员工/薪资", + SchemaName: "mock_hr", + InitSQL: `CREATE TABLE IF NOT EXISTS departments ( + id INT PRIMARY KEY, + name VARCHAR(64) NOT NULL +); + +CREATE TABLE IF NOT EXISTS employees ( + id INT PRIMARY KEY, + department_id INT NOT NULL, + name VARCHAR(64) NOT NULL, + salary INT NOT NULL +); + +DELETE FROM employees; +DELETE FROM departments; + +INSERT INTO departments (id, name) VALUES + (1, 'Engineering'), + (2, 'Sales'), + (3, 'HR'); + +INSERT INTO employees (id, department_id, name, salary) VALUES + (1, 1, 'Eve', 30000), + (2, 1, 'Mallory', 42000), + (3, 2, 'Trent', 28000), + (4, 2, 'Peggy', 35000), + (5, 3, 'Victor', 26000); +`, + }, + } +} + +func EnsureMockSchemas(mysql *sql.DB) error { + for _, m := range List() { + if _, err := mysql.Exec("CREATE DATABASE IF NOT EXISTS " + m.SchemaName + " CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci"); err != nil { + return err + } + if _, err := mysql.Exec("USE " + m.SchemaName); err != nil { + return err + } + if _, err := mysql.Exec(m.InitSQL); err != nil { + return err + } + } + return nil +} diff --git a/api/main.go b/api/main.go new file mode 100644 index 0000000..53b3599 --- /dev/null +++ b/api/main.go @@ -0,0 +1,116 @@ +package main + +import ( + "log" + "net/http" + "os" + "strings" + "time" + + "github.com/gin-contrib/cors" + "github.com/gin-gonic/gin" + + "query-database/api/internal/auth" + "query-database/api/internal/config" + "query-database/api/internal/db" + "query-database/api/internal/handlers" + "query-database/api/internal/mockdata" +) + +func main() { + cfg := config.Load() + + sqliteDB, err := db.OpenSQLite(cfg.SQLitePath) + if err != nil { + log.Fatal(err) + } + if err := db.MigrateSQLite(sqliteDB); err != nil { + log.Fatal(err) + } + if err := db.SeedSQLite(sqliteDB); err != nil { + log.Fatal(err) + } + + mysqlDB, err := db.OpenMySQL(cfg) + if err != nil { + log.Fatal(err) + } + if err := mockdata.EnsureMockSchemas(mysqlDB); err != nil { + log.Fatal(err) + } + + a := auth.New(cfg.JWTSecret) + h := handlers.New(handlers.Deps{ + Cfg: cfg, + Auth: a, + SQLite: sqliteDB, + MySQL: mysqlDB, + }) + + r := gin.New() + r.Use(gin.Recovery()) + r.Use(gin.LoggerWithFormatter(func(p gin.LogFormatterParams) string { + return "" + })) + + allowOrigins := []string{"http://localhost:5173"} + if strings.TrimSpace(cfg.CORSAllowAll) == "1" { + allowOrigins = []string{"*"} + } else if strings.TrimSpace(cfg.CORSOrigins) != "" { + parts := strings.Split(cfg.CORSOrigins, ",") + out := make([]string, 0, len(parts)) + for _, p := range parts { + v := strings.TrimSpace(p) + if v != "" { + out = append(out, v) + } + } + if len(out) > 0 { + allowOrigins = out + } + } + + r.Use(cors.New(cors.Config{ + AllowOrigins: allowOrigins, + AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"}, + AllowHeaders: []string{"Authorization", "Content-Type"}, + AllowCredentials: false, + MaxAge: 12 * time.Hour, + })) + + r.GET("/health", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"ok": true}) + }) + + api := r.Group("/api") + api.POST("/auth/register", h.Register) + api.POST("/auth/login", h.Login) + + api.GET("/mock-databases", a.RequireAuth(sqliteDB), h.ListMockDatabases) + + authed := api.Group("") + authed.Use(a.RequireAuth(sqliteDB)) + authed.GET("/me", h.Me) + authed.POST("/onboarding/complete", h.CompleteOnboarding) + authed.POST("/module/switch", h.SwitchModule) + + authed.GET("/exercises", h.ListExercises) + authed.GET("/exercises/:id", h.GetExercise) + + authed.POST("/sql/execute", h.ExecuteSQL) + authed.POST("/progress/upsert", h.UpsertProgress) + + authed.GET("/user-databases", h.ListUserDatabases) + authed.POST("/user-databases/activate", h.ActivateUserDatabase) + authed.POST("/user-databases/activate-mock", h.ActivateMockDatabase) + authed.POST("/user-databases/import", h.ImportDatabase) + + port := cfg.Port + if port == "" { + port = "8080" + } + if err := r.Run(":" + port); err != nil { + log.Println(err) + os.Exit(1) + } +} diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..e868ec5 --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,35 @@ +services: + mysql: + image: docker.m.daocloud.io/library/mysql:8.4 + environment: + MYSQL_ROOT_PASSWORD: root + ports: + - "3306:3306" + command: + - "--character-set-server=utf8mb4" + - "--collation-server=utf8mb4_general_ci" + volumes: + - mysql_data:/var/lib/mysql + + api: + build: + context: ./api + environment: + PORT: "8080" + MYSQL_HOST: mysql + MYSQL_PORT: "3306" + MYSQL_USER: root + MYSQL_PASSWORD: root + SQLITE_PATH: /app/data/app.db + CORS_ALLOW_ALL: "1" + depends_on: + - mysql + ports: + - "8080:8080" + volumes: + - api_data:/app/data + +volumes: + mysql_data: + api_data: + diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..363a995 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,16 @@ +services: + mysql: + image: docker.m.daocloud.io/library/mysql:8.4 + environment: + MYSQL_ROOT_PASSWORD: root + ports: + - "3306:3306" + command: + - "--character-set-server=utf8mb4" + - "--collation-server=utf8mb4_general_ci" + volumes: + - mysql_data:/var/lib/mysql + +volumes: + mysql_data: + diff --git a/docs/PAGE-MySQL查询练习网站-页面设计.md b/docs/PAGE-MySQL查询练习网站-页面设计.md new file mode 100644 index 0000000..478b144 --- /dev/null +++ b/docs/PAGE-MySQL查询练习网站-页面设计.md @@ -0,0 +1,122 @@ +# MySQL 查询练习网站|页面设计说明(Desktop-first) + +## 1. Global Styles(全局规范) +- 设计基调:学习型工具,信息密度中高,强调“编辑器可读性”和“结果对比清晰”。 +- Design tokens(建议) + - Background: #0B1220(深色)/ #FFFFFF(浅色,可选主题开关,默认深色) + - Primary: #3B82F6;Success: #22C55E;Warning: #F59E0B;Danger: #EF4444 + - Text: 主文 #E5E7EB;次文 #9CA3AF;反白 #111827 + - Font:系统字体 + 等宽(编辑器/SQL 区:ui-monospace) + - 圆角:8px;卡片阴影:轻量;分割线:1px #1F2937 +- 交互状态 + - Button:默认/hover 提亮 6–10%;disabled 降低不透明度并禁用点击 + - Link:hover 下划线;当前路由高亮 +- 响应式(桌面优先) + - ≥1200px:练习页三栏; + - 768–1199px:左右栏折叠为抽屉; + - <768px:编辑器与结果上下堆叠(仍可用)。 + +## 2. Page: 登录/注册页 +### Layout +- 居中单卡片(max-width 420px),背景使用柔和渐变或插画空白区。 + +### Meta Information +- title:登录|MySQL 查询练习 +- description:登录后开始 MySQL 分级练习。 + +### Page Structure +1. 顶部:Logo + 产品一句话 +2. 表单区:邮箱、密码、主按钮(登录/注册切换 Tab) +3. 辅助区:错误提示(表单下方),次按钮(忘记密码/返回首页) + +### Sections & Components +- AuthTabs:登录/注册切换(同一页完成) +- FormField:输入框带校验(必填、邮箱格式) +- FeedbackBanner:展示认证失败原因(如密码错误) + +## 3. Page: 首页(题目与引导) +### Layout +- 顶部导航 + 内容双列:左侧筛选,右侧题目列表(CSS Grid:280px + 1fr)。 + +### Meta Information +- title:题目练习|MySQL 查询练习 +- description:按新手/一般/进阶选择题目进行练习。 + +### Page Structure +1. TopNav(全局):Logo、导航(题目/数据库管理)、用户菜单(退出/重新引导) +2. LeftPanel:分级切换、新手引导入口、状态筛选、搜索框 +3. MainList:题目卡片列表(默认按模块过滤) + 分页/加载更多(任选其一) +4. ContinueCard:继续上次练习(若存在) + +### 强关联展示 +- TopNav 右侧展示“当前数据库”胶囊(名称 + 来源),帮助新手始终知道自己在对哪个库练习。 +- 题目列表仅展示你选择模块对应的题库。 + +### Sections & Components +- LevelSegment:三段式切换(新手/一般/进阶) +- SearchInput:关键词筛选(标题/题干关键字) +- StatusFilter:未做/已做 +- ExerciseCard:标题、难度徽标、完成状态、进入按钮 +- OnboardingTrigger:按钮“新手引导”,点击后弹出引导 + +### 新手引导(Overlay/Modal,不单独页面) +- Step 1:提示去“数据库管理”选择模拟库 +- Step 2:回到首页,选择一题进入练习 +- Step 3:练习页说明“编辑-运行-看结果-判题” +- 支持:跳过/上一步/下一步/完成;完成后记录状态,避免重复打扰 + +## 4. Page: 练习页(SQL 编辑与判题) +### Layout +- 桌面三栏布局(CSS Grid:320px | 1fr | 420px) + - 左:题目与表结构入口 + - 中:编辑器与运行 + - 右:结果/判题/提示 + +### Meta Information +- title:做题|{题目标题}|MySQL 查询练习 +- description:在线编写并运行 MySQL 查询,实时查看结果与判题。 + +### Page Structure +1. TopNav:返回题目列表、当前题目标题、当前激活数据库标识 +2. Left(QuestionPanel) + - 题目描述(支持代码块/要点) + - 目标输出说明(字段/排序/过滤) + - 表结构入口(按钮打开 Drawer/Modal 展示表与字段,只读) +3. Center(EditorPanel) + - Monaco Editor(SQL 高亮) + - 操作区:运行、重置、保存草稿(可自动保存,仅展示状态) + - 运行状态:loading、耗时、错误(语法/权限/超时) +4. Right(ResultPanel) + - 查询结果表格(表头固定、横向滚动) + - 判题结论:通过/未通过 + - 最小提示:例如缺字段、行数不一致、排序不一致(不泄露完整答案) + +### Interaction & States +- Run:触发执行后禁用按钮直到返回;错误时高亮并展示可读信息 +- Pass:展示绿色状态并可“下一题” +- Fail:展示差异点与建议方向(简短) + +### 防误操作(库不匹配) +- 若当前激活数据库与题目要求的数据库不一致: + - 结果区展示醒目提示 + - 提供“一键切换到推荐模拟库” + +## 5. Page: 数据库管理页 +### Layout +- 上下结构:顶部说明 + 两个卡片区(模拟库、导入库),右侧可显示“当前激活库”概览。 + +### Meta Information +- title:数据库管理|MySQL 查询练习 +- description:选择模拟数据库或导入你的自定义数据库用于练习。 + +### Page Structure +1. Header:说明“练习将基于当前激活数据库运行” +2. MockDBSection:模拟库列表卡片(名称、简介、表数量、激活按钮) +3. ImportSection:上传初始化 SQL 文件(建表+数据) +4. ActiveDBSummary:当前激活库信息(名称、来源、导入时间、查看表结构) + +### Sections & Components +- MockDBCard:点击设为激活,成功后 toast 提示 +- UploadInitSql:拖拽上传 + 进度条 + 成功/失败提示(失败给出原因摘要) +- TablePreviewDrawer:只读展示表/字段列表(避免在此页执行 SQL,保持职责清晰) + diff --git a/docs/PRD-MySQL查询练习网站.md b/docs/PRD-MySQL查询练习网站.md new file mode 100644 index 0000000..f37c901 --- /dev/null +++ b/docs/PRD-MySQL查询练习网站.md @@ -0,0 +1,51 @@ +## 1. Product Overview +面向初学者到进阶用户的 MySQL 查询练习网站,提供分级题目、可运行的在线 SQL 练习环境与学习引导。 +支持使用内置模拟数据库或导入你的自定义数据库,在真实数据结构上练习查询。 + +新增:通过“模块(电商/人事)”把题库与推荐数据库强绑定,降低新手决策成本。 + +## 2. Core Features + +### 2.1 Feature Module +本产品由以下核心页面构成: +1. **登录/注册页**:账号登录注册、找回入口、登录后跳转。 +2. **首页(题目与引导)**:新手引导入口/首次引导、模块与题目分级筛选、继续上次练习、当前数据库提示。 +3. **练习页(SQL 编辑与判题)**:题目描述、SQL 编辑器、运行与结果展示、答案校验与提示、进度保存。 +4. **数据库管理页**:选择模拟数据库、导入自定义数据库(上传初始化脚本/数据)、查看当前激活数据库。 + +### 2.2 Page Details +| Page Name | Module Name | Feature description | +|-----------|-------------|---------------------| +| 登录/注册页 | 登录注册 | 支持邮箱/密码注册与登录;登录成功后进入首页;展示基础校验与错误提示。 | +| 首页(题目与引导) | 新手引导 | 首次登录自动触发分步引导:如何选库、如何选题、如何运行 SQL;支持“跳过/下次再看/重新查看”。 | +| 首页(题目与引导) | 题目分级与列表 | 按**新手/一般/进阶**分级展示;支持按关键词搜索与按状态(未做/已做)筛选;点击进入练习页。 | +| 首页(题目与引导) | 模块-题库强关联 | 题目默认只展示你选择的模块;切换模块(可选能力)会同步影响题库与推荐模拟库。 | +| 首页(题目与引导) | 继续练习 | 展示你最近一次练习的题目与当前激活数据库,支持一键继续。 | +| 练习页(SQL 编辑与判题) | 题目内容 | 展示题目描述、目标输出说明(字段/排序/过滤要求)、关联的数据库/表信息入口。 | +| 练习页(SQL 编辑与判题) | SQL 编辑与运行 | 提供 SQL 编辑器;运行当前 SQL;展示运行耗时与错误信息;支持重置为上次保存草稿。 | +| 练习页(SQL 编辑与判题) | 结果与校验 | 展示查询结果表格;将结果与标准答案进行对比判定(通过/未通过);未通过时展示最小必要提示(如缺字段/行数不一致)。 | +| 练习页(SQL 编辑与判题) | 进度保存 | 自动保存你的 SQL 草稿与通过状态;返回首页后可按状态筛选。 | +| 数据库管理页 | 模拟数据库选择 | 列出内置模拟数据库(名称、简介、表数量);选择后设为当前激活数据库并影响做题环境。 | +| 数据库管理页 | 导入自定义数据库 | 上传你的初始化 SQL(建表+插入数据);显示导入进度与成功/失败原因;导入成功后可设为激活数据库。 | +| 数据库管理页 | 当前库概览 | 展示当前激活数据库的基本信息(名称、导入时间、表列表入口/只读预览)。 | + +### 2.3 Module Rules(模块规则) +- 模块:`电商(shop)`、`人事(hr)`。 +- 题目:每道题绑定一个 `databaseKey`(例如 `shop` 题只能在电商模拟库中判题)。 +- 推荐库:当你完成首次引导且未激活任何数据库时,系统会自动激活与你模块匹配的模拟库(电商→电商库;人事→人事库)。 +- 防误操作:当你在练习页运行 SQL 时,如果当前激活库与本题不匹配,系统提示“一键切换到推荐库”。 + +## 3. Core Process +- 账号流程:你在登录/注册页完成注册或登录后进入首页;首次登录会弹出新手引导,帮助你完成“选数据库 → 选题 → 运行 SQL → 查看结果”的最短路径。 +- 练习流程:你在首页按新手/一般/进阶选择题目进入练习页;在编辑器输入 SQL 并运行;系统展示结果并进行校验;通过后记录进度,未通过给出必要提示;你可随时返回首页继续下一题。 +- 数据库流程:你在数据库管理页选择一个模拟数据库作为当前练习环境,或上传初始化 SQL 导入自定义数据库;导入成功后可将其设为激活数据库,之后做题运行都基于该数据库。 + +```mermaid +graph TD + A["登录/注册页"] --> B["首页(题目与引导)"] + B --> C["练习页(SQL 编辑与判题)"] + B --> D["数据库管理页"] + D --> B + C --> B +``` + diff --git a/docs/TECH-MySQL查询练习网站-技术架构.md b/docs/TECH-MySQL查询练习网站-技术架构.md new file mode 100644 index 0000000..c53a4b1 --- /dev/null +++ b/docs/TECH-MySQL查询练习网站-技术架构.md @@ -0,0 +1,244 @@ +## 1.Architecture design +```mermaid +graph TD + U["User Browser"] --> F["Vue Frontend Application"] + F --> S["Backend API Service"] + S --> M["MySQL 8 Database"] + S --> P["SQLite (Metadata)"] + + subgraph "Frontend Layer" + F + end + + subgraph "Backend Layer" + S + end + + subgraph "Data Layer" + M + end + + subgraph "Service Layer (Provided by Supabase)" + P + end +``` + +## 2.Technology Description +- Frontend: Vue3 + Vite + TailwindCSS +- Backend: Go + Gin +- Auth: JWT(后端签发与校验) +- Metadata: SQLite(用户、题目、进度、用户数据库列表等) +- SQL Execution Engine: MySQL 8(用于模拟库与用户导入库的执行环境) + +## 3.Route definitions +| Route | Purpose | +|-------|---------| +| /login | 登录/注册页 | +| / | 首页:题目分级列表、新手引导、继续练习 | +| /practice/:id | 练习页:SQL 编辑、运行、结果与校验 | +| /databases | 数据库管理:选择模拟库、导入自定义库、激活当前库 | + +## 4.API definitions (If it includes backend services) +### 4.1 Core Types (TypeScript) +```ts +export type Level = 'beginner' | 'normal' | 'advanced' + +export type ModuleKey = 'shop' | 'hr' + +export type ActiveDatabase = { + id: string + name: string + source: 'mock' | 'imported' + schemaName: string +} + +export type ExerciseSummary = { + id: string + title: string + level: Level + isSolved: boolean +} + +export type ExecuteSqlRequest = { + exerciseId: string + sql: string +} + +export type ExecuteSqlResponse = { + ok: boolean + durationMs: number + columns?: string[] + rows?: Array> + errorMessage?: string + verdict?: 'pass' | 'fail' + hint?: string +} + +export type ImportDatabaseRequest = { + name: string + initSqlFileUrl: string +} + +export type UserDatabase = { + id: string + name: string + source: 'mock' | 'imported' + createdAt: string +} +``` + +### 4.2 Core API +- 获取题目列表 +``` +GET /api/exercises?level=beginner|normal|advanced&query=... +``` + +- 获取题目详情(含描述与校验配置) +``` +GET /api/exercises/:id +``` + +- 执行 SQL + 返回结果 + 判题 +``` +POST /api/sql/execute +``` +Request: ExecuteSqlRequest + +- 保存/更新进度(草稿 SQL、是否通过) +``` +POST /api/progress/upsert +``` + +- 获取/切换当前激活数据库 +``` +GET /api/user-databases +POST /api/user-databases/activate +``` + +- 导入自定义数据库(先上传文件到 Storage,再触发导入) +``` +POST /api/user-databases/import +``` +Request: ImportDatabaseRequest + +- 新手引导状态(是否已完成/已跳过) +``` +GET /api/onboarding +POST /api/onboarding/complete +``` + +## 5.Server architecture diagram (If it includes backend services) +```mermaid +graph TD + A["Client / Frontend"] --> B["API Controller"] + B --> C["Practice Service (Execute + Judge)"] + B --> D["Exercise Service"] + B --> E["Database Import Service"] + C --> F["MySQL Repository"] + D --> G["SQLite Repository (Metadata)"] + E --> F + + subgraph "Server" + B + C + D + E + F + G + end +``` + +## 6.Data model(if applicable) +### 6.1 Data model definition +(SQLite 用于题目/进度/用户库元数据;MySQL 用于实际运行 SQL 的数据面。) + +```mermaid +erDiagram + PROFILE { + uuid id + text email + timestamptz created_at + } + EXERCISE { + uuid id + text title + text level + text prompt + text answer_sql + } + USER_PROGRESS { + uuid id + uuid user_id + uuid exercise_id + text draft_sql + bool is_solved + timestamptz updated_at + } + USER_DATABASE { + uuid id + uuid user_id + text name + text source + text mysql_schema_name + bool is_active + timestamptz created_at + } +``` + +### 6.2 Data Definition Language +Profile Table (profiles) +``` +CREATE TABLE profiles ( + id UUID PRIMARY KEY, + email TEXT, + created_at TIMESTAMPTZ DEFAULT NOW() +); +GRANT SELECT ON profiles TO anon; +GRANT ALL PRIVILEGES ON profiles TO authenticated; +``` + +Exercise Table (exercises) +``` +CREATE TABLE exercises ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + title TEXT NOT NULL, + level TEXT NOT NULL CHECK (level IN ('beginner','normal','advanced')), + prompt TEXT NOT NULL, + answer_sql TEXT NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW() +); +GRANT SELECT ON exercises TO anon; +GRANT ALL PRIVILEGES ON exercises TO authenticated; +``` + +User Progress Table (user_progress) +``` +CREATE TABLE user_progress ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL, + exercise_id UUID NOT NULL, + draft_sql TEXT, + is_solved BOOLEAN DEFAULT FALSE, + updated_at TIMESTAMPTZ DEFAULT NOW() +); +CREATE INDEX idx_user_progress_user_id ON user_progress(user_id); +CREATE INDEX idx_user_progress_exercise_id ON user_progress(exercise_id); +GRANT SELECT ON user_progress TO anon; +GRANT ALL PRIVILEGES ON user_progress TO authenticated; +``` + +User Database Table (user_databases) +``` +CREATE TABLE user_databases ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL, + name TEXT NOT NULL, + source TEXT NOT NULL CHECK (source IN ('mock','imported')), + mysql_schema_name TEXT NOT NULL, + is_active BOOLEAN DEFAULT FALSE, + created_at TIMESTAMPTZ DEFAULT NOW() +); +CREATE INDEX idx_user_databases_user_id ON user_databases(user_id); +GRANT SELECT ON user_databases TO anon +``` + diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..0a64f29 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,33 @@ +import vuePlugin from 'eslint-plugin-vue' +import tsPlugin from '@typescript-eslint/eslint-plugin' +import tsParser from '@typescript-eslint/parser' +import vueParser from 'vue-eslint-parser' + +export default [ + { + ignores: ['dist/**', 'node_modules/**', 'api/**', 'data/**'], + }, + { + files: ['**/*.{ts,vue}'], + languageOptions: { + parser: vueParser, + parserOptions: { + parser: tsParser, + ecmaVersion: 2020, + sourceType: 'module', + }, + }, + plugins: { + vue: vuePlugin, + '@typescript-eslint': tsPlugin, + }, + rules: { + ...(vuePlugin.configs['vue3-recommended']?.rules ?? {}), + ...(tsPlugin.configs.recommended?.rules ?? {}), + 'vue/multi-word-component-names': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], + }, + }, +] + diff --git a/index.html b/index.html new file mode 100644 index 0000000..122e3ea --- /dev/null +++ b/index.html @@ -0,0 +1,24 @@ + + + + + + + My Trae Project + + + +
+ + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..71a3a44 --- /dev/null +++ b/package.json @@ -0,0 +1,43 @@ +{ + "name": "query-database", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vue-tsc -b && vite build", + "preview": "vite preview", + "check": "vue-tsc -b", + "lint": "pnpm exec eslint .", + "lint:fix": "pnpm exec eslint . --fix" + }, + "dependencies": { + "clsx": "^2.1.1", + "lucide-vue-next": "^0.511.0", + "tailwind-merge": "^3.3.0", + "vue": "^3.4.15", + "vue-router": "^4.2.5" + }, + "devDependencies": { + "@types/node": "^22.15.30", + "@typescript-eslint/eslint-plugin": "^7.0.1", + "@typescript-eslint/parser": "^7.0.1", + "@vitejs/plugin-vue": "^5.0.3", + "@vue/runtime-dom": "^3.4.15", + "@vue/tsconfig": "^0.7.0", + "autoprefixer": "^10.4.17", + "eslint": "^8.56.0", + "eslint-plugin-vue": "^9.20.1", + "vue-eslint-parser": "^9.4.3", + "postcss": "^8.4.35", + "tailwindcss": "^3.4.1", + "typescript": "~5.3.3", + "unplugin-vue-dev-locator": "^1.0.0", + "vite": "^5.0.12", + "vite-plugin-trae-solo-badge": "^1.0.0", + "vue-tsc": "^1.8.27" + }, + "pnpm": { + "onlyBuiltDependencies": ["esbuild"] + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..3e5a58a --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,3018 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + clsx: + specifier: ^2.1.1 + version: 2.1.1 + lucide-vue-next: + specifier: ^0.511.0 + version: 0.511.0(vue@3.5.30(typescript@5.3.3)) + tailwind-merge: + specifier: ^3.3.0 + version: 3.5.0 + vue: + specifier: ^3.4.15 + version: 3.5.30(typescript@5.3.3) + vue-router: + specifier: ^4.2.5 + version: 4.6.4(vue@3.5.30(typescript@5.3.3)) + devDependencies: + '@types/node': + specifier: ^22.15.30 + version: 22.19.15 + '@typescript-eslint/eslint-plugin': + specifier: ^7.0.1 + version: 7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.3.3))(eslint@8.57.1)(typescript@5.3.3) + '@typescript-eslint/parser': + specifier: ^7.0.1 + version: 7.18.0(eslint@8.57.1)(typescript@5.3.3) + '@vitejs/plugin-vue': + specifier: ^5.0.3 + version: 5.2.4(vite@5.4.21(@types/node@22.19.15))(vue@3.5.30(typescript@5.3.3)) + '@vue/runtime-dom': + specifier: ^3.4.15 + version: 3.5.30 + '@vue/tsconfig': + specifier: ^0.7.0 + version: 0.7.0(typescript@5.3.3)(vue@3.5.30(typescript@5.3.3)) + autoprefixer: + specifier: ^10.4.17 + version: 10.4.27(postcss@8.5.8) + eslint: + specifier: ^8.56.0 + version: 8.57.1 + eslint-plugin-vue: + specifier: ^9.20.1 + version: 9.33.0(eslint@8.57.1) + postcss: + specifier: ^8.4.35 + version: 8.5.8 + tailwindcss: + specifier: ^3.4.1 + version: 3.4.19 + typescript: + specifier: ~5.3.3 + version: 5.3.3 + unplugin-vue-dev-locator: + specifier: ^1.0.0 + version: 1.0.3(vite@5.4.21(@types/node@22.19.15)) + vite: + specifier: ^5.0.12 + version: 5.4.21(@types/node@22.19.15) + vite-plugin-trae-solo-badge: + specifier: ^1.0.0 + version: 1.0.0(vite@5.4.21(@types/node@22.19.15)) + vue-eslint-parser: + specifier: ^9.4.3 + version: 9.4.3(eslint@8.57.1) + vue-tsc: + specifier: ^1.8.27 + version: 1.8.27(typescript@5.3.3) + +packages: + + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.0': + resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-annotate-as-pure@7.27.3': + resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-create-class-features-plugin@7.28.6': + resolution: {integrity: sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-member-expression-to-functions@7.28.5': + resolution: {integrity: sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-optimise-call-expression@7.27.1': + resolution: {integrity: sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + engines: {node: '>=6.9.0'} + + '@babel/helper-replace-supers@7.28.6': + resolution: {integrity: sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + resolution: {integrity: sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.2': + resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.2': + resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-proposal-decorators@7.29.0': + resolution: {integrity: sha512-CVBVv3VY/XRMxRYq5dwr2DS7/MvqPm23cOCjbwNnVrfOqcWlnefua1uUs0sjdKOGjvPUG633o07uWzJq4oI6dA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-decorators@7.28.6': + resolution: {integrity: sha512-71EYI0ONURHJBL4rSFXnITXqXrrY8q4P0q006DPfN+Rk+ASM+++IBXem/ruokgBZR8YNEWZ8R6B+rCb8VcUTqA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-attributes@7.28.6': + resolution: {integrity: sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-meta@7.10.4': + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-jsx@7.28.6': + resolution: {integrity: sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.28.6': + resolution: {integrity: sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typescript@7.28.6': + resolution: {integrity: sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.23.0': + resolution: {integrity: sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/eslintrc@2.1.4': + resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@eslint/js@8.57.1': + resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@humanwhocodes/config-array@0.13.0': + resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} + engines: {node: '>=10.10.0'} + deprecated: Use @eslint/config-array instead + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/object-schema@2.0.3': + resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} + deprecated: Use @eslint/object-schema instead + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@rollup/rollup-android-arm-eabi@4.60.0': + resolution: {integrity: sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.60.0': + resolution: {integrity: sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.60.0': + resolution: {integrity: sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.60.0': + resolution: {integrity: sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.60.0': + resolution: {integrity: sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.60.0': + resolution: {integrity: sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.60.0': + resolution: {integrity: sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.60.0': + resolution: {integrity: sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.60.0': + resolution: {integrity: sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.60.0': + resolution: {integrity: sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.60.0': + resolution: {integrity: sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.60.0': + resolution: {integrity: sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.60.0': + resolution: {integrity: sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.60.0': + resolution: {integrity: sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.60.0': + resolution: {integrity: sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.60.0': + resolution: {integrity: sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.60.0': + resolution: {integrity: sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.60.0': + resolution: {integrity: sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.60.0': + resolution: {integrity: sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.60.0': + resolution: {integrity: sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.60.0': + resolution: {integrity: sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.60.0': + resolution: {integrity: sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.60.0': + resolution: {integrity: sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.60.0': + resolution: {integrity: sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.60.0': + resolution: {integrity: sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w==} + cpu: [x64] + os: [win32] + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/node@22.19.15': + resolution: {integrity: sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==} + + '@typescript-eslint/eslint-plugin@7.18.0': + resolution: {integrity: sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + '@typescript-eslint/parser': ^7.0.0 + eslint: ^8.56.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/parser@7.18.0': + resolution: {integrity: sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + eslint: ^8.56.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/scope-manager@7.18.0': + resolution: {integrity: sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==} + engines: {node: ^18.18.0 || >=20.0.0} + + '@typescript-eslint/type-utils@7.18.0': + resolution: {integrity: sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + eslint: ^8.56.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/types@7.18.0': + resolution: {integrity: sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==} + engines: {node: ^18.18.0 || >=20.0.0} + + '@typescript-eslint/typescript-estree@7.18.0': + resolution: {integrity: sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/utils@7.18.0': + resolution: {integrity: sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + eslint: ^8.56.0 + + '@typescript-eslint/visitor-keys@7.18.0': + resolution: {integrity: sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==} + engines: {node: ^18.18.0 || >=20.0.0} + + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + + '@vitejs/plugin-vue@5.2.4': + resolution: {integrity: sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==} + engines: {node: ^18.0.0 || >=20.0.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 + vue: ^3.2.25 + + '@volar/language-core@1.11.1': + resolution: {integrity: sha512-dOcNn3i9GgZAcJt43wuaEykSluAuOkQgzni1cuxLxTV0nJKanQztp7FxyswdRILaKH+P2XZMPRp2S4MV/pElCw==} + + '@volar/source-map@1.11.1': + resolution: {integrity: sha512-hJnOnwZ4+WT5iupLRnuzbULZ42L7BWWPMmruzwtLhJfpDVoZLjNBxHDi2sY2bgZXCKlpU5XcsMFoYrsQmPhfZg==} + + '@volar/typescript@1.11.1': + resolution: {integrity: sha512-iU+t2mas/4lYierSnoFOeRFQUhAEMgsFuQxoxvwn5EdQopw43j+J27a4lt9LMInx1gLJBC6qL14WYGlgymaSMQ==} + + '@vue/babel-helper-vue-transform-on@1.5.0': + resolution: {integrity: sha512-0dAYkerNhhHutHZ34JtTl2czVQHUNWv6xEbkdF5W+Yrv5pCWsqjeORdOgbtW2I9gWlt+wBmVn+ttqN9ZxR5tzA==} + + '@vue/babel-plugin-jsx@1.5.0': + resolution: {integrity: sha512-mneBhw1oOqCd2247O0Yw/mRwC9jIGACAJUlawkmMBiNmL4dGA2eMzuNZVNqOUfYTa6vqmND4CtOPzmEEEqLKFw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + peerDependenciesMeta: + '@babel/core': + optional: true + + '@vue/babel-plugin-resolve-type@1.5.0': + resolution: {integrity: sha512-Wm/60o+53JwJODm4Knz47dxJnLDJ9FnKnGZJbUUf8nQRAtt6P+undLUAVU3Ha33LxOJe6IPoifRQ6F/0RrU31w==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@vue/compiler-core@3.5.30': + resolution: {integrity: sha512-s3DfdZkcu/qExZ+td75015ljzHc6vE+30cFMGRPROYjqkroYI5NV2X1yAMX9UeyBNWB9MxCfPcsjpLS11nzkkw==} + + '@vue/compiler-dom@3.5.30': + resolution: {integrity: sha512-eCFYESUEVYHhiMuK4SQTldO3RYxyMR/UQL4KdGD1Yrkfdx4m/HYuZ9jSfPdA+nWJY34VWndiYdW/wZXyiPEB9g==} + + '@vue/compiler-sfc@3.5.30': + resolution: {integrity: sha512-LqmFPDn89dtU9vI3wHJnwaV6GfTRD87AjWpTWpyrdVOObVtjIuSeZr181z5C4PmVx/V3j2p+0f7edFKGRMpQ5A==} + + '@vue/compiler-ssr@3.5.30': + resolution: {integrity: sha512-NsYK6OMTnx109PSL2IAyf62JP6EUdk4Dmj6AkWcJGBvN0dQoMYtVekAmdqgTtWQgEJo+Okstbf/1p7qZr5H+bA==} + + '@vue/devtools-api@6.6.4': + resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==} + + '@vue/language-core@1.8.27': + resolution: {integrity: sha512-L8Kc27VdQserNaCUNiSFdDl9LWT24ly8Hpwf1ECy3aFb9m6bDhBGQYOujDm21N7EW3moKIOKEanQwe1q5BK+mA==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@vue/reactivity@3.5.30': + resolution: {integrity: sha512-179YNgKATuwj9gB+66snskRDOitDiuOZqkYia7mHKJaidOMo/WJxHKF8DuGc4V4XbYTJANlfEKb0yxTQotnx4Q==} + + '@vue/runtime-core@3.5.30': + resolution: {integrity: sha512-e0Z+8PQsUTdwV8TtEsLzUM7SzC7lQwYKePydb7K2ZnmS6jjND+WJXkmmfh/swYzRyfP1EY3fpdesyYoymCzYfg==} + + '@vue/runtime-dom@3.5.30': + resolution: {integrity: sha512-2UIGakjU4WSQ0T4iwDEW0W7vQj6n7AFn7taqZ9Cvm0Q/RA2FFOziLESrDL4GmtI1wV3jXg5nMoJSYO66egDUBw==} + + '@vue/server-renderer@3.5.30': + resolution: {integrity: sha512-v+R34icapydRwbZRD0sXwtHqrQJv38JuMB4JxbOxd8NEpGLny7cncMp53W9UH/zo4j8eDHjQ1dEJXwzFQknjtQ==} + peerDependencies: + vue: 3.5.30 + + '@vue/shared@3.5.30': + resolution: {integrity: sha512-YXgQ7JjaO18NeK2K9VTbDHaFy62WrObMa6XERNfNOkAhD1F1oDSf3ZJ7K6GqabZ0BvSDHajp8qfS5Sa2I9n8uQ==} + + '@vue/tsconfig@0.7.0': + resolution: {integrity: sha512-ku2uNz5MaZ9IerPPUyOHzyjhXoX2kVJaVf7hL315DC17vS6IiZRmmCPfggNbU16QTvM80+uYYy3eYJB59WCtvg==} + peerDependencies: + typescript: 5.x + vue: ^3.4.0 + peerDependenciesMeta: + typescript: + optional: true + vue: + optional: true + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.14.0: + resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + autoprefixer@10.4.27: + resolution: {integrity: sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + baseline-browser-mapping@2.10.10: + resolution: {integrity: sha512-sUoJ3IMxx4AyRqO4MLeHlnGDkyXRoUG0/AI9fjK+vS72ekpV0yWVY7O0BVjmBcRtkNcsAO2QDZ4tdKKGoI6YaQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camelcase-css@2.0.1: + resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} + engines: {node: '>= 6'} + + caniuse-lite@1.0.30001781: + resolution: {integrity: sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + computeds@0.0.1: + resolution: {integrity: sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + de-indent@1.0.2: + resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + didyoumean@1.2.2: + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + dlv@1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + + doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + + electron-to-chromium@1.5.322: + resolution: {integrity: sha512-vFU34OcrvMcH66T+dYC3G4nURmgfDVewMIu6Q2urXpumAPSMmzvcn04KVVV8Opikq8Vs5nUbO/8laNhNRqSzYw==} + + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-plugin-vue@9.33.0: + resolution: {integrity: sha512-174lJKuNsuDIlLpjeXc5E2Tss8P44uIimAfGD0b90k0NoirJqpG7stLuU9Vp/9ioTOrQdWVREc4mRd1BD+CvGw==} + engines: {node: ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 + + eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint@8.57.1: + resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. + hasBin: true + + espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@3.2.0: + resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} + engines: {node: ^10.12.0 || >=12.0.0} + + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + + fraction.js@5.3.4: + resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@7.2.3: + resolution: {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 + + globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + inflight@1.0.6: + resolution: {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. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jiti@1.21.7: + resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} + hasBin: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + kolorist@1.8.0: + resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash@4.17.23: + resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lucide-vue-next@0.511.0: + resolution: {integrity: sha512-VSv0F3pHniGN7JMMzDcLFNMQbl8381+shNnHwV8hi+El7xl2ZL8qdNuzPoiBViKk8mTKK5K3ZDfmE/wEcTZVIQ==} + peerDependencies: + vue: '>=3.0.1' + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + muggle-string@0.3.1: + resolution: {integrity: sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==} + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + node-releases@2.0.36: + resolution: {integrity: sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + postcss-import@15.1.0: + resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} + engines: {node: '>=14.0.0'} + peerDependencies: + postcss: ^8.0.0 + + postcss-js@4.1.0: + resolution: {integrity: sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==} + engines: {node: ^12 || ^14 || >= 16} + peerDependencies: + postcss: ^8.4.21 + + postcss-load-config@6.0.1: + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + engines: {node: '>= 18'} + peerDependencies: + jiti: '>=1.21.0' + postcss: '>=8.0.9' + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + tsx: + optional: true + yaml: + optional: true + + postcss-nested@6.2.0: + resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.2.14 + + postcss-selector-parser@6.1.2: + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} + engines: {node: '>=4'} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.5.8: + resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + read-cache@1.0.0: + resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve@1.22.11: + resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} + engines: {node: '>= 0.4'} + hasBin: true + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + rollup@4.60.0: + resolution: {integrity: sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + sucrase@3.35.1: + resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + tailwind-merge@3.5.0: + resolution: {integrity: sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==} + + tailwindcss@3.4.19: + resolution: {integrity: sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==} + engines: {node: '>=14.0.0'} + hasBin: true + + text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + to-fast-properties@2.0.0: + resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} + engines: {node: '>=4'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + ts-api-utils@1.4.3: + resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==} + engines: {node: '>=16'} + peerDependencies: + typescript: '>=4.2.0' + + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + + typescript@5.3.3: + resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + unplugin-vue-dev-locator@1.0.3: + resolution: {integrity: sha512-pF+qbHcfsUXXg2XzA7mn/uzNweeIr8rW/1LfCgMBP2n1vpLjjJDW0UhrPuQC283ye7+NZGfuDciwPcODVkfemw==} + + unplugin@1.16.1: + resolution: {integrity: sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==} + engines: {node: '>=14.0.0'} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + vite-plugin-trae-solo-badge@1.0.0: + resolution: {integrity: sha512-vrNr4QmbcqqMeJPzHBqef70nqDzN6CQngFwuXTJMAgn3zMU4TWzvY0B3BRdjUjvg/BtvNJLlwu2cPKXTUjalHg==} + peerDependencies: + vite: '>=4.0.0' + + vite-plugin-vue-dev-locator@1.0.3: + resolution: {integrity: sha512-Gcclrry8LlgbJafrv3+PQW1TS1CBJlSdr8w7nechAjCJ6joqticcYw1ovuK+9LvSM11n9ZzDH7afTaC6dzJeQA==} + peerDependencies: + vite: ^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 + + vite@5.4.21: + resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vue-eslint-parser@9.4.3: + resolution: {integrity: sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==} + engines: {node: ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '>=6.0.0' + + vue-router@4.6.4: + resolution: {integrity: sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==} + peerDependencies: + vue: ^3.5.0 + + vue-template-compiler@2.7.16: + resolution: {integrity: sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==} + + vue-tsc@1.8.27: + resolution: {integrity: sha512-WesKCAZCRAbmmhuGl3+VrdWItEvfoFIPXOvUJkjULi+x+6G/Dy69yO3TBRJDr9eUlmsNAwVmxsNZxvHKzbkKdg==} + hasBin: true + peerDependencies: + typescript: '*' + + vue@3.5.30: + resolution: {integrity: sha512-hTHLc6VNZyzzEH/l7PFGjpcTvUgiaPK5mdLkbjrTeWSRcEfxFrv56g/XckIYlE9ckuobsdwqd5mk2g1sBkMewg==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + xml-name-validator@4.0.0: + resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} + engines: {node: '>=12'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + +snapshots: + + '@alloc/quick-lru@5.2.0': {} + + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.0': {} + + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.29.2 + '@babel/parser': 7.29.2 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-annotate-as-pure@7.27.3': + dependencies: + '@babel/types': 7.29.0 + + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.0 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.1 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-create-class-features-plugin@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-member-expression-to-functions': 7.28.5 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0) + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/traverse': 7.29.0 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-member-expression-to-functions@7.28.5': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-optimise-call-expression@7.27.1': + dependencies: + '@babel/types': 7.29.0 + + '@babel/helper-plugin-utils@7.28.6': {} + + '@babel/helper-replace-supers@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-member-expression-to-functions': 7.28.5 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.29.2': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + + '@babel/parser@7.29.2': + dependencies: + '@babel/types': 7.29.0 + + '@babel/plugin-proposal-decorators@7.29.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-syntax-decorators': 7.28.6(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-syntax-decorators@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-import-attributes@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-typescript@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-typescript@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.2 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.23.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + to-fast-properties: 2.0.0 + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@eslint-community/eslint-utils@4.9.1(eslint@8.57.1)': + dependencies: + eslint: 8.57.1 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/eslintrc@2.1.4': + dependencies: + ajv: 6.14.0 + debug: 4.4.3 + espree: 9.6.1 + globals: 13.24.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@8.57.1': {} + + '@humanwhocodes/config-array@0.13.0': + dependencies: + '@humanwhocodes/object-schema': 2.0.3 + debug: 4.4.3 + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/object-schema@2.0.3': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@rollup/rollup-android-arm-eabi@4.60.0': + optional: true + + '@rollup/rollup-android-arm64@4.60.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.60.0': + optional: true + + '@rollup/rollup-darwin-x64@4.60.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.60.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.60.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.60.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.60.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.60.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.60.0': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.60.0': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.60.0': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.60.0': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.60.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.60.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.60.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.60.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.60.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.60.0': + optional: true + + '@rollup/rollup-openbsd-x64@4.60.0': + optional: true + + '@rollup/rollup-openharmony-arm64@4.60.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.60.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.60.0': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.60.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.60.0': + optional: true + + '@types/estree@1.0.8': {} + + '@types/node@22.19.15': + dependencies: + undici-types: 6.21.0 + + '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.3.3))(eslint@8.57.1)(typescript@5.3.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 7.18.0(eslint@8.57.1)(typescript@5.3.3) + '@typescript-eslint/scope-manager': 7.18.0 + '@typescript-eslint/type-utils': 7.18.0(eslint@8.57.1)(typescript@5.3.3) + '@typescript-eslint/utils': 7.18.0(eslint@8.57.1)(typescript@5.3.3) + '@typescript-eslint/visitor-keys': 7.18.0 + eslint: 8.57.1 + graphemer: 1.4.0 + ignore: 5.3.2 + natural-compare: 1.4.0 + ts-api-utils: 1.4.3(typescript@5.3.3) + optionalDependencies: + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.3.3)': + dependencies: + '@typescript-eslint/scope-manager': 7.18.0 + '@typescript-eslint/types': 7.18.0 + '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.3.3) + '@typescript-eslint/visitor-keys': 7.18.0 + debug: 4.4.3 + eslint: 8.57.1 + optionalDependencies: + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@7.18.0': + dependencies: + '@typescript-eslint/types': 7.18.0 + '@typescript-eslint/visitor-keys': 7.18.0 + + '@typescript-eslint/type-utils@7.18.0(eslint@8.57.1)(typescript@5.3.3)': + dependencies: + '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.3.3) + '@typescript-eslint/utils': 7.18.0(eslint@8.57.1)(typescript@5.3.3) + debug: 4.4.3 + eslint: 8.57.1 + ts-api-utils: 1.4.3(typescript@5.3.3) + optionalDependencies: + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@7.18.0': {} + + '@typescript-eslint/typescript-estree@7.18.0(typescript@5.3.3)': + dependencies: + '@typescript-eslint/types': 7.18.0 + '@typescript-eslint/visitor-keys': 7.18.0 + debug: 4.4.3 + globby: 11.1.0 + is-glob: 4.0.3 + minimatch: 9.0.9 + semver: 7.7.4 + ts-api-utils: 1.4.3(typescript@5.3.3) + optionalDependencies: + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@7.18.0(eslint@8.57.1)(typescript@5.3.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1) + '@typescript-eslint/scope-manager': 7.18.0 + '@typescript-eslint/types': 7.18.0 + '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.3.3) + eslint: 8.57.1 + transitivePeerDependencies: + - supports-color + - typescript + + '@typescript-eslint/visitor-keys@7.18.0': + dependencies: + '@typescript-eslint/types': 7.18.0 + eslint-visitor-keys: 3.4.3 + + '@ungap/structured-clone@1.3.0': {} + + '@vitejs/plugin-vue@5.2.4(vite@5.4.21(@types/node@22.19.15))(vue@3.5.30(typescript@5.3.3))': + dependencies: + vite: 5.4.21(@types/node@22.19.15) + vue: 3.5.30(typescript@5.3.3) + + '@volar/language-core@1.11.1': + dependencies: + '@volar/source-map': 1.11.1 + + '@volar/source-map@1.11.1': + dependencies: + muggle-string: 0.3.1 + + '@volar/typescript@1.11.1': + dependencies: + '@volar/language-core': 1.11.1 + path-browserify: 1.0.1 + + '@vue/babel-helper-vue-transform-on@1.5.0': {} + + '@vue/babel-plugin-jsx@1.5.0(@babel/core@7.29.0)': + dependencies: + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@vue/babel-helper-vue-transform-on': 1.5.0 + '@vue/babel-plugin-resolve-type': 1.5.0(@babel/core@7.29.0) + '@vue/shared': 3.5.30 + optionalDependencies: + '@babel/core': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@vue/babel-plugin-resolve-type@1.5.0(@babel/core@7.29.0)': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/parser': 7.29.2 + '@vue/compiler-sfc': 3.5.30 + transitivePeerDependencies: + - supports-color + + '@vue/compiler-core@3.5.30': + dependencies: + '@babel/parser': 7.29.2 + '@vue/shared': 3.5.30 + entities: 7.0.1 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.30': + dependencies: + '@vue/compiler-core': 3.5.30 + '@vue/shared': 3.5.30 + + '@vue/compiler-sfc@3.5.30': + dependencies: + '@babel/parser': 7.29.2 + '@vue/compiler-core': 3.5.30 + '@vue/compiler-dom': 3.5.30 + '@vue/compiler-ssr': 3.5.30 + '@vue/shared': 3.5.30 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.8 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.30': + dependencies: + '@vue/compiler-dom': 3.5.30 + '@vue/shared': 3.5.30 + + '@vue/devtools-api@6.6.4': {} + + '@vue/language-core@1.8.27(typescript@5.3.3)': + dependencies: + '@volar/language-core': 1.11.1 + '@volar/source-map': 1.11.1 + '@vue/compiler-dom': 3.5.30 + '@vue/shared': 3.5.30 + computeds: 0.0.1 + minimatch: 9.0.9 + muggle-string: 0.3.1 + path-browserify: 1.0.1 + vue-template-compiler: 2.7.16 + optionalDependencies: + typescript: 5.3.3 + + '@vue/reactivity@3.5.30': + dependencies: + '@vue/shared': 3.5.30 + + '@vue/runtime-core@3.5.30': + dependencies: + '@vue/reactivity': 3.5.30 + '@vue/shared': 3.5.30 + + '@vue/runtime-dom@3.5.30': + dependencies: + '@vue/reactivity': 3.5.30 + '@vue/runtime-core': 3.5.30 + '@vue/shared': 3.5.30 + csstype: 3.2.3 + + '@vue/server-renderer@3.5.30(vue@3.5.30(typescript@5.3.3))': + dependencies: + '@vue/compiler-ssr': 3.5.30 + '@vue/shared': 3.5.30 + vue: 3.5.30(typescript@5.3.3) + + '@vue/shared@3.5.30': {} + + '@vue/tsconfig@0.7.0(typescript@5.3.3)(vue@3.5.30(typescript@5.3.3))': + optionalDependencies: + typescript: 5.3.3 + vue: 3.5.30(typescript@5.3.3) + + acorn-jsx@5.3.2(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + + acorn@8.16.0: {} + + ajv@6.14.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + any-promise@1.3.0: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.2 + + arg@5.0.2: {} + + argparse@2.0.1: {} + + array-union@2.1.0: {} + + autoprefixer@10.4.27(postcss@8.5.8): + dependencies: + browserslist: 4.28.1 + caniuse-lite: 1.0.30001781 + fraction.js: 5.3.4 + picocolors: 1.1.1 + postcss: 8.5.8 + postcss-value-parser: 4.2.0 + + balanced-match@1.0.2: {} + + baseline-browser-mapping@2.10.10: {} + + binary-extensions@2.3.0: {} + + boolbase@1.0.0: {} + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.28.1: + dependencies: + baseline-browser-mapping: 2.10.10 + caniuse-lite: 1.0.30001781 + electron-to-chromium: 1.5.322 + node-releases: 2.0.36 + update-browserslist-db: 1.2.3(browserslist@4.28.1) + + callsites@3.1.0: {} + + camelcase-css@2.0.1: {} + + caniuse-lite@1.0.30001781: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + clsx@2.1.1: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + commander@4.1.1: {} + + computeds@0.0.1: {} + + concat-map@0.0.1: {} + + convert-source-map@2.0.0: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + cssesc@3.0.0: {} + + csstype@3.2.3: {} + + de-indent@1.0.2: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-is@0.1.4: {} + + didyoumean@1.2.2: {} + + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + dlv@1.1.3: {} + + doctrine@3.0.0: + dependencies: + esutils: 2.0.3 + + electron-to-chromium@1.5.322: {} + + entities@7.0.1: {} + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + escalade@3.2.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-plugin-vue@9.33.0(eslint@8.57.1): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1) + eslint: 8.57.1 + globals: 13.24.0 + natural-compare: 1.4.0 + nth-check: 2.1.1 + postcss-selector-parser: 6.1.2 + semver: 7.7.4 + vue-eslint-parser: 9.4.3(eslint@8.57.1) + xml-name-validator: 4.0.0 + transitivePeerDependencies: + - supports-color + + eslint-scope@7.2.2: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint@8.57.1: + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1) + '@eslint-community/regexpp': 4.12.2 + '@eslint/eslintrc': 2.1.4 + '@eslint/js': 8.57.1 + '@humanwhocodes/config-array': 0.13.0 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + '@ungap/structured-clone': 1.3.0 + ajv: 6.14.0 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.24.0 + graphemer: 1.4.0 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.1.1 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + + espree@9.6.1: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 3.4.3 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@2.0.2: {} + + esutils@2.0.3: {} + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + file-entry-cache@6.0.1: + dependencies: + flat-cache: 3.2.0 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@3.2.0: + dependencies: + flatted: 3.4.2 + keyv: 4.5.4 + rimraf: 3.0.2 + + flatted@3.4.2: {} + + fraction.js@5.3.4: {} + + fs.realpath@1.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + gensync@1.0.0-beta.2: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.5 + once: 1.4.0 + path-is-absolute: 1.0.1 + + globals@13.24.0: + dependencies: + type-fest: 0.20.2 + + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + + graphemer@1.4.0: {} + + has-flag@4.0.0: {} + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + he@1.2.0: {} + + ignore@5.3.2: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + is-path-inside@3.0.3: {} + + isexe@2.0.0: {} + + jiti@1.21.7: {} + + js-tokens@4.0.0: {} + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@2.2.3: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + kolorist@1.8.0: {} + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lilconfig@3.1.3: {} + + lines-and-columns@1.2.4: {} + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + lodash@4.17.23: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lucide-vue-next@0.511.0(vue@3.5.30(typescript@5.3.3)): + dependencies: + vue: 3.5.30(typescript@5.3.3) + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.12 + + minimatch@9.0.9: + dependencies: + brace-expansion: 2.0.2 + + ms@2.1.3: {} + + muggle-string@0.3.1: {} + + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + nanoid@3.3.11: {} + + natural-compare@1.4.0: {} + + node-releases@2.0.36: {} + + normalize-path@3.0.0: {} + + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + + object-assign@4.1.1: {} + + object-hash@3.0.0: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + path-browserify@1.0.1: {} + + path-exists@4.0.0: {} + + path-is-absolute@1.0.1: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + path-type@4.0.0: {} + + picocolors@1.1.1: {} + + picomatch@2.3.2: {} + + picomatch@4.0.4: {} + + pify@2.3.0: {} + + pirates@4.0.7: {} + + postcss-import@15.1.0(postcss@8.5.8): + dependencies: + postcss: 8.5.8 + postcss-value-parser: 4.2.0 + read-cache: 1.0.0 + resolve: 1.22.11 + + postcss-js@4.1.0(postcss@8.5.8): + dependencies: + camelcase-css: 2.0.1 + postcss: 8.5.8 + + postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.8): + dependencies: + lilconfig: 3.1.3 + optionalDependencies: + jiti: 1.21.7 + postcss: 8.5.8 + + postcss-nested@6.2.0(postcss@8.5.8): + dependencies: + postcss: 8.5.8 + postcss-selector-parser: 6.1.2 + + postcss-selector-parser@6.1.2: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-value-parser@4.2.0: {} + + postcss@8.5.8: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + punycode@2.3.1: {} + + queue-microtask@1.2.3: {} + + read-cache@1.0.0: + dependencies: + pify: 2.3.0 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.2 + + resolve-from@4.0.0: {} + + resolve@1.22.11: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + reusify@1.1.0: {} + + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + + rollup@4.60.0: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.60.0 + '@rollup/rollup-android-arm64': 4.60.0 + '@rollup/rollup-darwin-arm64': 4.60.0 + '@rollup/rollup-darwin-x64': 4.60.0 + '@rollup/rollup-freebsd-arm64': 4.60.0 + '@rollup/rollup-freebsd-x64': 4.60.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.60.0 + '@rollup/rollup-linux-arm-musleabihf': 4.60.0 + '@rollup/rollup-linux-arm64-gnu': 4.60.0 + '@rollup/rollup-linux-arm64-musl': 4.60.0 + '@rollup/rollup-linux-loong64-gnu': 4.60.0 + '@rollup/rollup-linux-loong64-musl': 4.60.0 + '@rollup/rollup-linux-ppc64-gnu': 4.60.0 + '@rollup/rollup-linux-ppc64-musl': 4.60.0 + '@rollup/rollup-linux-riscv64-gnu': 4.60.0 + '@rollup/rollup-linux-riscv64-musl': 4.60.0 + '@rollup/rollup-linux-s390x-gnu': 4.60.0 + '@rollup/rollup-linux-x64-gnu': 4.60.0 + '@rollup/rollup-linux-x64-musl': 4.60.0 + '@rollup/rollup-openbsd-x64': 4.60.0 + '@rollup/rollup-openharmony-arm64': 4.60.0 + '@rollup/rollup-win32-arm64-msvc': 4.60.0 + '@rollup/rollup-win32-ia32-msvc': 4.60.0 + '@rollup/rollup-win32-x64-gnu': 4.60.0 + '@rollup/rollup-win32-x64-msvc': 4.60.0 + fsevents: 2.3.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + semver@6.3.1: {} + + semver@7.7.4: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + slash@3.0.0: {} + + source-map-js@1.2.1: {} + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-json-comments@3.1.1: {} + + sucrase@3.35.1: + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + commander: 4.1.1 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.7 + tinyglobby: 0.2.15 + ts-interface-checker: 0.1.13 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + tailwind-merge@3.5.0: {} + + tailwindcss@3.4.19: + dependencies: + '@alloc/quick-lru': 5.2.0 + arg: 5.0.2 + chokidar: 3.6.0 + didyoumean: 1.2.2 + dlv: 1.1.3 + fast-glob: 3.3.3 + glob-parent: 6.0.2 + is-glob: 4.0.3 + jiti: 1.21.7 + lilconfig: 3.1.3 + micromatch: 4.0.8 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.1.1 + postcss: 8.5.8 + postcss-import: 15.1.0(postcss@8.5.8) + postcss-js: 4.1.0(postcss@8.5.8) + postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.8) + postcss-nested: 6.2.0(postcss@8.5.8) + postcss-selector-parser: 6.1.2 + resolve: 1.22.11 + sucrase: 3.35.1 + transitivePeerDependencies: + - tsx + - yaml + + text-table@0.2.0: {} + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + to-fast-properties@2.0.0: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + ts-api-utils@1.4.3(typescript@5.3.3): + dependencies: + typescript: 5.3.3 + + ts-interface-checker@0.1.13: {} + + tslib@2.8.1: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-fest@0.20.2: {} + + typescript@5.3.3: {} + + undici-types@6.21.0: {} + + unplugin-vue-dev-locator@1.0.3(vite@5.4.21(@types/node@22.19.15)): + dependencies: + kolorist: 1.8.0 + unplugin: 1.16.1 + vite-plugin-vue-dev-locator: 1.0.3(vite@5.4.21(@types/node@22.19.15)) + transitivePeerDependencies: + - supports-color + - vite + + unplugin@1.16.1: + dependencies: + acorn: 8.16.0 + webpack-virtual-modules: 0.6.2 + + update-browserslist-db@1.2.3(browserslist@4.28.1): + dependencies: + browserslist: 4.28.1 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + util-deprecate@1.0.2: {} + + vite-plugin-trae-solo-badge@1.0.0(vite@5.4.21(@types/node@22.19.15)): + dependencies: + tslib: 2.8.1 + vite: 5.4.21(@types/node@22.19.15) + + vite-plugin-vue-dev-locator@1.0.3(vite@5.4.21(@types/node@22.19.15)): + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-proposal-decorators': 7.29.0(@babel/core@7.29.0) + '@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.29.0) + '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0) + '@babel/types': 7.23.0 + '@vue/babel-plugin-jsx': 1.5.0(@babel/core@7.29.0) + '@vue/compiler-dom': 3.5.30 + kolorist: 1.8.0 + magic-string: 0.30.21 + vite: 5.4.21(@types/node@22.19.15) + transitivePeerDependencies: + - supports-color + + vite@5.4.21(@types/node@22.19.15): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.8 + rollup: 4.60.0 + optionalDependencies: + '@types/node': 22.19.15 + fsevents: 2.3.3 + + vue-eslint-parser@9.4.3(eslint@8.57.1): + dependencies: + debug: 4.4.3 + eslint: 8.57.1 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.7.0 + lodash: 4.17.23 + semver: 7.7.4 + transitivePeerDependencies: + - supports-color + + vue-router@4.6.4(vue@3.5.30(typescript@5.3.3)): + dependencies: + '@vue/devtools-api': 6.6.4 + vue: 3.5.30(typescript@5.3.3) + + vue-template-compiler@2.7.16: + dependencies: + de-indent: 1.0.2 + he: 1.2.0 + + vue-tsc@1.8.27(typescript@5.3.3): + dependencies: + '@volar/typescript': 1.11.1 + '@vue/language-core': 1.8.27(typescript@5.3.3) + semver: 7.7.4 + typescript: 5.3.3 + + vue@3.5.30(typescript@5.3.3): + dependencies: + '@vue/compiler-dom': 3.5.30 + '@vue/compiler-sfc': 3.5.30 + '@vue/runtime-dom': 3.5.30 + '@vue/server-renderer': 3.5.30(vue@3.5.30(typescript@5.3.3)) + '@vue/shared': 3.5.30 + optionalDependencies: + typescript: 5.3.3 + + webpack-virtual-modules@0.6.2: {} + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + wrappy@1.0.2: {} + + xml-name-validator@4.0.0: {} + + yallist@3.1.1: {} + + yocto-queue@0.1.0: {} diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..1d8a859 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,10 @@ +/** WARNING: DON'T EDIT THIS FILE */ +/** WARNING: DON'T EDIT THIS FILE */ +/** WARNING: DON'T EDIT THIS FILE */ + +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..c04c3c1 --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/App.vue b/src/App.vue new file mode 100644 index 0000000..87ed26e --- /dev/null +++ b/src/App.vue @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/src/assets/vue.svg b/src/assets/vue.svg new file mode 100644 index 0000000..770e9d3 --- /dev/null +++ b/src/assets/vue.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/Empty.vue b/src/components/Empty.vue new file mode 100644 index 0000000..48659e7 --- /dev/null +++ b/src/components/Empty.vue @@ -0,0 +1,3 @@ + diff --git a/src/components/ModuleSwitchDialog.vue b/src/components/ModuleSwitchDialog.vue new file mode 100644 index 0000000..b6cb62f --- /dev/null +++ b/src/components/ModuleSwitchDialog.vue @@ -0,0 +1,167 @@ + + + + diff --git a/src/components/TopNav.vue b/src/components/TopNav.vue new file mode 100644 index 0000000..8a9581c --- /dev/null +++ b/src/components/TopNav.vue @@ -0,0 +1,93 @@ + + + + diff --git a/src/composables/useAuth.ts b/src/composables/useAuth.ts new file mode 100644 index 0000000..e7b0bff --- /dev/null +++ b/src/composables/useAuth.ts @@ -0,0 +1,85 @@ +import { http } from '@/lib/http' +import { computed, reactive } from 'vue' + +export type Me = { + id: string + email: string + name: string + moduleKey: string + experienceLevel: 'beginner' | 'normal' | 'advanced' + onboardingCompleted: boolean + activeDatabaseId: string | null + activeDatabase: { + id: string + name: string + source: 'mock' | 'imported' + schemaName: string + } | null +} + +type AuthState = { + token: string | null + me: Me | null + meLoading: boolean +} + +const state = reactive({ + token: localStorage.getItem('token'), + me: null, + meLoading: false, +}) + +export function useAuthState() { + return state +} + +export function getToken() { + return state.token +} + +export function setToken(token: string | null) { + state.token = token + if (token) localStorage.setItem('token', token) + else localStorage.removeItem('token') +} + +export async function refreshMe() { + const token = state.token + if (!token) { + state.me = null + return + } + + state.meLoading = true + try { + state.me = await http('/api/me', { token }) + } finally { + state.meLoading = false + } +} + +export async function logout() { + setToken(null) + state.me = null +} + +export async function login(email: string, password: string) { + const res = await http<{ token: string }>('/api/auth/login', { + method: 'POST', + body: { email, password }, + }) + setToken(res.token) + await refreshMe() +} + +export async function register(email: string, password: string) { + const res = await http<{ token: string }>('/api/auth/register', { + method: 'POST', + body: { email, password }, + }) + setToken(res.token) + await refreshMe() +} + +export const isAuthed = computed(() => !!state.token) + diff --git a/src/composables/useTheme.ts b/src/composables/useTheme.ts new file mode 100644 index 0000000..83086ed --- /dev/null +++ b/src/composables/useTheme.ts @@ -0,0 +1,40 @@ +import { ref, watchEffect, onMounted, computed } from 'vue' + +type Theme = 'light' | 'dark' + +export function useTheme() { + const theme = ref('light') + + const getPreferredTheme = (): Theme => { + const saved = localStorage.getItem('theme') as Theme | null + if (saved === 'light' || saved === 'dark') return saved + return window.matchMedia('(prefers-color-scheme: dark)').matches + ? 'dark' + : 'light' + } + + const applyTheme = (t: Theme) => { + document.documentElement.classList.remove('light', 'dark') + document.documentElement.classList.add(t) + localStorage.setItem('theme', t) + } + + const toggleTheme = () => { + theme.value = theme.value === 'light' ? 'dark' : 'light' + } + + onMounted(() => { + theme.value = getPreferredTheme() + applyTheme(theme.value) + }) + + watchEffect(() => { + applyTheme(theme.value) + }) + + return { + theme, + toggleTheme, + isDark: computed(() => theme.value === 'dark'), + } +} diff --git a/src/lib/domain.ts b/src/lib/domain.ts new file mode 100644 index 0000000..dae5171 --- /dev/null +++ b/src/lib/domain.ts @@ -0,0 +1,18 @@ +export type ModuleKey = 'shop' | 'hr' + +export const MODULES: Array<{ key: ModuleKey; name: string; recommendedMockKey: ModuleKey }> = [ + { key: 'shop', name: '电商', recommendedMockKey: 'shop' }, + { key: 'hr', name: '人事', recommendedMockKey: 'hr' }, +] + +export function moduleName(key: string | null | undefined) { + const m = MODULES.find((x) => x.key === key) + return m ? m.name : '-' +} + +export function recommendedDatabaseNameForModule(key: string | null | undefined) { + if (key === 'shop') return '电商库' + if (key === 'hr') return '人事库' + return '-' +} + diff --git a/src/lib/http.ts b/src/lib/http.ts new file mode 100644 index 0000000..fc0f0de --- /dev/null +++ b/src/lib/http.ts @@ -0,0 +1,68 @@ +export type HttpError = { + status: number + message: string +} + +function normalizeBaseUrl(input: string | undefined) { + const v = (input || '').trim() + if (!v) return '' + return v.endsWith('/') ? v.slice(0, -1) : v +} + +const API_BASE_URL = normalizeBaseUrl((import.meta as any).env?.VITE_API_BASE_URL as string | undefined) + +function resolveUrl(url: string) { + if (!API_BASE_URL) return url + if (url.startsWith('http://') || url.startsWith('https://')) return url + if (!url.startsWith('/')) return url + return `${API_BASE_URL}${url}` +} + +async function readErrorMessage(res: Response) { + try { + const data = (await res.json()) as unknown + if (data && typeof data === 'object' && 'message' in data && typeof (data as any).message === 'string') { + return (data as any).message as string + } + } catch { + } + return `${res.status} ${res.statusText}` +} + +export async function http( + url: string, + opts: { + method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' + token?: string | null + body?: unknown + formData?: FormData + } = {}, +): Promise { + const finalUrl = resolveUrl(url) + const headers: Record = {} + if (opts.token) headers.Authorization = `Bearer ${opts.token}` + + let body: BodyInit | undefined + if (opts.formData) { + body = opts.formData + } else if (opts.body !== undefined) { + headers['Content-Type'] = 'application/json' + body = JSON.stringify(opts.body) + } + + const res = await fetch(finalUrl, { + method: opts.method ?? 'GET', + headers, + body, + }) + + if (!res.ok) { + const message = await readErrorMessage(res) + const err: HttpError = { status: res.status, message } + throw err + } + + if (res.status === 204) return undefined as T + return (await res.json()) as T +} + diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100644 index 0000000..bd0c391 --- /dev/null +++ b/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..b0019fe --- /dev/null +++ b/src/main.ts @@ -0,0 +1,13 @@ +import { createApp } from 'vue' +import './style.css' +import App from './App.vue' +import router from './router' + +// 创建Vue应用实例 +const app = createApp(App) + +// 使用路由 +app.use(router) + +// 挂载应用 +app.mount('#app') diff --git a/src/pages/DatabasesPage.vue b/src/pages/DatabasesPage.vue new file mode 100644 index 0000000..c93993b --- /dev/null +++ b/src/pages/DatabasesPage.vue @@ -0,0 +1,177 @@ + + + + diff --git a/src/pages/HomePage.vue b/src/pages/HomePage.vue new file mode 100644 index 0000000..52c267f --- /dev/null +++ b/src/pages/HomePage.vue @@ -0,0 +1,224 @@ + + + diff --git a/src/pages/LoginPage.vue b/src/pages/LoginPage.vue new file mode 100644 index 0000000..d2f3f46 --- /dev/null +++ b/src/pages/LoginPage.vue @@ -0,0 +1,95 @@ + + + + diff --git a/src/pages/OnboardingPage.vue b/src/pages/OnboardingPage.vue new file mode 100644 index 0000000..109c413 --- /dev/null +++ b/src/pages/OnboardingPage.vue @@ -0,0 +1,238 @@ + + + + diff --git a/src/pages/PracticePage.vue b/src/pages/PracticePage.vue new file mode 100644 index 0000000..c3597ba --- /dev/null +++ b/src/pages/PracticePage.vue @@ -0,0 +1,232 @@ + + +