From 4d03215fe400ce3806e4481e10313ede2b2ea7fe Mon Sep 17 00:00:00 2001 From: bujinyuan Date: Mon, 2 Mar 2026 11:31:24 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20Phase=201=20=E5=AE=8C=E6=88=90=20?= =?UTF-8?q?=E2=80=94=20=E5=9F=BA=E7=A1=80=E8=AE=BE=E6=96=BD=20+=20?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E8=AE=A4=E8=AF=81=20+=20=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E7=AB=AF=20+=20=E7=AB=AF=E5=88=B0=E7=AB=AF=E9=AA=8C=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 1 (基础设施与用户认证) 全部 11 个 Task 开发完成: 后端 (Go): - Auth 模块: 注册/登录/JWT(有状态)/Profile/密码修改 - Admin 模块: 用户列表/详情/禁用/启用/角色分配/创建用户 - 中间件: JWT认证 + RBAC角色权限 + 请求日志 + CORS + Panic恢复 - Dockerfile 多阶段构建 + Docker Compose 全栈部署 前台 (uni-app): - 登录/注册页面 + 自定义 TabBar + 个人中心 - 请求封装 + 状态管理 (Pinia) 管理端 (Vue 3 + Element Plus): - 登录/仪表盘/用户列表/用户详情 - Axios 封装 + 路由守卫 + Pinia 状态管理 验证: - 全流程 API 端到端测试通过 - Playwright 页面自动化验证通过 - code-reviewer 代码审查通过 Made-with: Cursor --- .gitignore | 5 + AGENTS.md | 6 + README.md | 84 +- admin/index.html | 13 + admin/package-lock.json | 1952 ++++ admin/package.json | 21 + admin/src/App.vue | 10 + admin/src/api/auth.js | 35 + admin/src/api/user.js | 89 + admin/src/main.js | 31 + admin/src/router/index.js | 85 + admin/src/store/user.js | 71 + admin/src/styles/global.css | 43 + admin/src/utils/request.js | 70 + admin/src/utils/storage.js | 54 + admin/src/views/dashboard/index.vue | 98 + admin/src/views/layout/index.vue | 234 + admin/src/views/login/index.vue | 203 + admin/src/views/user/detail.vue | 409 + admin/src/views/user/list.vue | 485 + admin/vite.config.js | 21 + backend/go-service/.dockerignore | 16 + backend/go-service/Dockerfile | 48 + .../controller/user_manage_controller.go | 202 + .../app/admin/dao/user_manage_dao.go | 106 + backend/go-service/app/admin/provider.go | 17 + backend/go-service/app/admin/router.go | 29 + .../app/admin/service/user_manage_service.go | 272 + backend/go-service/app/dto/admin_dto.go | 53 + backend/go-service/app/provider/provider.go | 34 +- backend/go-service/app/provider/wire.go | 2 + backend/go-service/app/provider/wire_gen.go | 8 +- backend/go-service/cmd/server/main.go | 8 +- backend/go-service/config/config.docker.yaml | 42 + backend/go-service/pkg/middleware/recovery.go | 2 + backend/go-service/router/router.go | 3 +- deploy/docker-compose.dev.yml | 16 + design-system/echochat/MASTER.md | 202 + design-system/echochat/pages/admin-login.md | 46 + .../echochat/pages/admin-user-list.md | 52 + design-system/echochat/pages/login.md | 50 + docs/api/admin/user.md | 58 +- docs/architecture/system-architecture.md | 33 +- .../2026-02-27-phase1-foundation-and-auth.md | 61 +- docs/progress/CURRENT_STATUS.md | 140 + frontend/.npmrc | 1 + frontend/package-lock.json | 8365 +++++++++++++++++ frontend/package.json | 2 + frontend/src/api/auth.js | 115 + frontend/src/components/CustomTabBar.vue | 127 + frontend/src/main.js | 31 +- frontend/src/pages.json | 62 +- frontend/src/pages/auth/login.vue | 347 + frontend/src/pages/auth/register.vue | 427 + frontend/src/pages/chat/index.vue | 62 + frontend/src/pages/contact/index.vue | 58 + frontend/src/pages/index/index.vue | 66 +- frontend/src/pages/meeting/index.vue | 58 + frontend/src/pages/profile/index.vue | 213 + frontend/src/services/websocket.js | 27 + frontend/src/store/user.js | 217 + frontend/src/utils/request.js | 173 + frontend/src/utils/storage.js | 140 + 63 files changed, 15908 insertions(+), 102 deletions(-) create mode 100644 admin/index.html create mode 100644 admin/package-lock.json create mode 100644 admin/package.json create mode 100644 admin/src/App.vue create mode 100644 admin/src/api/auth.js create mode 100644 admin/src/api/user.js create mode 100644 admin/src/main.js create mode 100644 admin/src/router/index.js create mode 100644 admin/src/store/user.js create mode 100644 admin/src/styles/global.css create mode 100644 admin/src/utils/request.js create mode 100644 admin/src/utils/storage.js create mode 100644 admin/src/views/dashboard/index.vue create mode 100644 admin/src/views/layout/index.vue create mode 100644 admin/src/views/login/index.vue create mode 100644 admin/src/views/user/detail.vue create mode 100644 admin/src/views/user/list.vue create mode 100644 admin/vite.config.js create mode 100644 backend/go-service/.dockerignore create mode 100644 backend/go-service/Dockerfile create mode 100644 backend/go-service/app/admin/controller/user_manage_controller.go create mode 100644 backend/go-service/app/admin/dao/user_manage_dao.go create mode 100644 backend/go-service/app/admin/provider.go create mode 100644 backend/go-service/app/admin/router.go create mode 100644 backend/go-service/app/admin/service/user_manage_service.go create mode 100644 backend/go-service/app/dto/admin_dto.go create mode 100644 backend/go-service/config/config.docker.yaml create mode 100644 design-system/echochat/MASTER.md create mode 100644 design-system/echochat/pages/admin-login.md create mode 100644 design-system/echochat/pages/admin-user-list.md create mode 100644 design-system/echochat/pages/login.md create mode 100644 docs/progress/CURRENT_STATUS.md create mode 100644 frontend/.npmrc create mode 100644 frontend/package-lock.json create mode 100644 frontend/src/api/auth.js create mode 100644 frontend/src/components/CustomTabBar.vue create mode 100644 frontend/src/pages/auth/login.vue create mode 100644 frontend/src/pages/auth/register.vue create mode 100644 frontend/src/pages/chat/index.vue create mode 100644 frontend/src/pages/contact/index.vue create mode 100644 frontend/src/pages/meeting/index.vue create mode 100644 frontend/src/pages/profile/index.vue create mode 100644 frontend/src/services/websocket.js create mode 100644 frontend/src/store/user.js create mode 100644 frontend/src/utils/request.js create mode 100644 frontend/src/utils/storage.js diff --git a/.gitignore b/.gitignore index 4e72b77..7e7afad 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ Thumbs.db # --- IDE --- .idea/ .vscode/ +.cursor/ *.swp *.swo *~ @@ -53,3 +54,7 @@ build/ # --- 测试覆盖率 --- coverage/ *.coverprofile + +# --- Playwright 截图 --- +*.png +!design-system/**/*.png diff --git a/AGENTS.md b/AGENTS.md index 916c0d3..e037eb4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -64,6 +64,12 @@ Usage notes: global + +my-first-skill +Example skill demonstrating Anthropic SKILL.md format. Load when learning to create skills or testing the OpenSkills loader. +global + + pdf Use this skill whenever the user wants to do anything with PDF files. This includes reading or extracting text/tables from PDFs, combining or merging multiple PDFs into one, splitting PDFs apart, rotating pages, adding watermarks, creating new PDFs, filling PDF forms, encrypting/decrypting PDFs, extracting images, and OCR on scanned PDFs to make them searchable. If the user mentions a .pdf file or asks to produce one, use this skill. diff --git a/README.md b/README.md index 96697b0..389ee1b 100644 --- a/README.md +++ b/README.md @@ -51,17 +51,23 @@ mediasoup Worker (C++ SFU) ``` EchoChat/ -├── frontend/ # 前台用户端 (uniapp) -├── admin/ # 后台管理端 (Vue 3 + Element Plus) +├── frontend/ # 前台用户端 (uni-app + Vue 3.4) +├── admin/ # 后台管理端 (Vue 3.5 + Element Plus) ├── backend/ -│ └── go-service/ # Go 后端服务 -├── media-server/ # mediasoup Node 媒体服务 -├── deploy/ # 部署配置 (Docker / Nginx / K8s) -├── docs/ # 项目文档 -│ ├── plans/ # 设计方案与实施计划 -│ ├── api/ # API 接口文档 -│ └── architecture/ # 架构设计文档 -├── scripts/ # 脚本工具 +│ └── go-service/ # Go 后端服务 (Gin + GORM + Wire) +│ ├── app/ # 业务模块 (auth / admin) +│ ├── cmd/server/ # 服务入口 +│ ├── config/ # 配置文件 +│ ├── pkg/ # 公共包 (db / logs / middleware / utils) +│ └── router/ # 路由聚合 +├── media-server/ # mediasoup Node 媒体服务 (Phase 3) +├── deploy/ # 部署配置 (Docker Compose) +├── design-system/ # UI 设计系统 (ui-ux-pro-max 生成) +├── docs/ # 项目文档 +│ ├── progress/ # 开发进度 +│ ├── plans/ # 实施计划 +│ ├── api/ # API 接口文档 +│ └── architecture/ # 架构设计文档 └── README.md ``` @@ -71,20 +77,34 @@ EchoChat/ ### 环境要求 -- Go 1.22+ +- Go 1.23+ - Node.js 18+ - Docker & Docker Compose -- PostgreSQL 17 (通过 Docker) -- Redis 7 (通过 Docker) +- PostgreSQL 17(通过 Docker 自动启动) +- Redis 7(通过 Docker 自动启动) -### 1. 启动基础设施 +### 方式一:Docker Compose 一键启动(推荐) ```bash cd deploy docker compose -f docker-compose.dev.yml up -d ``` -### 2. 启动 Go 后端 +启动后服务地址: +- Go 后端 API:`http://localhost:8085` +- PostgreSQL:`localhost:5432` +- Redis:`localhost:6379` + +### 方式二:分步手动启动(开发调试) + +**1. 启动基础设施(数据库 + 缓存)** + +```bash +cd deploy +docker compose -f docker-compose.dev.yml up -d postgres redis +``` + +**2. 启动 Go 后端** ```bash cd backend/go-service @@ -92,15 +112,19 @@ go mod tidy go run cmd/server/main.go ``` -### 3. 启动前台前端 +后端启动在 `http://localhost:8085`,健康检查:`GET /health` + +**3. 启动前台用户端(uni-app H5)** ```bash cd frontend -npm install +npm install --legacy-peer-deps npm run dev:h5 ``` -### 4. 启动管理端 +前台 H5 启动在 `http://localhost:5173`(端口可能递增) + +**4. 启动后台管理端(Vue 3)** ```bash cd admin @@ -108,17 +132,34 @@ npm install npm run dev ``` +管理端启动在 `http://localhost:3100`(自动代理 `/api` 到后端 8085) + --- ## MVP 功能规划(第一期) +### Phase 1 — 基础设施与用户认证 ✅ + - [x] 设计方案与架构文档 -- [ ] 用户注册/登录(邮箱+密码、用户名+密码) +- [x] Docker Compose 开发环境(PostgreSQL + Redis) +- [x] Go 后端服务骨架(Gin + GORM + Wire + Zap) +- [x] 用户注册/登录 API(用户名+邮箱+密码) +- [x] JWT Token 认证(有状态 JWT + Redis) +- [x] RBAC 角色权限(user / admin / super_admin) +- [x] 前台 uni-app 骨架 + 登录/注册/TabBar/个人中心 +- [x] 后台 Vue 3 管理端 + 登录/布局/用户管理 +- [x] Go 服务 Dockerfile + Docker Compose 全栈部署 + +### Phase 2 — 即时通讯(待开始) + - [ ] 即时聊天(单聊 + 群聊,文字/图片/文件) - [ ] 联系人/好友管理 -- [ ] 多人音视频会议(即时会议 + 预约会议) - [ ] 消息通知系统 -- [ ] 后台管理端(用户管理、会议监控、系统配置) + +### Phase 3 — 音视频会议(待开始) + +- [ ] 多人音视频会议(即时会议 + 预约会议) +- [ ] 后台管理端会议监控 ## 后续规划 @@ -135,6 +176,7 @@ npm run dev | 文档 | 路径 | 说明 | |------|------|------| +| 项目进度 | [docs/progress/CURRENT_STATUS.md](docs/progress/CURRENT_STATUS.md) | 当前开发进度与技术决策 | | 整体设计方案 | [docs/plans/2026-02-27-echochat-system-design.md](docs/plans/2026-02-27-echochat-system-design.md) | 系统完整设计方案 | | 第一阶段实施计划 | [docs/plans/2026-02-27-phase1-foundation-and-auth.md](docs/plans/2026-02-27-phase1-foundation-and-auth.md) | 基础设施+用户体系实施步骤 | | 系统架构文档 | [docs/architecture/system-architecture.md](docs/architecture/system-architecture.md) | 架构分层与技术选型 | diff --git a/admin/index.html b/admin/index.html new file mode 100644 index 0000000..6e4e0b5 --- /dev/null +++ b/admin/index.html @@ -0,0 +1,13 @@ + + + + + + EchoChat 管理后台 + + + +
+ + + diff --git a/admin/package-lock.json b/admin/package-lock.json new file mode 100644 index 0000000..752e8e4 --- /dev/null +++ b/admin/package-lock.json @@ -0,0 +1,1952 @@ +{ + "name": "echochat-admin", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "echochat-admin", + "version": "0.1.0", + "dependencies": { + "@element-plus/icons-vue": "^2.3.2", + "@vitejs/plugin-vue": "^6.0.4", + "axios": "^1.13.6", + "element-plus": "^2.13.3", + "pinia": "^3.0.4", + "vite": "^7.3.1", + "vue": "^3.5.29", + "vue-router": "^4.6.4" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@ctrl/tinycolor": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz", + "integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/@element-plus/icons-vue": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz", + "integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==", + "license": "MIT", + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.4.tgz", + "integrity": "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.5.tgz", + "integrity": "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.4", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@popperjs/core": { + "name": "@sxzz/popperjs-es", + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@sxzz/popperjs-es/-/popperjs-es-2.11.8.tgz", + "integrity": "sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.2", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.2.tgz", + "integrity": "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.24", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz", + "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==", + "license": "MIT" + }, + "node_modules/@types/lodash-es": { + "version": "4.17.12", + "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", + "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==", + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.4.tgz", + "integrity": "sha512-uM5iXipgYIn13UUQCZNdWkYk+sysBeA97d5mHsAoAt1u/wpN3+zxOmsVJWosuzX+IMGRzeYUNytztrYznboIkQ==", + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-rc.2" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.29.tgz", + "integrity": "sha512-cuzPhD8fwRHk8IGfmYaR4eEe4cAyJEL66Ove/WZL7yWNL134nqLddSLwNRIsFlnnW1kK+p8Ck3viFnC0chXCXw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@vue/shared": "3.5.29", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.29.tgz", + "integrity": "sha512-n0G5o7R3uBVmVxjTIYcz7ovr8sy7QObFG8OQJ3xGCDNhbG60biP/P5KnyY8NLd81OuT1WJflG7N4KWYHaeeaIg==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.29", + "@vue/shared": "3.5.29" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.29.tgz", + "integrity": "sha512-oJZhN5XJs35Gzr50E82jg2cYdZQ78wEwvRO6Y63TvLVTc+6xICzJHP1UIecdSPPYIbkautNBanDiWYa64QSFIA==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@vue/compiler-core": "3.5.29", + "@vue/compiler-dom": "3.5.29", + "@vue/compiler-ssr": "3.5.29", + "@vue/shared": "3.5.29", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.29.tgz", + "integrity": "sha512-Y/ARJZE6fpjzL5GH/phJmsFwx3g6t2KmHKHx5q+MLl2kencADKIrhH5MLF6HHpRMmlRAYBRSvv347Mepf1zVNw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.29", + "@vue/shared": "3.5.29" + } + }, + "node_modules/@vue/devtools-api": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.9.tgz", + "integrity": "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==", + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^7.7.9" + } + }, + "node_modules/@vue/devtools-kit": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz", + "integrity": "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==", + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^7.7.9", + "birpc": "^2.3.0", + "hookable": "^5.5.3", + "mitt": "^3.0.1", + "perfect-debounce": "^1.0.0", + "speakingurl": "^14.0.1", + "superjson": "^2.2.2" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz", + "integrity": "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==", + "license": "MIT", + "dependencies": { + "rfdc": "^1.4.1" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.29.tgz", + "integrity": "sha512-zcrANcrRdcLtmGZETBxWqIkoQei8HaFpZWx/GHKxx79JZsiZ8j1du0VUJtu4eJjgFvU/iKL5lRXFXksVmI+5DA==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.29" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.29.tgz", + "integrity": "sha512-8DpW2QfdwIWOLqtsNcds4s+QgwSaHSJY/SUe04LptianUQ/0xi6KVsu/pYVh+HO3NTVvVJjIPL2t6GdeKbS4Lg==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.29", + "@vue/shared": "3.5.29" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.29.tgz", + "integrity": "sha512-AHvvJEtcY9tw/uk+s/YRLSlxxQnqnAkjqvK25ZiM4CllCZWzElRAoQnCM42m9AHRLNJ6oe2kC5DCgD4AUdlvXg==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.29", + "@vue/runtime-core": "3.5.29", + "@vue/shared": "3.5.29", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.29.tgz", + "integrity": "sha512-G/1k6WK5MusLlbxSE2YTcqAAezS+VuwHhOvLx2KnQU7G2zCH6KIb+5Wyt6UjMq7a3qPzNEjJXs1hvAxDclQH+g==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.29", + "@vue/shared": "3.5.29" + }, + "peerDependencies": { + "vue": "3.5.29" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.29.tgz", + "integrity": "sha512-w7SR0A5zyRByL9XUkCfdLs7t9XOHUyJ67qPGQjOou3p6GvBeBW+AVjUUmlxtZ4PIYaRvE+1LmK44O4uajlZwcg==", + "license": "MIT" + }, + "node_modules/@vueuse/core": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.1.tgz", + "integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.20", + "@vueuse/metadata": "10.11.1", + "@vueuse/shared": "10.11.1", + "vue-demi": ">=0.14.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/core/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/@vueuse/metadata": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.1.tgz", + "integrity": "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.11.1.tgz", + "integrity": "sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==", + "license": "MIT", + "dependencies": { + "vue-demi": ">=0.14.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/async-validator": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz", + "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", + "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/birpc": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", + "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/copy-anything": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz", + "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==", + "license": "MIT", + "dependencies": { + "is-what": "^5.2.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/dayjs": { + "version": "1.11.19", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/element-plus": { + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.13.3.tgz", + "integrity": "sha512-RwLVtFpeHjZ4UCtHxVi1/sGR2cr2xOL7hqWa7qJc/+gdO6QavVG8Nw1C647obCb3tIg2ztMhNbIIjZUv+6z1og==", + "license": "MIT", + "dependencies": { + "@ctrl/tinycolor": "^3.4.1", + "@element-plus/icons-vue": "^2.3.2", + "@floating-ui/dom": "^1.0.1", + "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7", + "@types/lodash": "^4.17.20", + "@types/lodash-es": "^4.17.12", + "@vueuse/core": "^10.11.0", + "async-validator": "^4.2.5", + "dayjs": "^1.11.19", + "lodash": "^4.17.23", + "lodash-es": "^4.17.23", + "lodash-unified": "^1.0.3", + "memoize-one": "^6.0.0", + "normalize-wheel-es": "^1.2.0" + }, + "peerDependencies": { + "vue": "^3.3.0" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "license": "MIT" + }, + "node_modules/is-what": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz", + "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "license": "MIT", + "peer": true + }, + "node_modules/lodash-es": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz", + "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", + "license": "MIT", + "peer": true + }, + "node_modules/lodash-unified": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/lodash-unified/-/lodash-unified-1.0.3.tgz", + "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==", + "license": "MIT", + "peerDependencies": { + "@types/lodash-es": "*", + "lodash": "*", + "lodash-es": "*" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/normalize-wheel-es": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz", + "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==", + "license": "BSD-3-Clause" + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pinia": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz", + "integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^7.7.7" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.5.0", + "vue": "^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/speakingurl": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", + "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/superjson": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz", + "integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==", + "license": "MIT", + "dependencies": { + "copy-anything": "^4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.29.tgz", + "integrity": "sha512-BZqN4Ze6mDQVNAni0IHeMJ5mwr8VAJ3MQC9FmprRhcBYENw+wOAAjRj8jfmN6FLl0j96OXbR+CjWhmAmM+QGnA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/compiler-dom": "3.5.29", + "@vue/compiler-sfc": "3.5.29", + "@vue/runtime-dom": "3.5.29", + "@vue/server-renderer": "3.5.29", + "@vue/shared": "3.5.29" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-router": { + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz", + "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/vue-router/node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + } + } +} diff --git a/admin/package.json b/admin/package.json new file mode 100644 index 0000000..a5a5332 --- /dev/null +++ b/admin/package.json @@ -0,0 +1,21 @@ +{ + "name": "echochat-admin", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@element-plus/icons-vue": "^2.3.2", + "@vitejs/plugin-vue": "^6.0.4", + "axios": "^1.13.6", + "element-plus": "^2.13.3", + "pinia": "^3.0.4", + "vite": "^7.3.1", + "vue": "^3.5.29", + "vue-router": "^4.6.4" + } +} diff --git a/admin/src/App.vue b/admin/src/App.vue new file mode 100644 index 0000000..bfae83b --- /dev/null +++ b/admin/src/App.vue @@ -0,0 +1,10 @@ + + + + diff --git a/admin/src/api/auth.js b/admin/src/api/auth.js new file mode 100644 index 0000000..472db81 --- /dev/null +++ b/admin/src/api/auth.js @@ -0,0 +1,35 @@ +/** + * 管理后台认证 API + * + * 对应后端路由:/api/v1/auth/* + * 管理员登录使用与普通用户相同的登录接口, + * 权限区分由 JWT 中的 role 字段和 RequireRole 中间件控制 + */ +import request from '@/utils/request' + +/** + * 管理员登录 + * POST /api/v1/auth/login + * + * @param {Object} data - { account, password } + * @returns {Promise} { token, refresh_token, expires_in, user } + */ +export const login = (data) => { + return request.post('/api/v1/auth/login', data) +} + +/** + * 管理员登出 + * POST /api/v1/auth/logout + */ +export const logout = () => { + return request.post('/api/v1/auth/logout') +} + +/** + * 获取管理员个人信息 + * GET /api/v1/auth/profile + */ +export const getProfile = () => { + return request.get('/api/v1/auth/profile') +} diff --git a/admin/src/api/user.js b/admin/src/api/user.js new file mode 100644 index 0000000..5567fcd --- /dev/null +++ b/admin/src/api/user.js @@ -0,0 +1,89 @@ +/** + * 用户管理 API 模块 + * + * 对应后端路由:/api/v1/admin/users + * 所有接口需要 JWT + admin 角色权限 + * + * 接口列表: + * - GET /api/v1/admin/users 用户列表(分页 + 搜索 + 筛选) + * - GET /api/v1/admin/users/:id 用户详情 + * - PUT /api/v1/admin/users/:id/status 更新用户状态 + * - PUT /api/v1/admin/users/:id/role 分配角色 + * - POST /api/v1/admin/users 管理员创建用户 + */ +import request from '@/utils/request' + +/** + * 获取用户列表 + * @param {Object} params - 查询参数 + * @param {number} params.page - 页码(从 1 开始) + * @param {number} params.page_size - 每页数量 + * @param {string} [params.keyword] - 搜索关键词(用户名/邮箱) + * @param {number} [params.status] - 状态筛选:1=正常, 2=禁用 + * @returns {Promise<{data: {total: number, list: Array}}>} + */ +export function getUserList(params) { + return request({ + url: '/api/v1/admin/users', + method: 'get', + params + }) +} + +/** + * 获取用户详情 + * @param {number} id - 用户 ID + * @returns {Promise<{data: Object}>} + */ +export function getUserDetail(id) { + return request({ + url: `/api/v1/admin/users/${id}`, + method: 'get' + }) +} + +/** + * 更新用户状态 + * @param {number} id - 用户 ID + * @param {number} status - 目标状态:1=正常, 2=禁用 + * @returns {Promise} + */ +export function updateUserStatus(id, status) { + return request({ + url: `/api/v1/admin/users/${id}/status`, + method: 'put', + data: { status } + }) +} + +/** + * 分配角色 + * @param {number} id - 用户 ID + * @param {string} roleCode - 角色代码:user/admin/super_admin + * @returns {Promise} + */ +export function assignUserRole(id, roleCode) { + return request({ + url: `/api/v1/admin/users/${id}/role`, + method: 'put', + data: { role_code: roleCode } + }) +} + +/** + * 管理员创建用户 + * @param {Object} data - 用户信息 + * @param {string} data.username - 用户名 + * @param {string} data.email - 邮箱 + * @param {string} data.password - 初始密码 + * @param {string} [data.nickname] - 昵称 + * @param {string} [data.role_code] - 初始角色 + * @returns {Promise<{data: Object}>} + */ +export function createUser(data) { + return request({ + url: '/api/v1/admin/users', + method: 'post', + data + }) +} diff --git a/admin/src/main.js b/admin/src/main.js new file mode 100644 index 0000000..4f46823 --- /dev/null +++ b/admin/src/main.js @@ -0,0 +1,31 @@ +/** + * 管理后台入口文件 + * + * 初始化 Vue 3 应用,注册全局插件: + * - Element Plus(UI 组件库,含中文语言包) + * - Vue Router(路由管理) + * - Pinia(状态管理) + * - 全局样式 + */ +import { createApp } from 'vue' +import { createPinia } from 'pinia' +import ElementPlus from 'element-plus' +import zhCn from 'element-plus/dist/locale/zh-cn.mjs' +import * as ElementPlusIconsVue from '@element-plus/icons-vue' +import 'element-plus/dist/index.css' + +import App from './App.vue' +import router from './router' +import './styles/global.css' + +const app = createApp(App) + +app.use(createPinia()) +app.use(router) +app.use(ElementPlus, { locale: zhCn }) + +for (const [key, component] of Object.entries(ElementPlusIconsVue)) { + app.component(key, component) +} + +app.mount('#app') diff --git a/admin/src/router/index.js b/admin/src/router/index.js new file mode 100644 index 0000000..e940fc0 --- /dev/null +++ b/admin/src/router/index.js @@ -0,0 +1,85 @@ +/** + * 管理后台路由配置 + * + * 路由结构: + * - /login → 管理员登录页(无需认证) + * - / → 后台主布局(需要认证 + admin 角色) + * - /dashboard → 仪表盘 + * - /user/list → 用户管理列表 + * - /user/detail/:id → 用户详情 + * + * 路由守卫: + * - 未登录 → 重定向到 /login + * - 已登录访问 /login → 重定向到 /dashboard + */ +import { createRouter, createWebHashHistory } from 'vue-router' + +const routes = [ + { + path: '/login', + name: 'Login', + component: () => import('@/views/login/index.vue'), + meta: { title: '管理员登录', requiresAuth: false } + }, + { + path: '/', + component: () => import('@/views/layout/index.vue'), + redirect: '/dashboard', + meta: { requiresAuth: true }, + children: [ + { + path: 'dashboard', + name: 'Dashboard', + component: () => import('@/views/dashboard/index.vue'), + meta: { title: '仪表盘' } + }, + { + path: 'user/list', + name: 'UserList', + component: () => import('@/views/user/list.vue'), + meta: { title: '用户列表' } + }, + { + path: 'user/detail/:id', + name: 'UserDetail', + component: () => import('@/views/user/detail.vue'), + meta: { title: '用户详情' } + } + ] + } +] + +const router = createRouter({ + history: createWebHashHistory(), + routes +}) + +/** + * 全局前置守卫 + * 检查登录状态,控制页面访问权限 + */ +router.beforeEach((to, from, next) => { + const token = localStorage.getItem('admin_token') + + document.title = to.meta.title + ? `${to.meta.title} - EchoChat 管理后台` + : 'EchoChat 管理后台' + + if (to.meta.requiresAuth === false) { + if (token && to.path === '/login') { + next('/dashboard') + } else { + next() + } + return + } + + if (!token) { + next({ path: '/login', query: { redirect: to.fullPath } }) + return + } + + next() +}) + +export default router diff --git a/admin/src/store/user.js b/admin/src/store/user.js new file mode 100644 index 0000000..b8de4fc --- /dev/null +++ b/admin/src/store/user.js @@ -0,0 +1,71 @@ +/** + * 管理后台用户 Store + * + * 管理管理员认证状态、个人信息 + * 使用 Pinia 3.x + Composition API 风格 + */ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import { login as loginApi, logout as logoutApi, getProfile as getProfileApi } from '@/api/auth' +import { saveToken, getToken, removeToken, saveUserInfo, getUserInfo, removeUserInfo, clearAll } from '@/utils/storage' + +export const useUserStore = defineStore('admin-user', () => { + /** Access Token */ + const token = ref(getToken() || '') + + /** 管理员信息 */ + const userInfo = ref(getUserInfo() || null) + + /** 是否已登录 */ + const isLoggedIn = computed(() => !!token.value) + + /** 管理员用户名 */ + const username = computed(() => userInfo.value?.username || '') + + /** 角色列表 */ + const roles = computed(() => userInfo.value?.roles || []) + + /** + * 管理员登录 + * @param {Object} loginData - { account, password } + */ + const login = async (loginData) => { + const res = await loginApi(loginData) + token.value = res.data.token + userInfo.value = res.data.user + saveToken(res.data.token, res.data.refresh_token) + saveUserInfo(res.data.user) + return res.data + } + + /** 退出登录 */ + const logout = async () => { + try { + await logoutApi() + } catch { + // 即使后端请求失败也清除本地状态 + } + token.value = '' + userInfo.value = null + clearAll() + } + + /** 获取/刷新管理员信息 */ + const getProfile = async () => { + const res = await getProfileApi() + userInfo.value = res.data + saveUserInfo(res.data) + return res.data + } + + return { + token, + userInfo, + isLoggedIn, + username, + roles, + login, + logout, + getProfile + } +}) diff --git a/admin/src/styles/global.css b/admin/src/styles/global.css new file mode 100644 index 0000000..792c2c5 --- /dev/null +++ b/admin/src/styles/global.css @@ -0,0 +1,43 @@ +/** + * 管理后台全局样式 + * + * 设计系统来源:design-system/echochat/MASTER.md(管理端适配) + * 色板:Primary #2563EB / Background #F8FAFC / Text #1E293B + * + * 注意:管理端使用 Element Plus 组件库, + * 通过 CSS 变量覆盖 Element Plus 默认主题色以匹配设计系统 + */ + +:root { + /* 与前台统一品牌色,管理端基于 Element Plus 主题 */ + --el-color-primary: #2563EB; + --el-color-primary-light-3: #60A5FA; + --el-color-primary-light-5: #93C5FD; + --el-color-primary-light-7: #BFDBFE; + --el-color-primary-light-8: #DBEAFE; + --el-color-primary-light-9: #EFF6FF; + --el-color-primary-dark-2: #1D4ED8; + --el-color-danger: #EF4444; + --el-color-success: #22C55E; + --el-color-warning: #F59E0B; + --el-color-info: #64748B; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, + 'Helvetica Neue', Arial, sans-serif; + color: #1E293B; + background-color: #F0F2F5; + -webkit-font-smoothing: antialiased; +} + +a { + text-decoration: none; + color: inherit; +} diff --git a/admin/src/utils/request.js b/admin/src/utils/request.js new file mode 100644 index 0000000..9120b2b --- /dev/null +++ b/admin/src/utils/request.js @@ -0,0 +1,70 @@ +/** + * HTTP 请求封装(管理后台) + * + * 基于 Axios 封装,提供: + * - 基础 URL 配置(开发环境通过 Vite proxy 代理) + * - 请求拦截:自动附加 Authorization Header + * - 响应拦截:统一处理错误码(401 跳转登录等) + * + * 对应后端 API 规范见 docs/api/README.md + */ +import axios from 'axios' +import { ElMessage } from 'element-plus' +import router from '@/router' + +/** Axios 实例 */ +const service = axios.create({ + baseURL: import.meta.env.VITE_API_BASE_URL || '', + timeout: 15000, + headers: { 'Content-Type': 'application/json' } +}) + +/** + * 请求拦截器 + * 自动从 localStorage 读取 Token 并附加到 Authorization Header + */ +service.interceptors.request.use( + (config) => { + const token = localStorage.getItem('admin_token') + if (token) { + config.headers.Authorization = `Bearer ${token}` + } + return config + }, + (error) => Promise.reject(error) +) + +/** + * 响应拦截器 + * 统一处理后端 { code, message, data } 响应格式 + */ +service.interceptors.response.use( + (response) => { + const { data } = response + if (data.code === 0) { + return data + } + ElMessage.error(data.message || '请求失败') + return Promise.reject(data) + }, + (error) => { + if (error.response) { + const { status, data } = error.response + if (status === 401) { + ElMessage.error('登录已过期,请重新登录') + localStorage.removeItem('admin_token') + localStorage.removeItem('admin_user') + router.push('/login') + } else if (status === 403) { + ElMessage.error(data?.message || '没有访问权限') + } else { + ElMessage.error(data?.message || `请求错误 (${status})`) + } + } else { + ElMessage.error('网络异常,请检查网络连接') + } + return Promise.reject(error) + } +) + +export default service diff --git a/admin/src/utils/storage.js b/admin/src/utils/storage.js new file mode 100644 index 0000000..229bccc --- /dev/null +++ b/admin/src/utils/storage.js @@ -0,0 +1,54 @@ +/** + * 本地存储工具(管理后台) + * + * 管理端使用 localStorage,Key 前缀 admin_ 与前台隔离 + */ + +const KEYS = { + TOKEN: 'admin_token', + REFRESH_TOKEN: 'admin_refresh_token', + USER_INFO: 'admin_user' +} + +/** 保存 Token */ +export const saveToken = (token, refreshToken) => { + localStorage.setItem(KEYS.TOKEN, token) + if (refreshToken) localStorage.setItem(KEYS.REFRESH_TOKEN, refreshToken) +} + +/** 获取 Access Token */ +export const getToken = () => localStorage.getItem(KEYS.TOKEN) + +/** 获取 Refresh Token */ +export const getRefreshToken = () => localStorage.getItem(KEYS.REFRESH_TOKEN) + +/** 清除 Token */ +export const removeToken = () => { + localStorage.removeItem(KEYS.TOKEN) + localStorage.removeItem(KEYS.REFRESH_TOKEN) +} + +/** 保存用户信息 */ +export const saveUserInfo = (user) => { + localStorage.setItem(KEYS.USER_INFO, JSON.stringify(user)) +} + +/** 获取用户信息 */ +export const getUserInfo = () => { + try { + return JSON.parse(localStorage.getItem(KEYS.USER_INFO)) + } catch { + return null + } +} + +/** 清除用户信息 */ +export const removeUserInfo = () => { + localStorage.removeItem(KEYS.USER_INFO) +} + +/** 清除所有管理端缓存 */ +export const clearAll = () => { + removeToken() + removeUserInfo() +} diff --git a/admin/src/views/dashboard/index.vue b/admin/src/views/dashboard/index.vue new file mode 100644 index 0000000..3086ad3 --- /dev/null +++ b/admin/src/views/dashboard/index.vue @@ -0,0 +1,98 @@ + + + + + + diff --git a/admin/src/views/layout/index.vue b/admin/src/views/layout/index.vue new file mode 100644 index 0000000..f58c335 --- /dev/null +++ b/admin/src/views/layout/index.vue @@ -0,0 +1,234 @@ + + + + + + diff --git a/admin/src/views/login/index.vue b/admin/src/views/login/index.vue new file mode 100644 index 0000000..0a75b84 --- /dev/null +++ b/admin/src/views/login/index.vue @@ -0,0 +1,203 @@ + + + + + + diff --git a/admin/src/views/user/detail.vue b/admin/src/views/user/detail.vue new file mode 100644 index 0000000..1836e19 --- /dev/null +++ b/admin/src/views/user/detail.vue @@ -0,0 +1,409 @@ + + + + + + diff --git a/admin/src/views/user/list.vue b/admin/src/views/user/list.vue new file mode 100644 index 0000000..88060cf --- /dev/null +++ b/admin/src/views/user/list.vue @@ -0,0 +1,485 @@ + + + + + + diff --git a/admin/vite.config.js b/admin/vite.config.js new file mode 100644 index 0000000..521922e --- /dev/null +++ b/admin/vite.config.js @@ -0,0 +1,21 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import { resolve } from 'path' + +export default defineConfig({ + plugins: [vue()], + resolve: { + alias: { + '@': resolve(__dirname, 'src') + } + }, + server: { + port: 3100, + proxy: { + '/api': { + target: 'http://localhost:8085', + changeOrigin: true + } + } + } +}) diff --git a/backend/go-service/.dockerignore b/backend/go-service/.dockerignore new file mode 100644 index 0000000..d917736 --- /dev/null +++ b/backend/go-service/.dockerignore @@ -0,0 +1,16 @@ +# 日志和临时文件 +logs/ +*.log + +# IDE 配置 +.idea/ +.vscode/ +*.swp +*.swo + +# 测试文件 +*_test.go + +# Git +.git +.gitignore diff --git a/backend/go-service/Dockerfile b/backend/go-service/Dockerfile new file mode 100644 index 0000000..55db3b6 --- /dev/null +++ b/backend/go-service/Dockerfile @@ -0,0 +1,48 @@ +# ============================================================ +# EchoChat Go 后端服务 Dockerfile +# 多阶段构建:编译阶段 → 运行阶段(alpine 最小镜像) +# ============================================================ + +# --- 第一阶段:编译 --- +FROM golang:1.23-alpine AS builder + +# 安装编译依赖(CGO 需要 gcc) +RUN apk add --no-cache gcc musl-dev + +WORKDIR /build + +# 先复制依赖文件利用 Docker 缓存层 +COPY go.mod go.sum ./ +RUN go mod download + +# 复制全部源码并编译 +COPY . . +RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /build/echochat ./cmd/server + +# --- 第二阶段:运行 --- +FROM alpine:3.19 + +# 安装运行时依赖(时区数据 + CA 证书) +RUN apk add --no-cache ca-certificates tzdata + +# 设置时区为上海 +ENV TZ=Asia/Shanghai + +WORKDIR /app + +# 从编译阶段复制二进制文件 +COPY --from=builder /build/echochat . + +# 复制配置文件目录 +COPY --from=builder /build/config ./config + +# 创建日志目录 +RUN mkdir -p /app/logs + +EXPOSE 8085 + +# 健康检查 +HEALTHCHECK --interval=10s --timeout=3s --start-period=5s --retries=3 \ + CMD wget -q -O /dev/null http://localhost:8085/health || exit 1 + +CMD ["./echochat"] diff --git a/backend/go-service/app/admin/controller/user_manage_controller.go b/backend/go-service/app/admin/controller/user_manage_controller.go new file mode 100644 index 0000000..57de5b3 --- /dev/null +++ b/backend/go-service/app/admin/controller/user_manage_controller.go @@ -0,0 +1,202 @@ +// Package controller 提供 admin 模块的 HTTP 请求处理 +package controller + +import ( + "strconv" + + "github.com/echochat/backend/app/admin/service" + "github.com/echochat/backend/app/dto" + "github.com/echochat/backend/pkg/logs" + "github.com/echochat/backend/pkg/middleware" + "github.com/echochat/backend/pkg/utils" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +// UserManageController 管理端用户管理控制器 +type UserManageController struct { + userManageService *service.UserManageService +} + +// NewUserManageController 创建用户管理控制器实例 +func NewUserManageController(svc *service.UserManageService) *UserManageController { + return &UserManageController{userManageService: svc} +} + +// GetUserList 获取用户列表 +// GET /api/v1/admin/users?page=1&page_size=10&keyword=xxx&status=1 +func (ctl *UserManageController) GetUserList(c *gin.Context) { + funcName := "controller.user_manage_controller.GetUserList" + ctx := c.Request.Context() + + var req dto.UserListRequest + if err := c.ShouldBindQuery(&req); err != nil { + logs.Warn(ctx, funcName, "参数校验失败", zap.Error(err)) + utils.ResponseBadRequest(c, "参数错误: "+err.Error()) + return + } + + logs.Info(ctx, funcName, "获取用户列表", + zap.Int("page", req.Page), + zap.Int("page_size", req.PageSize), + zap.String("keyword", req.Keyword), + ) + + resp, err := ctl.userManageService.GetUserList(ctx, &req) + if err != nil { + logs.Error(ctx, funcName, "获取用户列表失败", zap.Error(err)) + utils.ResponseError(c, "获取用户列表失败") + return + } + + utils.ResponseOK(c, resp) +} + +// GetUserDetail 获取用户详情 +// GET /api/v1/admin/users/:id +func (ctl *UserManageController) GetUserDetail(c *gin.Context) { + funcName := "controller.user_manage_controller.GetUserDetail" + ctx := c.Request.Context() + + userID, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + utils.ResponseBadRequest(c, "无效的用户 ID") + return + } + + logs.Info(ctx, funcName, "获取用户详情", zap.Int64("user_id", userID)) + + detail, err := ctl.userManageService.GetUserDetail(ctx, userID) + if err != nil { + if err == service.ErrUserNotFound { + utils.ResponseNotFound(c, "用户不存在") + return + } + logs.Error(ctx, funcName, "获取用户详情失败", zap.Error(err)) + utils.ResponseError(c, "获取用户详情失败") + return + } + + utils.ResponseOK(c, detail) +} + +// UpdateUserStatus 更新用户状态(启用/禁用) +// PUT /api/v1/admin/users/:id/status +func (ctl *UserManageController) UpdateUserStatus(c *gin.Context) { + funcName := "controller.user_manage_controller.UpdateUserStatus" + ctx := c.Request.Context() + + userID, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + utils.ResponseBadRequest(c, "无效的用户 ID") + return + } + + var req dto.UpdateUserStatusRequest + if err := c.ShouldBindJSON(&req); err != nil { + logs.Warn(ctx, funcName, "参数校验失败", zap.Error(err)) + utils.ResponseBadRequest(c, "参数错误: "+err.Error()) + return + } + + adminUserID, ok := middleware.GetCurrentUserID(c) + if !ok { + utils.ResponseUnauthorized(c, "无法获取当前用户信息") + return + } + + logs.Info(ctx, funcName, "更新用户状态", + zap.Int64("target_user_id", userID), + zap.Int("status", req.Status), + zap.Int64("admin_user_id", adminUserID), + ) + + if err := ctl.userManageService.UpdateUserStatus(ctx, userID, req.Status, adminUserID); err != nil { + switch err { + case service.ErrUserNotFound: + utils.ResponseNotFound(c, "用户不存在") + case service.ErrCannotDisableSelf: + utils.ResponseBadRequest(c, "不能禁用自己的账号") + case service.ErrInvalidStatus: + utils.ResponseBadRequest(c, "无效的用户状态") + default: + logs.Error(ctx, funcName, "更新用户状态失败", zap.Error(err)) + utils.ResponseError(c, "更新用户状态失败") + } + return + } + + utils.ResponseOK(c, nil) +} + +// AssignRole 分配角色 +// PUT /api/v1/admin/users/:id/role +func (ctl *UserManageController) AssignRole(c *gin.Context) { + funcName := "controller.user_manage_controller.AssignRole" + ctx := c.Request.Context() + + userID, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + utils.ResponseBadRequest(c, "无效的用户 ID") + return + } + + var req dto.AssignRoleRequest + if err := c.ShouldBindJSON(&req); err != nil { + logs.Warn(ctx, funcName, "参数校验失败", zap.Error(err)) + utils.ResponseBadRequest(c, "参数错误: "+err.Error()) + return + } + + logs.Info(ctx, funcName, "分配角色", + zap.Int64("user_id", userID), + zap.String("role_code", req.RoleCode), + ) + + if err := ctl.userManageService.AssignUserRole(ctx, userID, req.RoleCode); err != nil { + switch err { + case service.ErrUserNotFound: + utils.ResponseNotFound(c, "用户不存在") + case service.ErrInvalidRole: + utils.ResponseBadRequest(c, "无效的角色代码") + default: + logs.Error(ctx, funcName, "分配角色失败", zap.Error(err)) + utils.ResponseError(c, "分配角色失败") + } + return + } + + utils.ResponseOK(c, nil) +} + +// CreateUser 管理员手动创建用户 +// POST /api/v1/admin/users +func (ctl *UserManageController) CreateUser(c *gin.Context) { + funcName := "controller.user_manage_controller.CreateUser" + ctx := c.Request.Context() + + var req dto.AdminCreateUserRequest + if err := c.ShouldBindJSON(&req); err != nil { + logs.Warn(ctx, funcName, "参数校验失败", zap.Error(err)) + utils.ResponseBadRequest(c, "参数错误: "+err.Error()) + return + } + + logs.Info(ctx, funcName, "管理员创建用户", + zap.String("username", req.Username), + zap.String("email", logs.MaskEmail(req.Email)), + ) + + userInfo, err := ctl.userManageService.CreateUser(ctx, &req) + if err != nil { + if err == service.ErrUserExists { + utils.ResponseBadRequest(c, "用户名或邮箱已被注册") + return + } + logs.Error(ctx, funcName, "创建用户失败", zap.Error(err)) + utils.ResponseError(c, "创建用户失败") + return + } + + utils.ResponseOK(c, userInfo) +} diff --git a/backend/go-service/app/admin/dao/user_manage_dao.go b/backend/go-service/app/admin/dao/user_manage_dao.go new file mode 100644 index 0000000..9a492be --- /dev/null +++ b/backend/go-service/app/admin/dao/user_manage_dao.go @@ -0,0 +1,106 @@ +// Package dao 提供 admin 模块的数据库访问操作 +package dao + +import ( + "context" + "fmt" + + "github.com/echochat/backend/app/auth/model" + "github.com/echochat/backend/app/dto" + "github.com/echochat/backend/pkg/logs" + "go.uber.org/zap" + "gorm.io/gorm" +) + +// UserManageDAO 管理端用户数据访问对象 +// 提供面向管理后台的用户查询操作(分页、搜索、筛选) +type UserManageDAO struct { + db *gorm.DB +} + +// NewUserManageDAO 创建 UserManageDAO 实例 +func NewUserManageDAO(db *gorm.DB) *UserManageDAO { + return &UserManageDAO{db: db} +} + +// ListUsers 分页查询用户列表 +// 支持按用户名/邮箱关键词搜索 + 按状态筛选 +// SQL 执行两次查询:COUNT 总数 + LIMIT/OFFSET 分页数据 +func (d *UserManageDAO) ListUsers(ctx context.Context, req *dto.UserListRequest) ([]model.User, int64, error) { + funcName := "dao.user_manage_dao.ListUsers" + logs.Info(ctx, funcName, "查询用户列表", + zap.Int("page", req.Page), + zap.Int("page_size", req.PageSize), + zap.String("keyword", req.Keyword), + ) + + var users []model.User + var total int64 + + query := d.db.WithContext(ctx).Model(&model.User{}) + + // 关键词搜索:模糊匹配用户名或邮箱 + if req.Keyword != "" { + like := fmt.Sprintf("%%%s%%", req.Keyword) + query = query.Where("username LIKE ? OR email LIKE ?", like, like) + } + + // 状态筛选 + if req.Status != nil { + query = query.Where("status = ?", *req.Status) + } + + // 先查询总数 + if err := query.Count(&total).Error; err != nil { + logs.Error(ctx, funcName, "查询用户总数失败", zap.Error(err)) + return nil, 0, err + } + + // 分页查询数据,按创建时间倒序 + offset := (req.Page - 1) * req.PageSize + if err := query.Order("created_at DESC").Offset(offset).Limit(req.PageSize).Find(&users).Error; err != nil { + logs.Error(ctx, funcName, "查询用户列表失败", zap.Error(err)) + return nil, 0, err + } + + logs.Info(ctx, funcName, "查询用户列表成功", + zap.Int64("total", total), + zap.Int("count", len(users)), + ) + return users, total, nil +} + +// CountUsers 统计用户总数(不含筛选条件) +func (d *UserManageDAO) CountUsers(ctx context.Context) (int64, error) { + funcName := "dao.user_manage_dao.CountUsers" + + var count int64 + err := d.db.WithContext(ctx).Model(&model.User{}).Count(&count).Error + if err != nil { + logs.Error(ctx, funcName, "统计用户总数失败", zap.Error(err)) + } + return count, err +} + +// UpdateUserStatus 更新用户账号状态 +// 仅更新 status 字段,不触发其他字段的更新 +func (d *UserManageDAO) UpdateUserStatus(ctx context.Context, userID int64, status int) error { + funcName := "dao.user_manage_dao.UpdateUserStatus" + logs.Info(ctx, funcName, "更新用户状态", + zap.Int64("user_id", userID), + zap.Int("status", status), + ) + + err := d.db.WithContext(ctx). + Model(&model.User{}). + Where("id = ?", userID). + Update("status", status).Error + + if err != nil { + logs.Error(ctx, funcName, "更新用户状态失败", + zap.Int64("user_id", userID), + zap.Error(err), + ) + } + return err +} diff --git a/backend/go-service/app/admin/provider.go b/backend/go-service/app/admin/provider.go new file mode 100644 index 0000000..fbca434 --- /dev/null +++ b/backend/go-service/app/admin/provider.go @@ -0,0 +1,17 @@ +// Package admin 提供管理后台功能模块 +package admin + +import ( + "github.com/echochat/backend/app/admin/controller" + "github.com/echochat/backend/app/admin/dao" + "github.com/echochat/backend/app/admin/service" + "github.com/google/wire" +) + +// AdminSet Admin 模块依赖注入 Provider Set +// 提供管理端的 DAO、Service 和 Controller 组件 +var AdminSet = wire.NewSet( + dao.NewUserManageDAO, + service.NewUserManageService, + controller.NewUserManageController, +) diff --git a/backend/go-service/app/admin/router.go b/backend/go-service/app/admin/router.go new file mode 100644 index 0000000..8922148 --- /dev/null +++ b/backend/go-service/app/admin/router.go @@ -0,0 +1,29 @@ +// Package admin 提供管理后台功能模块 +package admin + +import ( + "github.com/echochat/backend/app/admin/controller" + "github.com/echochat/backend/app/constants" + "github.com/echochat/backend/pkg/middleware" + "github.com/gin-gonic/gin" +) + +// RegisterRoutes 注册 admin 模块的所有路由 +// 所有管理端 API 需要 JWT + admin 角色双重中间件 +func RegisterRoutes( + r *gin.Engine, + ctrl *controller.UserManageController, + jwtAuth gin.HandlerFunc, +) { + // 管理端路由组:JWT 认证 + admin/super_admin 角色检查 + adminGroup := r.Group("/api/v1/admin") + adminGroup.Use(jwtAuth, middleware.RequireRole(constants.RoleAdmin, constants.RoleSuperAdmin)) + { + // 用户管理 + adminGroup.GET("/users", ctrl.GetUserList) + adminGroup.GET("/users/:id", ctrl.GetUserDetail) + adminGroup.PUT("/users/:id/status", ctrl.UpdateUserStatus) + adminGroup.PUT("/users/:id/role", ctrl.AssignRole) + adminGroup.POST("/users", ctrl.CreateUser) + } +} diff --git a/backend/go-service/app/admin/service/user_manage_service.go b/backend/go-service/app/admin/service/user_manage_service.go new file mode 100644 index 0000000..f62f9cf --- /dev/null +++ b/backend/go-service/app/admin/service/user_manage_service.go @@ -0,0 +1,272 @@ +// Package service 提供 admin 模块的核心业务逻辑 +package service + +import ( + "context" + "errors" + + authDAO "github.com/echochat/backend/app/auth/dao" + "github.com/echochat/backend/app/auth/model" + adminDAO "github.com/echochat/backend/app/admin/dao" + "github.com/echochat/backend/app/constants" + "github.com/echochat/backend/app/dto" + "github.com/echochat/backend/pkg/logs" + "github.com/echochat/backend/pkg/utils" + "go.uber.org/zap" + "gorm.io/gorm" +) + +var ( + ErrUserNotFound = errors.New("用户不存在") + ErrUserExists = errors.New("用户名或邮箱已被注册") + ErrInvalidStatus = errors.New("无效的用户状态") + ErrInvalidRole = errors.New("无效的角色代码") + ErrCannotDisableSelf = errors.New("不能禁用自己的账号") +) + +// UserManageService 管理端用户管理服务 +type UserManageService struct { + userManageDAO *adminDAO.UserManageDAO + userDAO *authDAO.UserDAO + roleDAO *authDAO.RoleDAO +} + +// NewUserManageService 创建用户管理服务实例 +func NewUserManageService( + userManageDAO *adminDAO.UserManageDAO, + userDAO *authDAO.UserDAO, + roleDAO *authDAO.RoleDAO, +) *UserManageService { + return &UserManageService{ + userManageDAO: userManageDAO, + userDAO: userDAO, + roleDAO: roleDAO, + } +} + +// GetUserList 获取用户列表(分页 + 搜索 + 筛选) +func (s *UserManageService) GetUserList(ctx context.Context, req *dto.UserListRequest) (*dto.UserListResponse, error) { + funcName := "service.user_manage_service.GetUserList" + logs.Info(ctx, funcName, "获取用户列表") + + users, total, err := s.userManageDAO.ListUsers(ctx, req) + if err != nil { + return nil, err + } + + list := make([]dto.AdminUserInfo, 0, len(users)) + for _, user := range users { + roles, roleErr := s.roleDAO.GetUserRoleCodes(ctx, user.ID) + if roleErr != nil { + logs.Warn(ctx, funcName, "获取用户角色失败,降级为空角色列表", + zap.Int64("user_id", user.ID), zap.Error(roleErr)) + roles = []string{} + } + list = append(list, *s.buildAdminUserInfo(&user, roles)) + } + + return &dto.UserListResponse{ + Total: total, + List: list, + }, nil +} + +// GetUserDetail 获取单个用户详情(含角色信息) +func (s *UserManageService) GetUserDetail(ctx context.Context, userID int64) (*dto.AdminUserInfo, error) { + funcName := "service.user_manage_service.GetUserDetail" + logs.Info(ctx, funcName, "获取用户详情", zap.Int64("user_id", userID)) + + user, err := s.userDAO.FindByID(ctx, userID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrUserNotFound + } + return nil, err + } + + roles, roleErr := s.roleDAO.GetUserRoleCodes(ctx, user.ID) + if roleErr != nil { + logs.Warn(ctx, funcName, "获取用户角色失败,降级为空角色列表", + zap.Int64("user_id", user.ID), zap.Error(roleErr)) + roles = []string{} + } + return s.buildAdminUserInfo(user, roles), nil +} + +// UpdateUserStatus 启用/禁用用户 +// adminUserID 用于防止管理员禁用自己 +func (s *UserManageService) UpdateUserStatus(ctx context.Context, userID int64, status int, adminUserID int64) error { + funcName := "service.user_manage_service.UpdateUserStatus" + logs.Info(ctx, funcName, "更新用户状态", + zap.Int64("target_user_id", userID), + zap.Int("status", status), + zap.Int64("admin_user_id", adminUserID), + ) + + if userID == adminUserID { + return ErrCannotDisableSelf + } + + if status != constants.UserStatusActive && status != constants.UserStatusDisabled { + return ErrInvalidStatus + } + + // 确认用户存在 + _, err := s.userDAO.FindByID(ctx, userID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrUserNotFound + } + return err + } + + if err := s.userManageDAO.UpdateUserStatus(ctx, userID, status); err != nil { + return err + } + + logs.Info(ctx, funcName, "用户状态更新成功", + zap.Int64("user_id", userID), + zap.Int("new_status", status), + ) + return nil +} + +// AssignUserRole 分配角色给用户 +func (s *UserManageService) AssignUserRole(ctx context.Context, userID int64, roleCode string) error { + funcName := "service.user_manage_service.AssignUserRole" + logs.Info(ctx, funcName, "分配角色", + zap.Int64("user_id", userID), + zap.String("role_code", roleCode), + ) + + // 验证用户存在 + _, err := s.userDAO.FindByID(ctx, userID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrUserNotFound + } + return err + } + + // 查找角色 + role, err := s.roleDAO.FindByCode(ctx, roleCode) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrInvalidRole + } + return err + } + + // 分配角色(RoleDAO.AssignRole 内部会处理重复分配) + if err := s.roleDAO.AssignRole(ctx, userID, role.ID); err != nil { + return err + } + + logs.Info(ctx, funcName, "角色分配成功", + zap.Int64("user_id", userID), + zap.String("role_code", roleCode), + ) + return nil +} + +// CreateUser 管理员手动创建用户 +func (s *UserManageService) CreateUser(ctx context.Context, req *dto.AdminCreateUserRequest) (*dto.AdminUserInfo, error) { + funcName := "service.user_manage_service.CreateUser" + logs.Info(ctx, funcName, "管理员创建用户", + zap.String("username", req.Username), + zap.String("email", logs.MaskEmail(req.Email)), + ) + + existing, findErr := s.userDAO.FindByUsername(ctx, req.Username) + if findErr != nil && !errors.Is(findErr, gorm.ErrRecordNotFound) { + return nil, findErr + } + if existing != nil { + return nil, ErrUserExists + } + existing, findErr = s.userDAO.FindByEmail(ctx, req.Email) + if findErr != nil && !errors.Is(findErr, gorm.ErrRecordNotFound) { + return nil, findErr + } + if existing != nil { + return nil, ErrUserExists + } + + // 加密密码 + hashedPassword, err := utils.HashPassword(req.Password) + if err != nil { + return nil, err + } + + nickname := req.Nickname + if nickname == "" { + nickname = req.Username + } + + user := &model.User{ + Username: req.Username, + Email: req.Email, + PasswordHash: hashedPassword, + Nickname: nickname, + Status: constants.UserStatusActive, + } + if err := s.userDAO.Create(ctx, user); err != nil { + return nil, err + } + + roleCode := req.RoleCode + if roleCode == "" { + roleCode = constants.RoleUser + } + role, roleErr := s.roleDAO.FindByCode(ctx, roleCode) + if roleErr != nil { + logs.Warn(ctx, funcName, "角色查找失败,将跳过角色分配", + zap.String("role_code", roleCode), zap.Error(roleErr)) + } else { + if assignErr := s.roleDAO.AssignRole(ctx, user.ID, role.ID); assignErr != nil { + logs.Warn(ctx, funcName, "角色分配失败", + zap.Int64("user_id", user.ID), zap.Error(assignErr)) + } + } + + roles, rolesErr := s.roleDAO.GetUserRoleCodes(ctx, user.ID) + if rolesErr != nil { + logs.Warn(ctx, funcName, "获取新建用户角色失败", + zap.Int64("user_id", user.ID), zap.Error(rolesErr)) + roles = []string{} + } + logs.Info(ctx, funcName, "管理员创建用户成功", zap.Int64("user_id", user.ID)) + return s.buildAdminUserInfo(user, roles), nil +} + +// buildAdminUserInfo 从 model.User 构建 dto.AdminUserInfo +func (s *UserManageService) buildAdminUserInfo(user *model.User, roles []string) *dto.AdminUserInfo { + if roles == nil { + roles = []string{} + } + + info := &dto.AdminUserInfo{ + ID: user.ID, + Username: user.Username, + Email: user.Email, + Nickname: user.Nickname, + Avatar: user.Avatar, + Gender: user.Gender, + Status: user.Status, + StatusText: constants.UserStatusMap[user.Status], + Roles: roles, + CreatedAt: user.CreatedAt.Format("2006-01-02 15:04:05"), + UpdatedAt: user.UpdatedAt.Format("2006-01-02 15:04:05"), + } + + if user.Phone != nil { + info.Phone = *user.Phone + } + if user.LastLoginAt != nil { + info.LastLoginAt = user.LastLoginAt.Format("2006-01-02 15:04:05") + } + if user.LastLoginIP != nil { + info.LastLoginIP = *user.LastLoginIP + } + return info +} diff --git a/backend/go-service/app/dto/admin_dto.go b/backend/go-service/app/dto/admin_dto.go new file mode 100644 index 0000000..2403843 --- /dev/null +++ b/backend/go-service/app/dto/admin_dto.go @@ -0,0 +1,53 @@ +// Package dto 定义数据传输对象 +package dto + +// UserListRequest 管理端用户列表查询请求参数 +type UserListRequest struct { + Page int `form:"page" binding:"required,min=1"` // 页码,从 1 开始 + PageSize int `form:"page_size" binding:"required,min=1,max=100"` // 每页数量,1-100 + Keyword string `form:"keyword"` // 搜索关键词(匹配用户名或邮箱) + Status *int `form:"status"` // 状态筛选:1=正常, 2=禁用, 3=注销;为空则不筛选 +} + +// UserListResponse 管理端用户列表响应 +type UserListResponse struct { + Total int64 `json:"total"` // 符合条件的用户总数 + List []AdminUserInfo `json:"list"` // 当前页的用户列表 +} + +// AdminUserInfo 管理端用户信息(比 UserInfo 更详细,包含管理字段) +type AdminUserInfo struct { + ID int64 `json:"id"` // 用户 ID + Username string `json:"username"` // 用户名 + Email string `json:"email"` // 邮箱 + Nickname string `json:"nickname"` // 昵称 + Avatar string `json:"avatar"` // 头像 URL + Gender int `json:"gender"` // 性别:0=未知, 1=男, 2=女 + Phone string `json:"phone,omitempty"` // 手机号 + Status int `json:"status"` // 账号状态 + StatusText string `json:"status_text"` // 状态中文描述 + Roles []string `json:"roles"` // 角色代码列表 + LastLoginAt string `json:"last_login_at,omitempty"` // 最后登录时间 + LastLoginIP string `json:"last_login_ip,omitempty"` // 最后登录 IP + CreatedAt string `json:"created_at"` // 注册时间 + UpdatedAt string `json:"updated_at"` // 更新时间 +} + +// UpdateUserStatusRequest 更新用户状态请求 +type UpdateUserStatusRequest struct { + Status int `json:"status" binding:"required,oneof=1 2"` // 目标状态:1=正常, 2=禁用 +} + +// AssignRoleRequest 分配角色请求 +type AssignRoleRequest struct { + RoleCode string `json:"role_code" binding:"required"` // 角色代码:user/admin/super_admin +} + +// AdminCreateUserRequest 管理员手动创建用户请求 +type AdminCreateUserRequest struct { + Username string `json:"username" binding:"required,min=3,max=50"` // 用户名 + Email string `json:"email" binding:"required,email"` // 邮箱 + Password string `json:"password" binding:"required,min=6,max=50"` // 初始密码 + Nickname string `json:"nickname" binding:"max=50"` // 昵称(选填) + RoleCode string `json:"role_code" binding:"omitempty"` // 初始角色(选填,默认 user) +} diff --git a/backend/go-service/app/provider/provider.go b/backend/go-service/app/provider/provider.go index 4116689..cac7a0d 100644 --- a/backend/go-service/app/provider/provider.go +++ b/backend/go-service/app/provider/provider.go @@ -3,7 +3,8 @@ package provider import ( - "github.com/echochat/backend/app/auth/controller" + adminController "github.com/echochat/backend/app/admin/controller" + authController "github.com/echochat/backend/app/auth/controller" "github.com/echochat/backend/app/auth/service" "github.com/echochat/backend/config" "github.com/echochat/backend/pkg/db" @@ -14,12 +15,13 @@ import ( // App 应用根容器,持有基础设施组件和各模块的 Controller/Service type App struct { - Config *config.Config - DB *gorm.DB - Redis *redis.Client - AuthService *service.AuthService // Auth 认证服务 - AuthController *controller.AuthController // 前台认证控制器 - AdminAuthController *controller.AdminAuthController // 后台认证控制器 + Config *config.Config + DB *gorm.DB + Redis *redis.Client + AuthService *service.AuthService // Auth 认证服务 + AuthController *authController.AuthController // 前台认证控制器 + AdminAuthController *authController.AdminAuthController // 后台认证控制器 + UserManageController *adminController.UserManageController // 管理端用户管理控制器 } // NewApp 创建应用实例 @@ -28,16 +30,18 @@ func NewApp( gormDB *gorm.DB, redisClient *redis.Client, authService *service.AuthService, - authCtrl *controller.AuthController, - adminAuthCtrl *controller.AdminAuthController, + authCtrl *authController.AuthController, + adminAuthCtrl *authController.AdminAuthController, + userManageCtrl *adminController.UserManageController, ) *App { return &App{ - Config: cfg, - DB: gormDB, - Redis: redisClient, - AuthService: authService, - AuthController: authCtrl, - AdminAuthController: adminAuthCtrl, + Config: cfg, + DB: gormDB, + Redis: redisClient, + AuthService: authService, + AuthController: authCtrl, + AdminAuthController: adminAuthCtrl, + UserManageController: userManageCtrl, } } diff --git a/backend/go-service/app/provider/wire.go b/backend/go-service/app/provider/wire.go index 055ec6a..52b07eb 100644 --- a/backend/go-service/app/provider/wire.go +++ b/backend/go-service/app/provider/wire.go @@ -4,6 +4,7 @@ package provider import ( + "github.com/echochat/backend/app/admin" "github.com/echochat/backend/app/auth" "github.com/echochat/backend/config" "github.com/google/wire" @@ -14,6 +15,7 @@ func InitializeApp(cfg *config.Config) (*App, error) { wire.Build( InfraSet, auth.AuthSet, + admin.AdminSet, ) return nil, nil } diff --git a/backend/go-service/app/provider/wire_gen.go b/backend/go-service/app/provider/wire_gen.go index c753327..2f698c9 100644 --- a/backend/go-service/app/provider/wire_gen.go +++ b/backend/go-service/app/provider/wire_gen.go @@ -7,6 +7,9 @@ package provider import ( + adminController "github.com/echochat/backend/app/admin/controller" + adminDAO "github.com/echochat/backend/app/admin/dao" + adminService "github.com/echochat/backend/app/admin/service" "github.com/echochat/backend/app/auth/controller" "github.com/echochat/backend/app/auth/dao" "github.com/echochat/backend/app/auth/service" @@ -35,6 +38,9 @@ func InitializeApp(cfg *config.Config) (*App, error) { authService := service.NewAuthService(userDAO, roleDAO, jwtConfig, tokenStore) authController := controller.NewAuthController(authService) adminAuthController := controller.NewAdminAuthController(authService) - app := NewApp(cfg, gormDB, client, authService, authController, adminAuthController) + userManageDAO := adminDAO.NewUserManageDAO(gormDB) + userManageService := adminService.NewUserManageService(userManageDAO, userDAO, roleDAO) + userManageController := adminController.NewUserManageController(userManageService) + app := NewApp(cfg, gormDB, client, authService, authController, adminAuthController, userManageController) return app, nil } diff --git a/backend/go-service/cmd/server/main.go b/backend/go-service/cmd/server/main.go index babca53..59c1c0c 100644 --- a/backend/go-service/cmd/server/main.go +++ b/backend/go-service/cmd/server/main.go @@ -20,8 +20,12 @@ import ( ) func main() { - // 1. 加载配置 - cfg, err := config.Load("config", "config.dev") + // 1. 加载配置(通过 CONFIG_NAME 环境变量指定配置文件名,默认 config.dev) + configName := os.Getenv("CONFIG_NAME") + if configName == "" { + configName = "config.dev" + } + cfg, err := config.Load("config", configName) if err != nil { fmt.Printf("加载配置失败: %v\n", err) os.Exit(1) diff --git a/backend/go-service/config/config.docker.yaml b/backend/go-service/config/config.docker.yaml new file mode 100644 index 0000000..15b5bab --- /dev/null +++ b/backend/go-service/config/config.docker.yaml @@ -0,0 +1,42 @@ +# ============================================================ +# EchoChat Docker 容器内配置 +# 与 config.dev.yaml 的区别:数据库和 Redis 地址使用 Docker 服务名 +# ============================================================ + +server: + port: 8085 + mode: debug + +database: + host: postgres + port: 5432 + user: echochat + password: echochat_dev_2026 + dbname: echochat + sslmode: disable + max_idle_conns: 10 + max_open_conns: 100 + +redis: + host: redis + port: 6379 + password: "" + db: 0 + +jwt: + secret: echochat-dev-jwt-secret-2026-please-change-in-production + access_expire_min: 120 + refresh_expire_day: 7 + issuer: echochat + +log: + level: debug + format: text + output_path: stdout + file: + enable: true + dir: logs + max_size: 50 + max_backups: 10 + max_age: 30 + compress: false diff --git a/backend/go-service/pkg/middleware/recovery.go b/backend/go-service/pkg/middleware/recovery.go index 3a57334..509eb20 100644 --- a/backend/go-service/pkg/middleware/recovery.go +++ b/backend/go-service/pkg/middleware/recovery.go @@ -3,6 +3,7 @@ package middleware import ( "net/http" "runtime/debug" + "time" "github.com/echochat/backend/pkg/logs" "github.com/echochat/backend/pkg/utils" @@ -28,6 +29,7 @@ func Recovery() gin.HandlerFunc { Code: 500, Message: "服务内部错误", TraceID: logs.GetTraceID(ctx), + Time: time.Now().Format("2006-01-02 15:04:05"), }) } }() diff --git a/backend/go-service/router/router.go b/backend/go-service/router/router.go index 5ce628a..a2262a0 100644 --- a/backend/go-service/router/router.go +++ b/backend/go-service/router/router.go @@ -6,6 +6,7 @@ package router import ( "time" + "github.com/echochat/backend/app/admin" "github.com/echochat/backend/app/auth" "github.com/echochat/backend/app/provider" "github.com/echochat/backend/pkg/middleware" @@ -30,10 +31,10 @@ func Setup(engine *gin.Engine, app *provider.App) { // --- 各模块路由注册 --- auth.RegisterRoutes(engine, app.AuthController, app.AdminAuthController, jwtAuth) + admin.RegisterRoutes(engine, app.UserManageController, jwtAuth) // [未来] im.RegisterRoutes(engine, app.ImController, jwtAuth) // [未来] meeting.RegisterRoutes(engine, app.MeetingController, jwtAuth) // [未来] contact.RegisterRoutes(engine, app.ContactController, jwtAuth) // [未来] notify.RegisterRoutes(engine, app.NotifyController, jwtAuth) - // [未来] admin.RegisterRoutes(engine, app.AdminController, jwtAuth) } diff --git a/deploy/docker-compose.dev.yml b/deploy/docker-compose.dev.yml index c60bdf5..d087225 100644 --- a/deploy/docker-compose.dev.yml +++ b/deploy/docker-compose.dev.yml @@ -32,6 +32,22 @@ services: timeout: 5s retries: 5 + go-service: + build: + context: ../backend/go-service + dockerfile: Dockerfile + container_name: echochat-go-service + environment: + CONFIG_NAME: config.docker + ports: + - "8085:8085" + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + restart: unless-stopped + volumes: pgdata: redisdata: diff --git a/design-system/echochat/MASTER.md b/design-system/echochat/MASTER.md new file mode 100644 index 0000000..88eeacc --- /dev/null +++ b/design-system/echochat/MASTER.md @@ -0,0 +1,202 @@ +# Design System Master File + +> **LOGIC:** When building a specific page, first check `design-system/pages/[page-name].md`. +> If that file exists, its rules **override** this Master file. +> If not, strictly follow the rules below. + +--- + +**Project:** EchoChat +**Generated:** 2026-03-02 10:27:26 +**Category:** Analytics Dashboard + +--- + +## Global Rules + +### Color Palette + +| Role | Hex | CSS Variable | +|------|-----|--------------| +| Primary | `#7C3AED` | `--color-primary` | +| Secondary | `#A78BFA` | `--color-secondary` | +| CTA/Accent | `#F97316` | `--color-cta` | +| Background | `#FAF5FF` | `--color-background` | +| Text | `#4C1D95` | `--color-text` | + +**Color Notes:** Excitement purple + action orange + +### Typography + +- **Heading Font:** Fira Code +- **Body Font:** Fira Sans +- **Mood:** dashboard, data, analytics, code, technical, precise +- **Google Fonts:** [Fira Code + Fira Sans](https://fonts.google.com/share?selection.family=Fira+Code:wght@400;500;600;700|Fira+Sans:wght@300;400;500;600;700) + +**CSS Import:** +```css +@import url('https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500;600;700&family=Fira+Sans:wght@300;400;500;600;700&display=swap'); +``` + +### Spacing Variables + +| Token | Value | Usage | +|-------|-------|-------| +| `--space-xs` | `4px` / `0.25rem` | Tight gaps | +| `--space-sm` | `8px` / `0.5rem` | Icon gaps, inline spacing | +| `--space-md` | `16px` / `1rem` | Standard padding | +| `--space-lg` | `24px` / `1.5rem` | Section padding | +| `--space-xl` | `32px` / `2rem` | Large gaps | +| `--space-2xl` | `48px` / `3rem` | Section margins | +| `--space-3xl` | `64px` / `4rem` | Hero padding | + +### Shadow Depths + +| Level | Value | Usage | +|-------|-------|-------| +| `--shadow-sm` | `0 1px 2px rgba(0,0,0,0.05)` | Subtle lift | +| `--shadow-md` | `0 4px 6px rgba(0,0,0,0.1)` | Cards, buttons | +| `--shadow-lg` | `0 10px 15px rgba(0,0,0,0.1)` | Modals, dropdowns | +| `--shadow-xl` | `0 20px 25px rgba(0,0,0,0.15)` | Hero images, featured cards | + +--- + +## Component Specs + +### Buttons + +```css +/* Primary Button */ +.btn-primary { + background: #F97316; + color: white; + padding: 12px 24px; + border-radius: 8px; + font-weight: 600; + transition: all 200ms ease; + cursor: pointer; +} + +.btn-primary:hover { + opacity: 0.9; + transform: translateY(-1px); +} + +/* Secondary Button */ +.btn-secondary { + background: transparent; + color: #7C3AED; + border: 2px solid #7C3AED; + padding: 12px 24px; + border-radius: 8px; + font-weight: 600; + transition: all 200ms ease; + cursor: pointer; +} +``` + +### Cards + +```css +.card { + background: #FAF5FF; + border-radius: 12px; + padding: 24px; + box-shadow: var(--shadow-md); + transition: all 200ms ease; + cursor: pointer; +} + +.card:hover { + box-shadow: var(--shadow-lg); + transform: translateY(-2px); +} +``` + +### Inputs + +```css +.input { + padding: 12px 16px; + border: 1px solid #E2E8F0; + border-radius: 8px; + font-size: 16px; + transition: border-color 200ms ease; +} + +.input:focus { + border-color: #7C3AED; + outline: none; + box-shadow: 0 0 0 3px #7C3AED20; +} +``` + +### Modals + +```css +.modal-overlay { + background: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(4px); +} + +.modal { + background: white; + border-radius: 16px; + padding: 32px; + box-shadow: var(--shadow-xl); + max-width: 500px; + width: 90%; +} +``` + +--- + +## Style Guidelines + +**Style:** Data-Dense Dashboard + +**Keywords:** Multiple charts/widgets, data tables, KPI cards, minimal padding, grid layout, space-efficient, maximum data visibility + +**Best For:** Business intelligence dashboards, financial analytics, enterprise reporting, operational dashboards, data warehousing + +**Key Effects:** Hover tooltips, chart zoom on click, row highlighting on hover, smooth filter animations, data loading spinners + +### Page Pattern + +**Pattern Name:** Data-Dense + Drill-Down + +- **CTA Placement:** Above fold +- **Section Order:** Hero > Features > CTA + +--- + +## Anti-Patterns (Do NOT Use) + +- ❌ Ornate design +- ❌ No filtering + +### Additional Forbidden Patterns + +- ❌ **Emojis as icons** — Use SVG icons (Heroicons, Lucide, Simple Icons) +- ❌ **Missing cursor:pointer** — All clickable elements must have cursor:pointer +- ❌ **Layout-shifting hovers** — Avoid scale transforms that shift layout +- ❌ **Low contrast text** — Maintain 4.5:1 minimum contrast ratio +- ❌ **Instant state changes** — Always use transitions (150-300ms) +- ❌ **Invisible focus states** — Focus states must be visible for a11y + +--- + +## Pre-Delivery Checklist + +Before delivering any UI code, verify: + +- [ ] No emojis used as icons (use SVG instead) +- [ ] All icons from consistent icon set (Heroicons/Lucide) +- [ ] `cursor-pointer` on all clickable elements +- [ ] Hover states with smooth transitions (150-300ms) +- [ ] Light mode: text contrast 4.5:1 minimum +- [ ] Focus states visible for keyboard navigation +- [ ] `prefers-reduced-motion` respected +- [ ] Responsive: 375px, 768px, 1024px, 1440px +- [ ] No content hidden behind fixed navbars +- [ ] No horizontal scroll on mobile diff --git a/design-system/echochat/pages/admin-login.md b/design-system/echochat/pages/admin-login.md new file mode 100644 index 0000000..46c1dc4 --- /dev/null +++ b/design-system/echochat/pages/admin-login.md @@ -0,0 +1,46 @@ +# Admin Login Page Overrides + +> **PROJECT:** EchoChat +> **Generated:** 2026-03-02 10:27:26 +> **Page Type:** Dashboard / Data View + +> ⚠️ **IMPORTANT:** Rules in this file **override** the Master file (`design-system/MASTER.md`). +> Only deviations from the Master are documented here. For all other rules, refer to the Master. + +--- + +## Page-Specific Rules + +### Layout Overrides + +- **Max Width:** 1200px (standard) +- **Layout:** Full-width sections, centered content + +### Spacing Overrides + +- No overrides — use Master spacing + +### Typography Overrides + +- No overrides — use Master typography + +### Color Overrides + +- No overrides — use Master colors + +### Component Overrides + +- Avoid: Use arbitrary large z-index values + +--- + +## Page-Specific Components + +- No unique components for this page + +--- + +## Recommendations + +- Effects: Deal movement animations, metric updates, leaderboard ranking changes, gauge needle movements, status change highlights +- Layout: Define z-index scale system (10 20 30 50) diff --git a/design-system/echochat/pages/admin-user-list.md b/design-system/echochat/pages/admin-user-list.md new file mode 100644 index 0000000..8bc6db5 --- /dev/null +++ b/design-system/echochat/pages/admin-user-list.md @@ -0,0 +1,52 @@ +# Admin User List Page Overrides + +> **PROJECT:** EchoChat +> **Generated:** 2026-03-02 10:58:45 +> **Page Type:** Dashboard / Data View + +> ⚠️ **IMPORTANT:** Rules in this file **override** the Master file (`design-system/MASTER.md`). +> Only deviations from the Master are documented here. For all other rules, refer to the Master. + +--- + +## Page-Specific Rules + +### Layout Overrides + +- **Max Width:** 1400px or full-width +- **Grid:** 12-column grid for data flexibility +- **Sections:** 1. Hero (product + aggregate rating), 2. Rating breakdown, 3. Individual reviews, 4. Buy/CTA + +### Spacing Overrides + +- **Content Density:** High — optimize for information display + +### Typography Overrides + +- No overrides — use Master typography + +### Color Overrides + +- **Strategy:** Trust colors. Star ratings gold. Verified badge green. Review sentiment colors. + +### Component Overrides + +- Avoid: Use for flat single-level sites +- Avoid: Ignore accessibility motion settings +- Avoid: Validate only on submit + +--- + +## Page-Specific Components + +- No unique components for this page + +--- + +## Recommendations + +- Effects: Hover tooltips, chart zoom on click, row highlighting on hover, smooth filter animations, data loading spinners +- Navigation: Use for sites with 3+ levels of depth +- Animation: Check prefers-reduced-motion media query +- Forms: Validate on blur for most fields +- CTA Placement: After reviews summary + Buy button alongside reviews diff --git a/design-system/echochat/pages/login.md b/design-system/echochat/pages/login.md new file mode 100644 index 0000000..f78ec8a --- /dev/null +++ b/design-system/echochat/pages/login.md @@ -0,0 +1,50 @@ +# Login Page Overrides + +> **PROJECT:** EchoChat +> **Generated:** 2026-03-02 09:58:55 +> **Page Type:** Authentication + +> ⚠️ **IMPORTANT:** Rules in this file **override** the Master file (`design-system/MASTER.md`). +> Only deviations from the Master are documented here. For all other rules, refer to the Master. + +--- + +## Page-Specific Rules + +### Layout Overrides + +- **Max Width:** 800px (narrow, focused) +- **Layout:** Single column, centered +- **Sections:** 1. Hero (benefit headline), 2. Lead magnet preview (ebook cover, checklist, etc), 3. Form (minimal fields), 4. CTA submit + +### Spacing Overrides + +- **Content Density:** Low — focus on clarity + +### Typography Overrides + +- No overrides — use Master typography + +### Color Overrides + +- **Strategy:** Lead magnet: Professional design. Form: Clean white bg. Inputs: Light border #CCCCCC. CTA: Brand color + +### Component Overrides + +- Avoid: No feedback after submit +- Avoid: Placeholder-only inputs + +--- + +## Page-Specific Components + +- No unique components for this page + +--- + +## Recommendations + +- Effects: Hover states on CTA (color shift, slight scale), form field focus animations, loading spinner, success feedback +- Forms: Show loading then success/error state +- Accessibility: Use label with for attribute or wrap input +- CTA Placement: Form CTA: Submit button diff --git a/docs/api/admin/user.md b/docs/api/admin/user.md index 1558356..e3e4f7c 100644 --- a/docs/api/admin/user.md +++ b/docs/api/admin/user.md @@ -26,13 +26,41 @@ **查询参数:** -| 参数 | 类型 | 默认值 | 说明 | -|------|------|--------|------| -| keyword | string | 无 | 搜索关键词(匹配用户名/邮箱/昵称) | -| status | int | 无 | 按状态筛选:1=正常,2=禁用,3=注销 | -| role | string | 无 | 按角色筛选:user / admin / super_admin | -| page | int | 1 | 页码 | -| page_size | int | 20 | 每页数量 | +| 参数 | 类型 | 必填 | 默认值 | 说明 | +|------|------|------|--------|------| +| page | int | 是 | - | 页码(从 1 开始) | +| page_size | int | 是 | - | 每页数量(1-100) | +| keyword | string | 否 | 无 | 搜索关键词(模糊匹配用户名或邮箱) | +| status | int | 否 | 无 | 按状态筛选:1=正常,2=禁用,3=注销 | + +**成功响应:** +```json +{ + "code": 0, + "message": "success", + "data": { + "total": 100, + "list": [ + { + "id": 1, + "username": "zhangsan", + "email": "zhangsan@example.com", + "nickname": "张三", + "avatar": "", + "gender": 1, + "phone": "13800138000", + "status": 1, + "status_text": "正常", + "roles": ["user"], + "last_login_at": "2026-02-27 10:00:00", + "last_login_ip": "192.168.1.100", + "created_at": "2026-02-20 08:00:00", + "updated_at": "2026-02-27 10:00:00" + } + ] + } +} +``` --- @@ -46,20 +74,22 @@ ```json { "code": 0, - "message": "ok", + "message": "success", "data": { "id": 1, "username": "zhangsan", "email": "zhangsan@example.com", "nickname": "张三", - "avatar": "https://...", + "avatar": "", "gender": 1, "phone": "13800138000", "status": 1, + "status_text": "正常", "roles": ["user"], "last_login_at": "2026-02-27 10:00:00", "last_login_ip": "192.168.1.100", - "created_at": "2026-02-20 08:00:00" + "created_at": "2026-02-20 08:00:00", + "updated_at": "2026-02-27 10:00:00" } } ``` @@ -86,7 +116,7 @@ `PUT /api/v1/admin/users/:id/role` -**权限:** super_admin +**权限:** admin / super_admin **请求参数:** @@ -102,10 +132,14 @@ **权限:** admin -**请求参数:** 同前台用户注册接口,额外支持: +**请求参数:** | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| +| username | string | 是 | 用户名,3-50 字符 | +| email | string | 是 | 邮箱地址 | +| password | string | 是 | 初始密码,6-50 字符 | +| nickname | string | 否 | 昵称(默认使用用户名) | | role_code | string | 否 | 指定角色,默认为 user | --- diff --git a/docs/architecture/system-architecture.md b/docs/architecture/system-architecture.md index fa4134d..562d754 100644 --- a/docs/architecture/system-architecture.md +++ b/docs/architecture/system-architecture.md @@ -283,10 +283,39 @@ services: | **mediasoup** | 最高性能的开源 SFU,C++ 实现 | | **PostgreSQL 17** | 强一致性、JSONB 支持、性能优异 | | **Redis 7** | 实时状态存储、发布订阅、高速缓存 | -| **uniapp (Vue 3)** | 一套代码多端运行(H5/App/小程序) | -| **Element Plus** | Vue 3 生态最成熟的 PC 端组件库 | +| **uniapp (Vue 3.4)** | 一套代码多端运行(H5/App/小程序),Vue 版本由 uni-app 框架锁定 | +| **Element Plus** | Vue 3 生态最成熟的 PC 端组件库,管理端使用 | | **Docker Compose** | 轻量级容器编排,适合初期和开发环境 | +### 7.1 前端技术栈版本策略 + +系统包含两个独立前端项目,因框架约束,版本策略有所区别: + +| 前端项目 | 目录 | 框架 | Vue 版本 | 状态管理 | 原因 | +|----------|------|------|---------|---------|------| +| 前台用户端 | `frontend/` | uni-app 3.0 | 3.4.21(框架锁定) | Pinia 2.x | uni-app 尚未适配 Vue 3.5,Pinia 3.x 需要 Vue >= 3.5.11 | +| 后台管理端 | `admin/` | Vue 3 + Element Plus | 3.5+(不受限) | Pinia 3.x | 独立 Vue 3 项目,可使用最新版本 | + +**npm 兼容性说明:** uni-app 的 peer dependency 链与 npm 7+ 的严格依赖解析存在冲突,`frontend/` 项目需在 `.npmrc` 中设置 `legacy-peer-deps=true`,这是 uni-app 社区的标准做法。 + +**版本升级策略:** 当 uni-app 正式适配 Vue 3.5+ 后,可统一将前台的 Pinia 升级至 3.x,API 层面几乎无需改动(Pinia 2.x 与 3.x 的 `defineStore` API 完全兼容)。 + +### 7.2 两端开发规范差异 + +前台用户端和后台管理端是**完全独立**的前端项目,开发规范、依赖版本、构建方式不需要强制统一: + +| 维度 | 前台用户端 (`frontend/`) | 后台管理端 (`admin/`) | +|------|------------------------|---------------------| +| 框架 | uni-app 3.0(Vue 3.4.21) | Vue 3.5+ + Vite | +| 状态管理 | Pinia 2.x(`pinia-plugin-persistedstate@3`) | Pinia 3.x(最新版) | +| HTTP 客户端 | `uni.request` 封装 | Axios | +| 路由 | `pages.json` + `uni.navigateTo/switchTab` | Vue Router 4 | +| UI 组件 | 原生 uni-app 组件(``/``/``) | Element Plus | +| 模块系统 | ES Module(Vite 要求) | ES Module(Vite 标准) | +| npm 配置 | 需要 `.npmrc` 设置 `legacy-peer-deps=true` | 标准 npm 配置 | +| 适配能力 | H5 / 小程序 / App / 桌面端 | 仅 PC 浏览器 | +| 设计系统 | ui-ux-pro-max 生成的设计系统 | ui-ux-pro-max + Element Plus 主题 | + --- ## 八、日志系统与链路追踪设计 diff --git a/docs/plans/2026-02-27-phase1-foundation-and-auth.md b/docs/plans/2026-02-27-phase1-foundation-and-auth.md index 030b08a..5792ed0 100644 --- a/docs/plans/2026-02-27-phase1-foundation-and-auth.md +++ b/docs/plans/2026-02-27-phase1-foundation-and-auth.md @@ -6,7 +6,7 @@ **架构:** Go 单体模块化服务 + uniapp 前台 + Vue3 管理端 + PostgreSQL + Redis,Docker Compose 编排。 -**技术栈:** Go 1.23+ (Gin + GORM + Wire + zap) / Vue 3.4+ / uniapp 3.0 / Element Plus / PostgreSQL 17 / Redis 7 / Docker Compose +**技术栈:** Go 1.23+ (Gin + GORM + Wire + zap) / uniapp 3.0 (Vue 3.4 + Pinia 2.x) / Vue 3.5+ 管理端 (Pinia 3.x + Element Plus) / PostgreSQL 17 / Redis 7 / Docker Compose **设计文档:** `docs/plans/2026-02-27-echochat-system-design.md` @@ -30,11 +30,52 @@ 7. **中间件**:说明中间件的作用、执行顺序依赖和副作用 **前端注释规范(Vue/uniapp):** -1. **组件注释**:每个 `.vue` 文件顶部说明组件用途和依赖 -2. **API 函数**:每个 API 调用函数说明对应后端接口 +1. **组件注释**:每个 `.vue` 文件顶部 `` 说明组件用途、功能清单、对应 API 和文档引用 +2. **API 函数**:每个 API 调用函数说明对应后端接口路径和参数 3. **Store**:state 字段、action 方法需要注释说明 4. **工具函数**:参数和返回值类型、用途说明 5. **复杂模板逻辑**:`v-if`/`v-for` 等复杂条件需要注释说明 +6. **CSS 注释**:` diff --git a/frontend/src/main.js b/frontend/src/main.js index dd44bf7..8163f52 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -1,10 +1,27 @@ -import { - createSSRApp -} from "vue"; -import App from "./App.vue"; +/** + * 应用入口文件 + * + * 初始化 Vue 应用实例,集成: + * - Pinia 状态管理(含持久化插件) + * + * uni-app 要求导出 createApp 工厂函数,由框架负责调用 + */ +import { createSSRApp } from "vue" +import * as Pinia from "pinia" +import piniaPluginPersistedstate from "pinia-plugin-persistedstate" +import App from "./App.vue" + export function createApp() { - const app = createSSRApp(App); + const app = createSSRApp(App) + + // 创建 Pinia 实例并注册持久化插件 + const pinia = Pinia.createPinia() + pinia.use(piniaPluginPersistedstate) + app.use(pinia) + return { app, - }; -} + // uni-app 要求返回 Pinia 实例,用于 SSR 场景 + Pinia, + } +} diff --git a/frontend/src/pages.json b/frontend/src/pages.json index bf32f65..58a2caf 100644 --- a/frontend/src/pages.json +++ b/frontend/src/pages.json @@ -1,16 +1,66 @@ { - "pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages + "pages": [ { "path": "pages/index/index", "style": { - "navigationBarTitleText": "uni-app" + "navigationBarTitleText": "EchoChat", + "navigationStyle": "custom" + } + }, + { + "path": "pages/auth/login", + "style": { + "navigationBarTitleText": "登录" + } + }, + { + "path": "pages/auth/register", + "style": { + "navigationBarTitleText": "注册" + } + }, + { + "path": "pages/chat/index", + "style": { + "navigationBarTitleText": "消息" + } + }, + { + "path": "pages/contact/index", + "style": { + "navigationBarTitleText": "联系人" + } + }, + { + "path": "pages/meeting/index", + "style": { + "navigationBarTitleText": "会议" + } + }, + { + "path": "pages/profile/index", + "style": { + "navigationBarTitleText": "我的" } } ], + "tabBar": { + "custom": true, + "color": "#94A3B8", + "selectedColor": "#2563EB", + "backgroundColor": "#FFFFFF", + "borderStyle": "white", + "list": [ + { "pagePath": "pages/chat/index", "text": "消息" }, + { "pagePath": "pages/contact/index", "text": "联系人" }, + { "pagePath": "pages/meeting/index", "text": "会议" }, + { "pagePath": "pages/profile/index", "text": "我的" } + ] + }, "globalStyle": { "navigationBarTextStyle": "black", - "navigationBarTitleText": "uni-app", - "navigationBarBackgroundColor": "#F8F8F8", - "backgroundColor": "#F8F8F8" + "navigationBarTitleText": "EchoChat", + "navigationBarBackgroundColor": "#FFFFFF", + "backgroundColor": "#F8FAFC" } -} +} diff --git a/frontend/src/pages/auth/login.vue b/frontend/src/pages/auth/login.vue new file mode 100644 index 0000000..17b5387 --- /dev/null +++ b/frontend/src/pages/auth/login.vue @@ -0,0 +1,347 @@ + + + + + + diff --git a/frontend/src/pages/auth/register.vue b/frontend/src/pages/auth/register.vue new file mode 100644 index 0000000..f0cb676 --- /dev/null +++ b/frontend/src/pages/auth/register.vue @@ -0,0 +1,427 @@ + + + + + + diff --git a/frontend/src/pages/chat/index.vue b/frontend/src/pages/chat/index.vue new file mode 100644 index 0000000..29405e2 --- /dev/null +++ b/frontend/src/pages/chat/index.vue @@ -0,0 +1,62 @@ + + + + + + diff --git a/frontend/src/pages/contact/index.vue b/frontend/src/pages/contact/index.vue new file mode 100644 index 0000000..f5a7047 --- /dev/null +++ b/frontend/src/pages/contact/index.vue @@ -0,0 +1,58 @@ + + + + + + diff --git a/frontend/src/pages/index/index.vue b/frontend/src/pages/index/index.vue index 23afcff..2513c5a 100644 --- a/frontend/src/pages/index/index.vue +++ b/frontend/src/pages/index/index.vue @@ -1,48 +1,52 @@ + - diff --git a/frontend/src/pages/meeting/index.vue b/frontend/src/pages/meeting/index.vue new file mode 100644 index 0000000..34b608c --- /dev/null +++ b/frontend/src/pages/meeting/index.vue @@ -0,0 +1,58 @@ + + + + + + diff --git a/frontend/src/pages/profile/index.vue b/frontend/src/pages/profile/index.vue new file mode 100644 index 0000000..fd788a6 --- /dev/null +++ b/frontend/src/pages/profile/index.vue @@ -0,0 +1,213 @@ + + + + + + diff --git a/frontend/src/services/websocket.js b/frontend/src/services/websocket.js new file mode 100644 index 0000000..4c9a620 --- /dev/null +++ b/frontend/src/services/websocket.js @@ -0,0 +1,27 @@ +/** + * WebSocket 服务模块(占位) + * + * 后续阶段实现 IM 即时通讯和会议信令时使用。 + * 职责: + * - 建立和管理 WebSocket 连接 + * - 心跳保活机制 + * - 断线自动重连 + * - 消息收发与事件分发 + * + * 对应架构设计见:docs/architecture/system-architecture.md 第 4.1 节 + * 日志规范见:docs/architecture/system-architecture.md 第 8.7 节 + */ + +// TODO: Phase 2 - IM 模块开发时实现 +// 预期功能: +// - connect(token) — 建立 WebSocket 连接,附带认证 Token +// - disconnect() — 主动断开连接 +// - send(event, data) — 发送消息 +// - on(event, callback) — 监听事件 +// - off(event, callback) — 移除监听 +// - 自动心跳(30s 间隔) +// - 断线重连(指数退避,最大 5 次) + +export default { + // 占位导出,后续实现时替换 +} diff --git a/frontend/src/store/user.js b/frontend/src/store/user.js new file mode 100644 index 0000000..02eb708 --- /dev/null +++ b/frontend/src/store/user.js @@ -0,0 +1,217 @@ +/** + * 用户状态管理 Store + * + * 管理用户认证状态和个人信息,提供: + * - 登录/注册/登出操作 + * - Token 自动管理(存储 + 刷新) + * - 用户信息缓存与更新 + * - 持久化存储(通过 pinia-plugin-persistedstate,H5 用 localStorage,小程序用 uni 存储) + * + * 对应后端 API:/api/v1/auth/* + * 接口文档见:docs/api/frontend/auth.md + */ + +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import authApi from '@/api/auth' +import { + saveToken, + getToken, + getRefreshToken, + removeToken, + saveUserInfo, + getUserInfo, + removeUserInfo, + clearAll +} from '@/utils/storage' + +/** + * 用户 Store(Composition API 风格) + * + * state: + * - token: Access Token + * - userInfo: 用户信息对象 + * + * getters: + * - isLoggedIn: 是否已登录 + * - username: 用户名快捷访问 + * - roles: 用户角色列表 + * + * actions: + * - login: 用户登录 + * - register: 用户注册 + * - logout: 退出登录 + * - getProfile: 获取/刷新用户信息 + * - updateProfile: 更新个人信息 + * - changePassword: 修改密码 + * - refreshUserToken: 刷新 Token + */ +export const useUserStore = defineStore('user', () => { + // ==================== State ==================== + + /** Access Token,用于 API 认证 */ + const token = ref(getToken() || '') + + /** 用户信息对象,与后端 dto.UserInfo 结构一致 */ + const userInfo = ref(getUserInfo() || null) + + // ==================== Getters ==================== + + /** 是否已登录(Token 存在视为已登录) */ + const isLoggedIn = computed(() => !!token.value) + + /** 用户名快捷访问 */ + const username = computed(() => userInfo.value?.username || '') + + /** 用户角色列表 */ + const roles = computed(() => userInfo.value?.roles || []) + + // ==================== Actions ==================== + + /** + * 处理登录/注册成功后的通用逻辑 + * 保存 Token 和用户信息到 Store 和本地存储 + * + * @param {Object} data - 后端返回的登录响应 { token, refresh_token, expires_in, user } + */ + const _handleAuthSuccess = (data) => { + token.value = data.token + userInfo.value = data.user + saveToken(data.token, data.refresh_token, data.expires_in) + saveUserInfo(data.user) + } + + /** + * 用户登录 + * + * @param {Object} loginData - { account, password } + * @returns {Promise} 登录响应数据 + */ + const login = async (loginData) => { + const res = await authApi.login(loginData) + _handleAuthSuccess(res.data) + return res.data + } + + /** + * 用户注册(注册成功后自动登录) + * + * @param {Object} registerData - { username, email, password, nickname? } + * @returns {Promise} 注册响应数据 + */ + const register = async (registerData) => { + const res = await authApi.register(registerData) + _handleAuthSuccess(res.data) + return res.data + } + + /** + * 退出登录 + * 1. 调用后端 API 从 Redis 删除 Token(使 Token 立即失效) + * 2. 清除本地存储的 Token 和用户信息 + * 3. 重置 Store 状态 + */ + const logout = async () => { + try { + await authApi.logout() + } catch (e) { + // 即使后端请求失败,也要清除本地状态(避免用户被卡住) + console.warn('登出 API 调用失败,仍然清除本地状态', e) + } + token.value = '' + userInfo.value = null + clearAll() + } + + /** + * 获取/刷新用户信息 + * 从后端获取最新的用户资料并更新 Store 和本地缓存 + * + * @returns {Promise} 用户信息 + */ + const getProfile = async () => { + const res = await authApi.getProfile() + userInfo.value = res.data + saveUserInfo(res.data) + return res.data + } + + /** + * 更新个人信息 + * + * @param {Object} data - 需要更新的字段 { nickname?, avatar?, gender?, phone? } + * @returns {Promise} 更新后的用户信息 + */ + const updateProfile = async (data) => { + const res = await authApi.updateProfile(data) + userInfo.value = res.data + saveUserInfo(res.data) + return res.data + } + + /** + * 修改密码 + * + * @param {Object} data - { old_password, new_password } + * @returns {Promise} + */ + const changePassword = async (data) => { + return await authApi.changePassword(data) + } + + /** + * 刷新 Token + * 使用 Refresh Token 获取新的 Access Token + * 新 Token 会覆盖 Redis 中的旧值(实现单设备登录) + * + * @returns {Promise} 刷新是否成功 + */ + const refreshUserToken = async () => { + const refreshTokenValue = getRefreshToken() + if (!refreshTokenValue) return false + + try { + const res = await authApi.refreshToken(refreshTokenValue) + _handleAuthSuccess(res.data) + return true + } catch (e) { + // Refresh Token 也过期或无效,需要重新登录 + token.value = '' + userInfo.value = null + clearAll() + return false + } + } + + return { + // state + token, + userInfo, + // getters + isLoggedIn, + username, + roles, + // actions + login, + register, + logout, + getProfile, + updateProfile, + changePassword, + refreshUserToken + } +}, { + /** + * Pinia 持久化配置 + * H5 环境使用 localStorage,小程序/App 使用 uni 存储 + * 仅持久化 token 和 userInfo,避免存储过多无关数据 + */ + persist: { + key: 'echo-user-store', + storage: { + getItem: (key) => uni.getStorageSync(key), + setItem: (key, value) => uni.setStorageSync(key, value) + }, + paths: ['token', 'userInfo'] + } +}) diff --git a/frontend/src/utils/request.js b/frontend/src/utils/request.js new file mode 100644 index 0000000..b9f7410 --- /dev/null +++ b/frontend/src/utils/request.js @@ -0,0 +1,173 @@ +/** + * HTTP 请求封装模块 + * + * 基于 uni.request 封装统一的请求方法,提供: + * - 基础 URL 配置(区分开发/生产环境) + * - 请求拦截:自动附加 Authorization Header + * - 响应拦截:统一处理错误码(401 自动跳转登录等) + * - Promise 化的请求接口 + * + * 对应后端 API 规范见 docs/api/README.md + */ + +import { getToken, removeToken } from './storage' + +// 基础 URL 配置,根据环境自动切换 +const BASE_URL = process.env.NODE_ENV === 'development' + ? 'http://localhost:8085' + : '' // 生产环境由 Nginx 反向代理,使用相对路径 + +// 请求超时时间(毫秒) +const TIMEOUT = 15000 + +/** + * 统一请求方法 + * + * @param {Object} options - 请求配置 + * @param {string} options.url - 请求路径(不含 BASE_URL,如 /api/v1/auth/login) + * @param {string} [options.method='GET'] - HTTP 方法 + * @param {Object} [options.data] - 请求体数据 + * @param {Object} [options.params] - URL Query 参数 + * @param {Object} [options.header] - 自定义请求头 + * @param {boolean} [options.needAuth=true] - 是否需要自动附加 Token + * @returns {Promise} 响应中的 data 字段(已解包) + */ +const request = (options) => { + return new Promise((resolve, reject) => { + // 构建请求头 + const header = { + 'Content-Type': 'application/json', + ...options.header + } + + // 自动附加 Authorization Header(默认需要认证) + const needAuth = options.needAuth !== false + if (needAuth) { + const token = getToken() + if (token) { + header['Authorization'] = `Bearer ${token}` + } + } + + uni.request({ + url: `${BASE_URL}${options.url}`, + method: options.method || 'GET', + data: options.data, + header, + timeout: TIMEOUT, + success: (res) => { + const { statusCode, data } = res + + // HTTP 状态码 2xx 视为成功 + if (statusCode >= 200 && statusCode < 300) { + // 后端统一响应格式:{ code: 0, message: "success", data: ... } + if (data.code === 0) { + resolve(data) + } else { + // code 非 0 表示业务逻辑错误 + uni.showToast({ + title: data.message || '请求失败', + icon: 'none', + duration: 2000 + }) + reject(data) + } + } else if (statusCode === 401) { + // Token 过期或无效,清除本地 Token 并跳转登录页 + removeToken() + uni.showToast({ + title: '登录已过期,请重新登录', + icon: 'none', + duration: 2000 + }) + // 延迟跳转,让 Toast 有时间显示 + setTimeout(() => { + uni.reLaunch({ url: '/pages/auth/login' }) + }, 1500) + reject({ code: 401, message: '认证已失效' }) + } else if (statusCode === 403) { + uni.showToast({ + title: data.message || '没有访问权限', + icon: 'none', + duration: 2000 + }) + reject(data) + } else { + // 其他 HTTP 错误(400/404/500 等) + uni.showToast({ + title: data.message || `请求错误 (${statusCode})`, + icon: 'none', + duration: 2000 + }) + reject(data) + } + }, + fail: (err) => { + // 网络错误或请求超时 + uni.showToast({ + title: '网络异常,请检查网络连接', + icon: 'none', + duration: 2000 + }) + reject({ code: -1, message: err.errMsg || '网络异常' }) + } + }) + }) +} + +/** + * GET 请求快捷方法 + * + * @param {string} url - 请求路径 + * @param {Object} [data] - Query 参数 + * @param {Object} [options] - 额外配置 + * @returns {Promise} + */ +const get = (url, data, options = {}) => { + return request({ url, method: 'GET', data, ...options }) +} + +/** + * POST 请求快捷方法 + * + * @param {string} url - 请求路径 + * @param {Object} [data] - 请求体数据 + * @param {Object} [options] - 额外配置 + * @returns {Promise} + */ +const post = (url, data, options = {}) => { + return request({ url, method: 'POST', data, ...options }) +} + +/** + * PUT 请求快捷方法 + * + * @param {string} url - 请求路径 + * @param {Object} [data] - 请求体数据 + * @param {Object} [options] - 额外配置 + * @returns {Promise} + */ +const put = (url, data, options = {}) => { + return request({ url, method: 'PUT', data, ...options }) +} + +/** + * DELETE 请求快捷方法 + * + * @param {string} url - 请求路径 + * @param {Object} [data] - 请求体数据 + * @param {Object} [options] - 额外配置 + * @returns {Promise} + */ +const del = (url, data, options = {}) => { + return request({ url, method: 'DELETE', data, ...options }) +} + +export { + request, + get, + post, + put, + del, + BASE_URL +} diff --git a/frontend/src/utils/storage.js b/frontend/src/utils/storage.js new file mode 100644 index 0000000..abc8922 --- /dev/null +++ b/frontend/src/utils/storage.js @@ -0,0 +1,140 @@ +/** + * 本地存储工具模块 + * + * 封装 uni.setStorageSync / uni.getStorageSync,提供: + * - Token 存取(Access Token + Refresh Token) + * - 用户信息缓存 + * - 通用存储操作 + * + * 存储方式: + * - H5 环境:localStorage + * - 小程序/App:uni 内置存储 + * - 均通过 uni.setStorageSync/getStorageSync 统一调用 + */ + +// 存储 Key 常量,集中管理避免硬编码 +const STORAGE_KEYS = { + /** Access Token,用于 API 认证 */ + ACCESS_TOKEN: 'echo_access_token', + /** Refresh Token,用于刷新 Access Token */ + REFRESH_TOKEN: 'echo_refresh_token', + /** Access Token 过期时间戳(毫秒) */ + TOKEN_EXPIRE_TIME: 'echo_token_expire_time', + /** 用户信息缓存 */ + USER_INFO: 'echo_user_info' +} + +// ==================== Token 操作 ==================== + +/** + * 保存 Token 信息(登录/注册成功后调用) + * + * @param {string} accessToken - Access Token + * @param {string} refreshToken - Refresh Token + * @param {number} expiresIn - Access Token 有效期(秒) + */ +const saveToken = (accessToken, refreshToken, expiresIn) => { + uni.setStorageSync(STORAGE_KEYS.ACCESS_TOKEN, accessToken) + uni.setStorageSync(STORAGE_KEYS.REFRESH_TOKEN, refreshToken) + // 计算并存储过期时间戳,用于前端主动判断是否需要刷新 + const expireTime = Date.now() + expiresIn * 1000 + uni.setStorageSync(STORAGE_KEYS.TOKEN_EXPIRE_TIME, expireTime) +} + +/** + * 获取 Access Token + * + * @returns {string|null} Access Token,不存在时返回 null + */ +const getToken = () => { + return uni.getStorageSync(STORAGE_KEYS.ACCESS_TOKEN) || null +} + +/** + * 获取 Refresh Token + * + * @returns {string|null} Refresh Token,不存在时返回 null + */ +const getRefreshToken = () => { + return uni.getStorageSync(STORAGE_KEYS.REFRESH_TOKEN) || null +} + +/** + * 判断 Access Token 是否即将过期(提前 5 分钟视为过期) + * 用于在请求前主动触发 Token 刷新 + * + * @returns {boolean} true 表示已过期或即将过期 + */ +const isTokenExpired = () => { + const expireTime = uni.getStorageSync(STORAGE_KEYS.TOKEN_EXPIRE_TIME) + if (!expireTime) return true + // 提前 5 分钟(300秒)判定为过期,给刷新操作留出时间窗口 + return Date.now() >= expireTime - 300 * 1000 +} + +/** + * 清除所有 Token(登出时调用) + * 同时清除服务端 Redis 中的 Token(通过 logout API),这里只清除客户端缓存 + */ +const removeToken = () => { + uni.removeStorageSync(STORAGE_KEYS.ACCESS_TOKEN) + uni.removeStorageSync(STORAGE_KEYS.REFRESH_TOKEN) + uni.removeStorageSync(STORAGE_KEYS.TOKEN_EXPIRE_TIME) +} + +// ==================== 用户信息操作 ==================== + +/** + * 缓存用户信息 + * + * @param {Object} userInfo - 用户信息对象(与 dto.UserInfo 结构一致) + */ +const saveUserInfo = (userInfo) => { + uni.setStorageSync(STORAGE_KEYS.USER_INFO, JSON.stringify(userInfo)) +} + +/** + * 获取缓存的用户信息 + * + * @returns {Object|null} 用户信息对象,不存在时返回 null + */ +const getUserInfo = () => { + const data = uni.getStorageSync(STORAGE_KEYS.USER_INFO) + if (!data) return null + try { + return JSON.parse(data) + } catch (e) { + return null + } +} + +/** + * 清除用户信息缓存 + */ +const removeUserInfo = () => { + uni.removeStorageSync(STORAGE_KEYS.USER_INFO) +} + +// ==================== 通用存储操作 ==================== + +/** + * 清除所有应用数据(重置应用状态) + * 谨慎使用,会清除所有本地存储 + */ +const clearAll = () => { + removeToken() + removeUserInfo() +} + +export { + STORAGE_KEYS, + saveToken, + getToken, + getRefreshToken, + isTokenExpired, + removeToken, + saveUserInfo, + getUserInfo, + removeUserInfo, + clearAll +}