From 320673dd3426740684199e4557c72af5631dc831 Mon Sep 17 00:00:00 2001 From: liqi Date: Wed, 3 Dec 2025 12:56:57 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96=E9=A1=B9=E7=9B=AE?= =?UTF-8?q?=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 25 + .npmrc | 2 + PROJECT_STATUS.md | 87 + README.md | 74 + index.html | 15 + package.json | 28 + pnpm-lock.yaml | 2121 +++++++++++++++++++++++++ postcss.config.js | 7 + public/vite.svg | 2 + src/App.vue | 8 + src/api/modules/attachment.ts | 37 + src/api/modules/auth.ts | 32 + src/api/modules/contact.ts | 79 + src/api/modules/message.ts | 26 + src/api/modules/room.ts | 22 + src/api/modules/system.ts | 31 + src/api/modules/user.ts | 34 + src/api/request/index.ts | 58 + src/api/request/types.ts | 24 + src/api/websocket/index.ts | 178 +++ src/assets/styles/main.scss | 106 ++ src/components/chat/MessageBubble.vue | 35 + src/components/chat/MessageInput.vue | 181 +++ src/components/chat/MessageList.vue | 61 + src/components/chat/bubble/Audio.vue | 38 + src/components/chat/bubble/File.vue | 48 + src/components/chat/bubble/Image.vue | 29 + src/components/chat/bubble/Text.vue | 47 + src/components/chat/bubble/Video.vue | 24 + src/components/common/Avatar.vue | 41 + src/components/common/ContextMenu.vue | 70 + src/composables/useContextMenu.ts | 47 + src/main.ts | 13 + src/router/index.ts | 47 + src/stores/auth.ts | 101 ++ src/stores/chat.ts | 102 ++ src/stores/contact.ts | 79 + src/stores/contextMenu.ts | 51 + src/types/api.ts | 143 ++ src/types/message.ts | 35 + src/types/user.ts | 20 + src/utils/format.ts | 77 + src/utils/storage.ts | 68 + src/views/chat/ChatView.vue | 502 ++++++ src/views/contact/ContactView.vue | 164 ++ src/views/login/LoginView.vue | 229 +++ src/vite-env.d.ts | 8 + tailwind.config.js | 34 + tsconfig.json | 32 + tsconfig.node.json | 11 + vite.config.ts | 28 + 51 files changed, 5361 insertions(+) create mode 100644 .gitignore create mode 100644 .npmrc create mode 100644 PROJECT_STATUS.md create mode 100644 README.md create mode 100644 index.html create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 postcss.config.js create mode 100644 public/vite.svg create mode 100644 src/App.vue create mode 100644 src/api/modules/attachment.ts create mode 100644 src/api/modules/auth.ts create mode 100644 src/api/modules/contact.ts create mode 100644 src/api/modules/message.ts create mode 100644 src/api/modules/room.ts create mode 100644 src/api/modules/system.ts create mode 100644 src/api/modules/user.ts create mode 100644 src/api/request/index.ts create mode 100644 src/api/request/types.ts create mode 100644 src/api/websocket/index.ts create mode 100644 src/assets/styles/main.scss create mode 100644 src/components/chat/MessageBubble.vue create mode 100644 src/components/chat/MessageInput.vue create mode 100644 src/components/chat/MessageList.vue create mode 100644 src/components/chat/bubble/Audio.vue create mode 100644 src/components/chat/bubble/File.vue create mode 100644 src/components/chat/bubble/Image.vue create mode 100644 src/components/chat/bubble/Text.vue create mode 100644 src/components/chat/bubble/Video.vue create mode 100644 src/components/common/Avatar.vue create mode 100644 src/components/common/ContextMenu.vue create mode 100644 src/composables/useContextMenu.ts create mode 100644 src/main.ts create mode 100644 src/router/index.ts create mode 100644 src/stores/auth.ts create mode 100644 src/stores/chat.ts create mode 100644 src/stores/contact.ts create mode 100644 src/stores/contextMenu.ts create mode 100644 src/types/api.ts create mode 100644 src/types/message.ts create mode 100644 src/types/user.ts create mode 100644 src/utils/format.ts create mode 100644 src/utils/storage.ts create mode 100644 src/views/chat/ChatView.vue create mode 100644 src/views/contact/ContactView.vue create mode 100644 src/views/login/LoginView.vue create mode 100644 src/vite-env.d.ts create mode 100644 tailwind.config.js create mode 100644 tsconfig.json create mode 100644 tsconfig.node.json create mode 100644 vite.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d600b6c --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..8cebe20 --- /dev/null +++ b/.npmrc @@ -0,0 +1,2 @@ +shamefully-hoist=true + diff --git a/PROJECT_STATUS.md b/PROJECT_STATUS.md new file mode 100644 index 0000000..ae46874 --- /dev/null +++ b/PROJECT_STATUS.md @@ -0,0 +1,87 @@ +# 项目完成状态 + +## ✅ 已完成 + +### 1. 项目初始化 +- ✅ Vue 3 + TypeScript项目结构 +- ✅ Vite配置 +- ✅ Tailwind CSS 4配置(暗色主题) +- ✅ TypeScript配置 +- ✅ 路由配置 + +### 2. API层 +- ✅ Axios请求封装(统一响应格式处理) +- ✅ 所有Postman Collection中的API接口已对接: + - ✅ 认证模块(登录、注册、验证码、Token检查) + - ✅ 用户管理(获取信息、列表、创建、更新、删除) + - ✅ 联系人管理(列表、搜索、添加、申请、分组) + - ✅ 房间管理(创建、获取) + - ✅ 消息管理(发送、历史消息) + - ✅ 附件管理(上传、获取、删除) + - ✅ 系统接口(健康检查、ICE服务器、在线状态) + +### 3. WebSocket +- ✅ WebSocket连接管理 +- ✅ 消息接收处理 +- ✅ ClientID管理 +- ✅ 自动重连机制 + +### 4. 状态管理(Pinia) +- ✅ 认证Store +- ✅ 聊天Store +- ✅ 联系人Store +- ✅ 右键菜单Store(单例化) + +### 5. 组件 +- ✅ 右键菜单组件(单例化,确保同时只显示一个) +- ✅ 头像组件 +- ✅ 消息列表组件 +- ✅ 消息气泡组件(文本、图片、语音、视频、文件) +- ✅ 消息输入组件(支持文本、文件、语音录制) + +### 6. 视图页面 +- ✅ 登录/注册页面 +- ✅ 聊天主页面(三栏布局) +- ✅ 联系人页面(搜索、添加好友、申请管理) + +### 7. 工具函数 +- ✅ 存储工具(Token、用户信息管理) +- ✅ 格式化工具(时间、文件大小、URL解析、颜色生成) + +## 🚧 待完善 + +### 1. WebRTC通话功能 +- ⏳ 通话窗口组件(拖拽、最大化、最小化) +- ⏳ WebRTC信令处理 +- ⏳ 音视频流处理 +- ⏳ 通话控制(静音、关闭摄像头) + +### 2. 功能增强 +- ⏳ 文件上传和预览 +- ⏳ 语音消息发送 +- ⏳ 消息状态显示(发送中/已发送/已读) +- ⏳ 消息时间分组显示 +- ⏳ 联系人分组管理UI +- ⏳ 用户资料编辑 + +### 3. UI优化 +- ⏳ Toast提示组件 +- ⏳ 加载状态(骨架屏) +- ⏳ 更多交互动画效果 +- ⏳ 响应式优化 + +## 📝 使用说明 + +1. 安装依赖:`npm install` +2. 启动开发服务器:`npm run dev` +3. 确保后端服务运行在 `http://localhost:12080` +4. 访问 http://localhost:3000 + +## 🔧 技术要点 + +1. **统一响应格式**:所有API返回 `{code, message, result, type, interface_info}`,HTTP状态码统一200 +2. **右键菜单单例化**:使用Pinia store管理,确保同时只显示一个菜单 +3. **WebSocket管理**:自动连接、重连、消息分发 +4. **类型安全**:完整的TypeScript类型定义 +5. **暗色主题**:复刻HTML demo的UI风格 + diff --git a/README.md b/README.md new file mode 100644 index 0000000..87b3a9d --- /dev/null +++ b/README.md @@ -0,0 +1,74 @@ +# NL-IM Vue 3 + TypeScript 前端项目 + +基于Vue 3 + TypeScript构建的即时通讯系统前端应用。 + +## 技术栈 + +- Vue 3.4 + TypeScript +- Pinia (状态管理) +- Vue Router 4 +- Axios (HTTP客户端) +- Tailwind CSS 4 +- WebSocket (实时通信) +- WebRTC (音视频通话) + +## 项目结构 + +``` +nl-im-vue-ts/ +├── src/ +│ ├── api/ # API接口层 +│ ├── components/ # 公共组件 +│ ├── composables/ # 组合式函数 +│ ├── stores/ # Pinia状态管理 +│ ├── types/ # TypeScript类型定义 +│ ├── utils/ # 工具函数 +│ ├── views/ # 页面组件 +│ └── router/ # 路由配置 +``` + +## 开发 + +### 安装依赖 + +```bash +npm install +``` + +### 启动开发服务器 + +```bash +npm run dev +``` + +访问 http://localhost:3000 + +### 构建生产版本 + +```bash +npm run build +``` + +## 后端API + +确保后端服务运行在 `http://localhost:12080` + +WebSocket地址: `ws://localhost:12080/ws` + +## 功能特性 + +- ✅ 用户登录/注册 +- ✅ 联系人管理 +- ✅ 实时消息聊天 +- ✅ 多媒体消息(文本、图片、语音、视频、文件) +- ✅ 语音录制和发送 +- ✅ 右键菜单(单例化) +- ✅ WebRTC音视频通话(待完善) + +## 注意事项 + +1. 需要后端服务运行在 `http://localhost:12080` +2. 登录后Token会自动存储在localStorage +3. WebSocket连接会自动建立和管理 +4. 所有API响应统一格式:`{code, message, result, type, interface_info}` + diff --git a/index.html b/index.html new file mode 100644 index 0000000..31f2297 --- /dev/null +++ b/index.html @@ -0,0 +1,15 @@ + + + + + + + NL-IM 即时通讯系统 + + + + +
+ + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..9b7b0c2 --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "name": "nl-im-vue-ts", + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vue-tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@ant-design/icons-vue": "^7.0.0", + "axios": "^1.6.0", + "pinia": "^2.1.0", + "vue": "^3.4.0", + "vue-router": "^4.2.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.0.0", + "@vueuse/core": "^10.7.0", + "autoprefixer": "^10.4.0", + "postcss": "^8.4.0", + "sass": "^1.94.2", + "tailwindcss": "^3.4.18", + "typescript": "^5.3.0", + "vite": "^5.0.0", + "vue-tsc": "^1.8.0" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..f82d864 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,2121 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@ant-design/icons-vue': + specifier: ^7.0.0 + version: 7.0.1(vue@3.5.25(typescript@5.9.3)) + axios: + specifier: ^1.6.0 + version: 1.13.2 + pinia: + specifier: ^2.1.0 + version: 2.3.1(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3)) + vue: + specifier: ^3.4.0 + version: 3.5.25(typescript@5.9.3) + vue-router: + specifier: ^4.2.0 + version: 4.6.3(vue@3.5.25(typescript@5.9.3)) + devDependencies: + '@vitejs/plugin-vue': + specifier: ^5.0.0 + version: 5.2.4(vite@5.4.21(lightningcss@1.30.2)(sass@1.94.2))(vue@3.5.25(typescript@5.9.3)) + '@vueuse/core': + specifier: ^10.7.0 + version: 10.11.1(vue@3.5.25(typescript@5.9.3)) + autoprefixer: + specifier: ^10.4.0 + version: 10.4.22(postcss@8.5.6) + postcss: + specifier: ^8.4.0 + version: 8.5.6 + sass: + specifier: ^1.94.2 + version: 1.94.2 + tailwindcss: + specifier: ^3.4.18 + version: 3.4.18 + typescript: + specifier: ^5.3.0 + version: 5.9.3 + vite: + specifier: ^5.0.0 + version: 5.4.21(lightningcss@1.30.2)(sass@1.94.2) + vue-tsc: + specifier: ^1.8.0 + version: 1.8.27(typescript@5.9.3) + +packages: + + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + + '@ant-design/colors@6.0.0': + resolution: {integrity: sha512-qAZRvPzfdWHtfameEGP2Qvuf838NhergR35o+EuVyB5XvSA98xod5r4utvi4TJ3ywmevm290g9nsCG5MryrdWQ==} + + '@ant-design/icons-svg@4.4.2': + resolution: {integrity: sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==} + + '@ant-design/icons-vue@7.0.1': + resolution: {integrity: sha512-eCqY2unfZK6Fe02AwFlDHLfoyEFreP6rBwAZMIJ1LugmfMiVgwWDYlp1YsRugaPtICYOabV1iWxXdP12u9U43Q==} + peerDependencies: + vue: '>=3.0.3' + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.28.5': + resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.28.5': + resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} + engines: {node: '>=6.9.0'} + + '@ctrl/tinycolor@3.6.1': + resolution: {integrity: sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==} + engines: {node: '>=10'} + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@parcel/watcher-android-arm64@2.5.1': + resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + + '@parcel/watcher-darwin-arm64@2.5.1': + resolution: {integrity: sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.5.1': + resolution: {integrity: sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.5.1': + resolution: {integrity: sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.5.1': + resolution: {integrity: sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-arm-musl@2.5.1': + resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + libc: [musl] + + '@parcel/watcher-linux-arm64-glibc@2.5.1': + resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-arm64-musl@2.5.1': + resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@parcel/watcher-linux-x64-glibc@2.5.1': + resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-x64-musl@2.5.1': + resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@parcel/watcher-win32-arm64@2.5.1': + resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-ia32@2.5.1': + resolution: {integrity: sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==} + engines: {node: '>= 10.0.0'} + cpu: [ia32] + os: [win32] + + '@parcel/watcher-win32-x64@2.5.1': + resolution: {integrity: sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.5.1': + resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} + engines: {node: '>= 10.0.0'} + + '@rollup/rollup-android-arm-eabi@4.53.3': + resolution: {integrity: sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.53.3': + resolution: {integrity: sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.53.3': + resolution: {integrity: sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.53.3': + resolution: {integrity: sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.53.3': + resolution: {integrity: sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.53.3': + resolution: {integrity: sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.53.3': + resolution: {integrity: sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.53.3': + resolution: {integrity: sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.53.3': + resolution: {integrity: sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.53.3': + resolution: {integrity: sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.53.3': + resolution: {integrity: sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-gnu@4.53.3': + resolution: {integrity: sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-gnu@4.53.3': + resolution: {integrity: sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.53.3': + resolution: {integrity: sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.53.3': + resolution: {integrity: sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.53.3': + resolution: {integrity: sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.53.3': + resolution: {integrity: sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openharmony-arm64@4.53.3': + resolution: {integrity: sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.53.3': + resolution: {integrity: sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.53.3': + resolution: {integrity: sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.53.3': + resolution: {integrity: sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.53.3': + resolution: {integrity: sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==} + cpu: [x64] + os: [win32] + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/web-bluetooth@0.0.20': + resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==} + + '@vitejs/plugin-vue@5.2.4': + resolution: {integrity: sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==} + engines: {node: ^18.0.0 || >=20.0.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 + vue: ^3.2.25 + + '@volar/language-core@1.11.1': + resolution: {integrity: sha512-dOcNn3i9GgZAcJt43wuaEykSluAuOkQgzni1cuxLxTV0nJKanQztp7FxyswdRILaKH+P2XZMPRp2S4MV/pElCw==} + + '@volar/source-map@1.11.1': + resolution: {integrity: sha512-hJnOnwZ4+WT5iupLRnuzbULZ42L7BWWPMmruzwtLhJfpDVoZLjNBxHDi2sY2bgZXCKlpU5XcsMFoYrsQmPhfZg==} + + '@volar/typescript@1.11.1': + resolution: {integrity: sha512-iU+t2mas/4lYierSnoFOeRFQUhAEMgsFuQxoxvwn5EdQopw43j+J27a4lt9LMInx1gLJBC6qL14WYGlgymaSMQ==} + + '@vue/compiler-core@3.5.25': + resolution: {integrity: sha512-vay5/oQJdsNHmliWoZfHPoVZZRmnSWhug0BYT34njkYTPqClh3DNWLkZNJBVSjsNMrg0CCrBfoKkjZQPM/QVUw==} + + '@vue/compiler-dom@3.5.25': + resolution: {integrity: sha512-4We0OAcMZsKgYoGlMjzYvaoErltdFI2/25wqanuTu+S4gismOTRTBPi4IASOjxWdzIwrYSjnqONfKvuqkXzE2Q==} + + '@vue/compiler-sfc@3.5.25': + resolution: {integrity: sha512-PUgKp2rn8fFsI++lF2sO7gwO2d9Yj57Utr5yEsDf3GNaQcowCLKL7sf+LvVFvtJDXUp/03+dC6f2+LCv5aK1ag==} + + '@vue/compiler-ssr@3.5.25': + resolution: {integrity: sha512-ritPSKLBcParnsKYi+GNtbdbrIE1mtuFEJ4U1sWeuOMlIziK5GtOL85t5RhsNy4uWIXPgk+OUdpnXiTdzn8o3A==} + + '@vue/devtools-api@6.6.4': + resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==} + + '@vue/language-core@1.8.27': + resolution: {integrity: sha512-L8Kc27VdQserNaCUNiSFdDl9LWT24ly8Hpwf1ECy3aFb9m6bDhBGQYOujDm21N7EW3moKIOKEanQwe1q5BK+mA==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@vue/reactivity@3.5.25': + resolution: {integrity: sha512-5xfAypCQepv4Jog1U4zn8cZIcbKKFka3AgWHEFQeK65OW+Ys4XybP6z2kKgws4YB43KGpqp5D/K3go2UPPunLA==} + + '@vue/runtime-core@3.5.25': + resolution: {integrity: sha512-Z751v203YWwYzy460bzsYQISDfPjHTl+6Zzwo/a3CsAf+0ccEjQ8c+0CdX1WsumRTHeywvyUFtW6KvNukT/smA==} + + '@vue/runtime-dom@3.5.25': + resolution: {integrity: sha512-a4WrkYFbb19i9pjkz38zJBg8wa/rboNERq3+hRRb0dHiJh13c+6kAbgqCPfMaJ2gg4weWD3APZswASOfmKwamA==} + + '@vue/server-renderer@3.5.25': + resolution: {integrity: sha512-UJaXR54vMG61i8XNIzTSf2Q7MOqZHpp8+x3XLGtE3+fL+nQd+k7O5+X3D/uWrnQXOdMw5VPih+Uremcw+u1woQ==} + peerDependencies: + vue: 3.5.25 + + '@vue/shared@3.5.25': + resolution: {integrity: sha512-AbOPdQQnAnzs58H2FrrDxYj/TJfmeS2jdfEEhgiKINy+bnOANmVizIEgq1r+C5zsbs6l1CCQxtcj71rwNQ4jWg==} + + '@vueuse/core@10.11.1': + resolution: {integrity: sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==} + + '@vueuse/metadata@10.11.1': + resolution: {integrity: sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==} + + '@vueuse/shared@10.11.1': + resolution: {integrity: sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==} + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + autoprefixer@10.4.22: + resolution: {integrity: sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + + axios@1.13.2: + resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + baseline-browser-mapping@2.8.32: + resolution: {integrity: sha512-OPz5aBThlyLFgxyhdwf/s2+8ab3OvT7AdTNvKHBwpXomIYeXqpUUuT8LrdtxZSsWJ4R4CU1un4XGh5Ez3nlTpw==} + hasBin: true + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.28.0: + resolution: {integrity: sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + camelcase-css@2.0.1: + resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} + engines: {node: '>= 6'} + + caniuse-lite@1.0.30001757: + resolution: {integrity: sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ==} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + computeds@0.0.1: + resolution: {integrity: sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + de-indent@1.0.2: + resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + detect-libc@1.0.3: + resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} + engines: {node: '>=0.10'} + hasBin: true + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + didyoumean@1.2.2: + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + + dlv@1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + electron-to-chromium@1.5.263: + resolution: {integrity: sha512-DrqJ11Knd+lo+dv+lltvfMDLU27g14LMdH2b0O3Pio4uk0x+z7OR+JrmyacTPN2M8w3BrZ7/RTwG3R9B7irPlg==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fastq@1.19.1: + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + + fraction.js@5.3.4: + resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + + immutable@5.1.4: + resolution: {integrity: sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + jiti@1.21.7: + resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} + hasBin: true + + lightningcss-android-arm64@1.30.2: + resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.30.2: + resolution: {integrity: sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.30.2: + resolution: {integrity: sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.30.2: + resolution: {integrity: sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.30.2: + resolution: {integrity: sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.30.2: + resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.30.2: + resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.30.2: + resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.30.2: + resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.30.2: + resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.30.2: + resolution: {integrity: sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.30.2: + resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} + engines: {node: '>= 12.0.0'} + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + muggle-string@0.3.1: + resolution: {integrity: sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==} + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + + node-releases@2.0.27: + resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + normalize-range@0.1.2: + resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} + engines: {node: '>=0.10.0'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + + pinia@2.3.1: + resolution: {integrity: sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==} + peerDependencies: + typescript: '>=4.4.4' + vue: ^2.7.0 || ^3.5.11 + peerDependenciesMeta: + typescript: + optional: true + + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + postcss-import@15.1.0: + resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} + engines: {node: '>=14.0.0'} + peerDependencies: + postcss: ^8.0.0 + + postcss-js@4.1.0: + resolution: {integrity: sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==} + engines: {node: ^12 || ^14 || >= 16} + peerDependencies: + postcss: ^8.4.21 + + postcss-load-config@6.0.1: + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + engines: {node: '>= 18'} + peerDependencies: + jiti: '>=1.21.0' + postcss: '>=8.0.9' + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + tsx: + optional: true + yaml: + optional: true + + postcss-nested@6.2.0: + resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.2.14 + + postcss-selector-parser@6.1.2: + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} + engines: {node: '>=4'} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + read-cache@1.0.0: + resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + resolve@1.22.11: + resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} + engines: {node: '>= 0.4'} + hasBin: true + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rollup@4.53.3: + resolution: {integrity: sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + sass@1.94.2: + resolution: {integrity: sha512-N+7WK20/wOr7CzA2snJcUSSNTCzeCGUTFY3OgeQP3mZ1aj9NMQ0mSTXwlrnd89j33zzQJGqIN52GIOmYrfq46A==} + engines: {node: '>=14.0.0'} + hasBin: true + + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + engines: {node: '>=10'} + hasBin: true + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + sucrase@3.35.1: + resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + tailwindcss@3.4.18: + resolution: {integrity: sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==} + engines: {node: '>=14.0.0'} + hasBin: true + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + update-browserslist-db@1.1.4: + resolution: {integrity: sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + vite@5.4.21: + resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vue-demi@0.14.10: + resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==} + engines: {node: '>=12'} + hasBin: true + peerDependencies: + '@vue/composition-api': ^1.0.0-rc.1 + vue: ^3.0.0-0 || ^2.6.0 + peerDependenciesMeta: + '@vue/composition-api': + optional: true + + vue-router@4.6.3: + resolution: {integrity: sha512-ARBedLm9YlbvQomnmq91Os7ck6efydTSpRP3nuOKCvgJOHNrhRoJDSKtee8kcL1Vf7nz6U+PMBL+hTvR3bTVQg==} + peerDependencies: + vue: ^3.5.0 + + vue-template-compiler@2.7.16: + resolution: {integrity: sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==} + + vue-tsc@1.8.27: + resolution: {integrity: sha512-WesKCAZCRAbmmhuGl3+VrdWItEvfoFIPXOvUJkjULi+x+6G/Dy69yO3TBRJDr9eUlmsNAwVmxsNZxvHKzbkKdg==} + hasBin: true + peerDependencies: + typescript: '*' + + vue@3.5.25: + resolution: {integrity: sha512-YLVdgv2K13WJ6n+kD5owehKtEXwdwXuj2TTyJMsO7pSeKw2bfRNZGjhB7YzrpbMYj5b5QsUebHpOqR3R3ziy/g==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + +snapshots: + + '@alloc/quick-lru@5.2.0': {} + + '@ant-design/colors@6.0.0': + dependencies: + '@ctrl/tinycolor': 3.6.1 + + '@ant-design/icons-svg@4.4.2': {} + + '@ant-design/icons-vue@7.0.1(vue@3.5.25(typescript@5.9.3))': + dependencies: + '@ant-design/colors': 6.0.0 + '@ant-design/icons-svg': 4.4.2 + vue: 3.5.25(typescript@5.9.3) + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/parser@7.28.5': + dependencies: + '@babel/types': 7.28.5 + + '@babel/types@7.28.5': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@ctrl/tinycolor@3.6.1': {} + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.19.1 + + '@parcel/watcher-android-arm64@2.5.1': + optional: true + + '@parcel/watcher-darwin-arm64@2.5.1': + optional: true + + '@parcel/watcher-darwin-x64@2.5.1': + optional: true + + '@parcel/watcher-freebsd-x64@2.5.1': + optional: true + + '@parcel/watcher-linux-arm-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-arm-musl@2.5.1': + optional: true + + '@parcel/watcher-linux-arm64-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-arm64-musl@2.5.1': + optional: true + + '@parcel/watcher-linux-x64-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-x64-musl@2.5.1': + optional: true + + '@parcel/watcher-win32-arm64@2.5.1': + optional: true + + '@parcel/watcher-win32-ia32@2.5.1': + optional: true + + '@parcel/watcher-win32-x64@2.5.1': + optional: true + + '@parcel/watcher@2.5.1': + dependencies: + detect-libc: 1.0.3 + is-glob: 4.0.3 + micromatch: 4.0.8 + node-addon-api: 7.1.1 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.5.1 + '@parcel/watcher-darwin-arm64': 2.5.1 + '@parcel/watcher-darwin-x64': 2.5.1 + '@parcel/watcher-freebsd-x64': 2.5.1 + '@parcel/watcher-linux-arm-glibc': 2.5.1 + '@parcel/watcher-linux-arm-musl': 2.5.1 + '@parcel/watcher-linux-arm64-glibc': 2.5.1 + '@parcel/watcher-linux-arm64-musl': 2.5.1 + '@parcel/watcher-linux-x64-glibc': 2.5.1 + '@parcel/watcher-linux-x64-musl': 2.5.1 + '@parcel/watcher-win32-arm64': 2.5.1 + '@parcel/watcher-win32-ia32': 2.5.1 + '@parcel/watcher-win32-x64': 2.5.1 + optional: true + + '@rollup/rollup-android-arm-eabi@4.53.3': + optional: true + + '@rollup/rollup-android-arm64@4.53.3': + optional: true + + '@rollup/rollup-darwin-arm64@4.53.3': + optional: true + + '@rollup/rollup-darwin-x64@4.53.3': + optional: true + + '@rollup/rollup-freebsd-arm64@4.53.3': + optional: true + + '@rollup/rollup-freebsd-x64@4.53.3': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.53.3': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.53.3': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.53.3': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.53.3': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.53.3': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.53.3': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.53.3': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.53.3': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.53.3': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.53.3': + optional: true + + '@rollup/rollup-linux-x64-musl@4.53.3': + optional: true + + '@rollup/rollup-openharmony-arm64@4.53.3': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.53.3': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.53.3': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.53.3': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.53.3': + optional: true + + '@types/estree@1.0.8': {} + + '@types/web-bluetooth@0.0.20': {} + + '@vitejs/plugin-vue@5.2.4(vite@5.4.21(lightningcss@1.30.2)(sass@1.94.2))(vue@3.5.25(typescript@5.9.3))': + dependencies: + vite: 5.4.21(lightningcss@1.30.2)(sass@1.94.2) + vue: 3.5.25(typescript@5.9.3) + + '@volar/language-core@1.11.1': + dependencies: + '@volar/source-map': 1.11.1 + + '@volar/source-map@1.11.1': + dependencies: + muggle-string: 0.3.1 + + '@volar/typescript@1.11.1': + dependencies: + '@volar/language-core': 1.11.1 + path-browserify: 1.0.1 + + '@vue/compiler-core@3.5.25': + dependencies: + '@babel/parser': 7.28.5 + '@vue/shared': 3.5.25 + entities: 4.5.0 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.25': + dependencies: + '@vue/compiler-core': 3.5.25 + '@vue/shared': 3.5.25 + + '@vue/compiler-sfc@3.5.25': + dependencies: + '@babel/parser': 7.28.5 + '@vue/compiler-core': 3.5.25 + '@vue/compiler-dom': 3.5.25 + '@vue/compiler-ssr': 3.5.25 + '@vue/shared': 3.5.25 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.6 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.25': + dependencies: + '@vue/compiler-dom': 3.5.25 + '@vue/shared': 3.5.25 + + '@vue/devtools-api@6.6.4': {} + + '@vue/language-core@1.8.27(typescript@5.9.3)': + dependencies: + '@volar/language-core': 1.11.1 + '@volar/source-map': 1.11.1 + '@vue/compiler-dom': 3.5.25 + '@vue/shared': 3.5.25 + computeds: 0.0.1 + minimatch: 9.0.5 + muggle-string: 0.3.1 + path-browserify: 1.0.1 + vue-template-compiler: 2.7.16 + optionalDependencies: + typescript: 5.9.3 + + '@vue/reactivity@3.5.25': + dependencies: + '@vue/shared': 3.5.25 + + '@vue/runtime-core@3.5.25': + dependencies: + '@vue/reactivity': 3.5.25 + '@vue/shared': 3.5.25 + + '@vue/runtime-dom@3.5.25': + dependencies: + '@vue/reactivity': 3.5.25 + '@vue/runtime-core': 3.5.25 + '@vue/shared': 3.5.25 + csstype: 3.2.3 + + '@vue/server-renderer@3.5.25(vue@3.5.25(typescript@5.9.3))': + dependencies: + '@vue/compiler-ssr': 3.5.25 + '@vue/shared': 3.5.25 + vue: 3.5.25(typescript@5.9.3) + + '@vue/shared@3.5.25': {} + + '@vueuse/core@10.11.1(vue@3.5.25(typescript@5.9.3))': + dependencies: + '@types/web-bluetooth': 0.0.20 + '@vueuse/metadata': 10.11.1 + '@vueuse/shared': 10.11.1(vue@3.5.25(typescript@5.9.3)) + vue-demi: 0.14.10(vue@3.5.25(typescript@5.9.3)) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + + '@vueuse/metadata@10.11.1': {} + + '@vueuse/shared@10.11.1(vue@3.5.25(typescript@5.9.3))': + dependencies: + vue-demi: 0.14.10(vue@3.5.25(typescript@5.9.3)) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + + any-promise@1.3.0: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + arg@5.0.2: {} + + asynckit@0.4.0: {} + + autoprefixer@10.4.22(postcss@8.5.6): + dependencies: + browserslist: 4.28.0 + caniuse-lite: 1.0.30001757 + fraction.js: 5.3.4 + normalize-range: 0.1.2 + picocolors: 1.1.1 + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + axios@1.13.2: + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.5 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + + balanced-match@1.0.2: {} + + baseline-browser-mapping@2.8.32: {} + + binary-extensions@2.3.0: {} + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.28.0: + dependencies: + baseline-browser-mapping: 2.8.32 + caniuse-lite: 1.0.30001757 + electron-to-chromium: 1.5.263 + node-releases: 2.0.27 + update-browserslist-db: 1.1.4(browserslist@4.28.0) + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + camelcase-css@2.0.1: {} + + caniuse-lite@1.0.30001757: {} + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + commander@4.1.1: {} + + computeds@0.0.1: {} + + cssesc@3.0.0: {} + + csstype@3.2.3: {} + + de-indent@1.0.2: {} + + delayed-stream@1.0.0: {} + + detect-libc@1.0.3: + optional: true + + detect-libc@2.1.2: + optional: true + + didyoumean@1.2.2: {} + + dlv@1.1.3: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + electron-to-chromium@1.5.263: {} + + entities@4.5.0: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + escalade@3.2.0: {} + + estree-walker@2.0.2: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fastq@1.19.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + follow-redirects@1.15.11: {} + + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + + fraction.js@5.3.4: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + get-intrinsic@1.3.0: + 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 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + gopd@1.2.0: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + he@1.2.0: {} + + immutable@5.1.4: {} + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + jiti@1.21.7: {} + + lightningcss-android-arm64@1.30.2: + optional: true + + lightningcss-darwin-arm64@1.30.2: + optional: true + + lightningcss-darwin-x64@1.30.2: + optional: true + + lightningcss-freebsd-x64@1.30.2: + optional: true + + lightningcss-linux-arm-gnueabihf@1.30.2: + optional: true + + lightningcss-linux-arm64-gnu@1.30.2: + optional: true + + lightningcss-linux-arm64-musl@1.30.2: + optional: true + + lightningcss-linux-x64-gnu@1.30.2: + optional: true + + lightningcss-linux-x64-musl@1.30.2: + optional: true + + lightningcss-win32-arm64-msvc@1.30.2: + optional: true + + lightningcss-win32-x64-msvc@1.30.2: + optional: true + + lightningcss@1.30.2: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.30.2 + lightningcss-darwin-arm64: 1.30.2 + lightningcss-darwin-x64: 1.30.2 + lightningcss-freebsd-x64: 1.30.2 + lightningcss-linux-arm-gnueabihf: 1.30.2 + lightningcss-linux-arm64-gnu: 1.30.2 + lightningcss-linux-arm64-musl: 1.30.2 + lightningcss-linux-x64-gnu: 1.30.2 + lightningcss-linux-x64-musl: 1.30.2 + lightningcss-win32-arm64-msvc: 1.30.2 + lightningcss-win32-x64-msvc: 1.30.2 + optional: true + + lilconfig@3.1.3: {} + + lines-and-columns@1.2.4: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + math-intrinsics@1.1.0: {} + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.2 + + muggle-string@0.3.1: {} + + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + nanoid@3.3.11: {} + + node-addon-api@7.1.1: + optional: true + + node-releases@2.0.27: {} + + normalize-path@3.0.0: {} + + normalize-range@0.1.2: {} + + object-assign@4.1.1: {} + + object-hash@3.0.0: {} + + path-browserify@1.0.1: {} + + path-parse@1.0.7: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.3: {} + + pify@2.3.0: {} + + pinia@2.3.1(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3)): + dependencies: + '@vue/devtools-api': 6.6.4 + vue: 3.5.25(typescript@5.9.3) + vue-demi: 0.14.10(vue@3.5.25(typescript@5.9.3)) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - '@vue/composition-api' + + pirates@4.0.7: {} + + postcss-import@15.1.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + read-cache: 1.0.0 + resolve: 1.22.11 + + postcss-js@4.1.0(postcss@8.5.6): + dependencies: + camelcase-css: 2.0.1 + postcss: 8.5.6 + + postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.6): + dependencies: + lilconfig: 3.1.3 + optionalDependencies: + jiti: 1.21.7 + postcss: 8.5.6 + + postcss-nested@6.2.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 6.1.2 + + postcss-selector-parser@6.1.2: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-value-parser@4.2.0: {} + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + proxy-from-env@1.1.0: {} + + queue-microtask@1.2.3: {} + + read-cache@1.0.0: + dependencies: + pify: 2.3.0 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + readdirp@4.1.2: {} + + resolve@1.22.11: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + reusify@1.1.0: {} + + rollup@4.53.3: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.53.3 + '@rollup/rollup-android-arm64': 4.53.3 + '@rollup/rollup-darwin-arm64': 4.53.3 + '@rollup/rollup-darwin-x64': 4.53.3 + '@rollup/rollup-freebsd-arm64': 4.53.3 + '@rollup/rollup-freebsd-x64': 4.53.3 + '@rollup/rollup-linux-arm-gnueabihf': 4.53.3 + '@rollup/rollup-linux-arm-musleabihf': 4.53.3 + '@rollup/rollup-linux-arm64-gnu': 4.53.3 + '@rollup/rollup-linux-arm64-musl': 4.53.3 + '@rollup/rollup-linux-loong64-gnu': 4.53.3 + '@rollup/rollup-linux-ppc64-gnu': 4.53.3 + '@rollup/rollup-linux-riscv64-gnu': 4.53.3 + '@rollup/rollup-linux-riscv64-musl': 4.53.3 + '@rollup/rollup-linux-s390x-gnu': 4.53.3 + '@rollup/rollup-linux-x64-gnu': 4.53.3 + '@rollup/rollup-linux-x64-musl': 4.53.3 + '@rollup/rollup-openharmony-arm64': 4.53.3 + '@rollup/rollup-win32-arm64-msvc': 4.53.3 + '@rollup/rollup-win32-ia32-msvc': 4.53.3 + '@rollup/rollup-win32-x64-gnu': 4.53.3 + '@rollup/rollup-win32-x64-msvc': 4.53.3 + fsevents: 2.3.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + sass@1.94.2: + dependencies: + chokidar: 4.0.3 + immutable: 5.1.4 + source-map-js: 1.2.1 + optionalDependencies: + '@parcel/watcher': 2.5.1 + + semver@7.7.3: {} + + source-map-js@1.2.1: {} + + sucrase@3.35.1: + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + commander: 4.1.1 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.7 + tinyglobby: 0.2.15 + ts-interface-checker: 0.1.13 + + supports-preserve-symlinks-flag@1.0.0: {} + + tailwindcss@3.4.18: + dependencies: + '@alloc/quick-lru': 5.2.0 + arg: 5.0.2 + chokidar: 3.6.0 + didyoumean: 1.2.2 + dlv: 1.1.3 + fast-glob: 3.3.3 + glob-parent: 6.0.2 + is-glob: 4.0.3 + jiti: 1.21.7 + lilconfig: 3.1.3 + micromatch: 4.0.8 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.1.1 + postcss: 8.5.6 + postcss-import: 15.1.0(postcss@8.5.6) + postcss-js: 4.1.0(postcss@8.5.6) + postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.6) + postcss-nested: 6.2.0(postcss@8.5.6) + postcss-selector-parser: 6.1.2 + resolve: 1.22.11 + sucrase: 3.35.1 + transitivePeerDependencies: + - tsx + - yaml + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + ts-interface-checker@0.1.13: {} + + typescript@5.9.3: {} + + update-browserslist-db@1.1.4(browserslist@4.28.0): + dependencies: + browserslist: 4.28.0 + escalade: 3.2.0 + picocolors: 1.1.1 + + util-deprecate@1.0.2: {} + + vite@5.4.21(lightningcss@1.30.2)(sass@1.94.2): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.6 + rollup: 4.53.3 + optionalDependencies: + fsevents: 2.3.3 + lightningcss: 1.30.2 + sass: 1.94.2 + + vue-demi@0.14.10(vue@3.5.25(typescript@5.9.3)): + dependencies: + vue: 3.5.25(typescript@5.9.3) + + vue-router@4.6.3(vue@3.5.25(typescript@5.9.3)): + dependencies: + '@vue/devtools-api': 6.6.4 + vue: 3.5.25(typescript@5.9.3) + + vue-template-compiler@2.7.16: + dependencies: + de-indent: 1.0.2 + he: 1.2.0 + + vue-tsc@1.8.27(typescript@5.9.3): + dependencies: + '@volar/typescript': 1.11.1 + '@vue/language-core': 1.8.27(typescript@5.9.3) + semver: 7.7.3 + typescript: 5.9.3 + + vue@3.5.25(typescript@5.9.3): + dependencies: + '@vue/compiler-dom': 3.5.25 + '@vue/compiler-sfc': 3.5.25 + '@vue/runtime-dom': 3.5.25 + '@vue/server-renderer': 3.5.25(vue@3.5.25(typescript@5.9.3)) + '@vue/shared': 3.5.25 + optionalDependencies: + typescript: 5.9.3 diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..b4a6220 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,7 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} + diff --git a/public/vite.svg b/public/vite.svg new file mode 100644 index 0000000..119d59b --- /dev/null +++ b/public/vite.svg @@ -0,0 +1,2 @@ + + diff --git a/src/App.vue b/src/App.vue new file mode 100644 index 0000000..f88682b --- /dev/null +++ b/src/App.vue @@ -0,0 +1,8 @@ + + + + diff --git a/src/api/modules/attachment.ts b/src/api/modules/attachment.ts new file mode 100644 index 0000000..407aec9 --- /dev/null +++ b/src/api/modules/attachment.ts @@ -0,0 +1,37 @@ +import request from '../request' +import type { Attachment, PaginatedResponse } from '@/types/api' + +/** + * 附件管理相关API + */ + +// 上传附件 +export function uploadAttachment(file: File, type: 'image' | 'video' | 'file') { + const formData = new FormData() + formData.append('file', file) + formData.append('type', type) + + return request.post('/attachments/upload', formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }) +} + +// 获取附件信息 +export function getAttachment(id: number) { + return request.get(`/attachments/${id}`) +} + +// 获取附件列表 +export function getAttachments(type?: string, page = 1, pageSize = 20) { + return request.get>('/attachments', { + params: { type, page, page_size: pageSize }, + }) +} + +// 删除附件 +export function deleteAttachment(id: number) { + return request.post(`/attachments/delete/${id}`) +} + diff --git a/src/api/modules/auth.ts b/src/api/modules/auth.ts new file mode 100644 index 0000000..05534f3 --- /dev/null +++ b/src/api/modules/auth.ts @@ -0,0 +1,32 @@ +import request from '../request' +import type { LoginRequest, LoginResponse, RegisterRequest, SendCodeRequest } from '@/types/api' + +/** + * 认证相关API + */ + +// 用户登录 +export function login(data: LoginRequest) { + return request.post('/login', data) +} + +// 用户注册 +export function register(data: RegisterRequest) { + return request.post('/register', data) +} + +// 发送邮箱验证码 +export function sendEmailCode(data: SendCodeRequest) { + return request.post('/send-email-code', data) +} + +// 发送短信验证码 +export function sendSmsCode(data: SendCodeRequest) { + return request.post('/send-sms-code', data) +} + +// 检查Token有效性 +export function checkToken() { + return request.get('/check-token') +} + diff --git a/src/api/modules/contact.ts b/src/api/modules/contact.ts new file mode 100644 index 0000000..07b955b --- /dev/null +++ b/src/api/modules/contact.ts @@ -0,0 +1,79 @@ +import request from '../request' +import type { Contact, User, FriendRequest, ContactGroup, PaginatedResponse } from '@/types/api' + +/** + * 联系人管理相关API + */ + +// 获取联系人列表 +export function getContacts() { + return request.get('/contacts') +} + +// 搜索用户 +export function searchUsers(keyword: string, limit = 20) { + return request.get('/contacts/search', { + params: { keyword, limit }, + }) +} + +// 添加好友(发送申请) +export function addFriend(data: { to_user_id: string; message?: string }) { + return request.post('/contacts/add-friend', data) +} + +// 获取好友申请列表 +export function getFriendRequests() { + return request.get('/contacts/friend-requests') +} + +// 接受好友申请 +export function acceptFriendRequest(requestId: number) { + return request.post('/contacts/accept-request', { request_id: requestId }) +} + +// 拒绝好友申请 +export function rejectFriendRequest(requestId: number) { + return request.post('/contacts/reject-request', { request_id: requestId }) +} + +// 获取分组列表 +export function getGroups() { + return request.get('/contacts/groups') +} + +// 创建分组 +export function createGroup(data: { group_name: string }) { + return request.post('/contacts/groups', data) +} + +// 更新分组 +export function updateGroup(id: number, data: { group_name?: string; sort_order?: number }) { + return request.post(`/contacts/groups/update/${id}`, data) +} + +// 删除分组 +export function deleteGroup(id: number) { + return request.post(`/contacts/groups/delete/${id}`) +} + +// 获取好友详情 +export function getContactDetail(id: string) { + return request.get(`/contacts/${id}`) +} + +// 更新好友信息 +export function updateContact(id: string, data: { + remark_name?: string + group_id?: number + is_top?: boolean + is_muted?: boolean +}) { + return request.post(`/contacts/update/${id}`, data) +} + +// 删除好友 +export function deleteContact(id: string) { + return request.post(`/contacts/delete/${id}`) +} + diff --git a/src/api/modules/message.ts b/src/api/modules/message.ts new file mode 100644 index 0000000..4020a69 --- /dev/null +++ b/src/api/modules/message.ts @@ -0,0 +1,26 @@ +import request from '../request' +import type { ChatMessage, SendMessageRequest, PaginatedResponse } from '@/types/api' + +/** + * 消息管理相关API + */ + +// 发送消息 +export function sendMessage(data: SendMessageRequest) { + return request.post('/send', data) +} + +// 获取历史消息 +export function getMessages(roomId: string, page = 1, pageSize = 50) { + return request.get>('/messages', { + params: { room_id: roomId, page, page_size: pageSize }, + }) +} + +// 同步消息(兼容接口) +export function syncMessages(roomId: string, page = 1, pageSize = 50) { + return request.get>('/messages/sync', { + params: { room_id: roomId, page, page_size: pageSize }, + }) +} + diff --git a/src/api/modules/room.ts b/src/api/modules/room.ts new file mode 100644 index 0000000..b65f0ab --- /dev/null +++ b/src/api/modules/room.ts @@ -0,0 +1,22 @@ +import request from '../request' +import type { Room } from '@/types/api' + +/** + * 房间管理相关API + */ + +// 创建房间 +export function createRoom(data: { + room_type: 'p2p' | 'group' + members: string[] + name?: string + avatar?: string +}) { + return request.post('/rooms', data) +} + +// 获取房间信息 +export function getRoom(id: string) { + return request.get(`/rooms/${id}`) +} + diff --git a/src/api/modules/system.ts b/src/api/modules/system.ts new file mode 100644 index 0000000..a38d0b8 --- /dev/null +++ b/src/api/modules/system.ts @@ -0,0 +1,31 @@ +import request from '../request' +import type { ICEServerConfig } from '@/types/api' + +/** + * 系统接口相关API + */ + +// 健康检查 +export function healthCheck() { + return request.get<{ status: string; node: string; time: string }>('/health') +} + +// 获取ICE服务器配置 +export function getIceServers(userId: string) { + return request.get('/ice-servers', { + params: { user_id: userId }, + }) +} + +// 检查用户在线状态 +export function checkUserOnline(userId: string) { + return request.get<{ is_online: boolean }>('/check-user-online', { + params: { user_id: userId }, + }) +} + +// 绑定ClientID和UserID +export function bindClient(data: { user_id: string; client_id: string }) { + return request.post('/bind', data) +} + diff --git a/src/api/modules/user.ts b/src/api/modules/user.ts new file mode 100644 index 0000000..c750d3e --- /dev/null +++ b/src/api/modules/user.ts @@ -0,0 +1,34 @@ +import request from '../request' +import type { User, PaginatedResponse } from '@/types/api' + +/** + * 用户管理相关API + */ + +// 获取当前用户信息 +export function getMyInfo() { + return request.get('/user/my-info') +} + +// 获取用户列表 +export function getUserList(page = 1, pageSize = 20) { + return request.get>('/user/list', { + params: { page, page_size: pageSize }, + }) +} + +// 创建用户(管理员功能) +export function createUser(data: Partial) { + return request.post('/user/create', data) +} + +// 更新用户信息 +export function updateUser(data: { id: string; updates: Partial }) { + return request.post('/user/update', data) +} + +// 删除用户(管理员功能) +export function deleteUser(id: string) { + return request.post('/user/delete', { id }) +} + diff --git a/src/api/request/index.ts b/src/api/request/index.ts new file mode 100644 index 0000000..80ff2a1 --- /dev/null +++ b/src/api/request/index.ts @@ -0,0 +1,58 @@ +import axios, { type AxiosInstance, type AxiosRequestConfig, type AxiosResponse } from 'axios' +import type { ApiResponse } from './types' + +// 创建axios实例 +const request: AxiosInstance = axios.create({ + baseURL: '/api', + timeout: 30000, + headers: { + 'Content-Type': 'application/json', + }, +}) + +// 请求拦截器 +request.interceptors.request.use( + (config) => { + // 从localStorage获取token + const token = localStorage.getItem('token') + if (token) { + config.headers.Authorization = `Bearer ${token}` + } + + // 从localStorage获取user_id,用于需要X-User-ID header的接口 + const userId = localStorage.getItem('user_id') + if (userId) { + config.headers['X-User-ID'] = userId + } + + return config + }, + (error) => { + return Promise.reject(error) + } +) + +// 响应拦截器 +request.interceptors.response.use( + (response: AxiosResponse) => { + const res = response.data + + // 统一响应格式处理 + // 后端所有接口返回HTTP 200,错误通过code字段标识 + if (res.code !== 0) { + // 错误处理 + console.error('API Error:', res.message) + return Promise.reject(new Error(res.message || '请求失败')) + } + + // 返回result字段的数据 + return res.result + }, + (error) => { + console.error('Request Error:', error) + return Promise.reject(error) + } +) + +export default request + diff --git a/src/api/request/types.ts b/src/api/request/types.ts new file mode 100644 index 0000000..532fae2 --- /dev/null +++ b/src/api/request/types.ts @@ -0,0 +1,24 @@ +/** + * 统一API响应格式 + */ +export interface ApiResponse { + code: number + message: string + result: T + type: 'success' | 'error' + interface_info: { + result_time: string + ecs: string + } +} + +/** + * 分页响应格式 + */ +export interface PaginatedResponse { + data: T[] + total: number + page: number + size: number +} + diff --git a/src/api/websocket/index.ts b/src/api/websocket/index.ts new file mode 100644 index 0000000..5653f29 --- /dev/null +++ b/src/api/websocket/index.ts @@ -0,0 +1,178 @@ +import type { ChatMessage } from '@/types/api' +import type { MessageType, CallStatus } from '@/types/message' + +/** + * WebSocket连接管理 + */ + +export interface WebSocketMessage { + request_type?: string + clientId?: string + data?: ChatMessage +} + +export type MessageHandler = (message: ChatMessage) => void +export type SignalHandler = (message: ChatMessage) => void + +class WebSocketManager { + private ws: WebSocket | null = null + private clientId: string | null = null + private userId: string | null = null + private messageHandlers: MessageHandler[] = [] + private signalHandlers: SignalHandler[] = [] + private reconnectAttempts = 0 + private maxReconnectAttempts = 5 + private reconnectDelay = 3000 + + /** + * 连接WebSocket + */ + connect(userId: string): Promise { + return new Promise((resolve, reject) => { + if (this.ws?.readyState === WebSocket.OPEN) { + resolve() + return + } + + this.userId = userId + const wsUrl = `ws://localhost:12080/ws?user_id=${userId}` + + try { + this.ws = new WebSocket(wsUrl) + + this.ws.onopen = () => { + console.log('WebSocket connected') + this.reconnectAttempts = 0 + resolve() + } + + this.ws.onmessage = (event) => { + try { + const payload: WebSocketMessage = JSON.parse(event.data) + + // 接收clientId + if (payload.clientId) { + this.clientId = payload.clientId + console.log('Received clientId:', this.clientId) + } + + // 处理接收消息 + if (payload.request_type === 'receive_message' && payload.data) { + this.handleMessage(payload.data) + } + } catch (error) { + console.error('WebSocket message parse error:', error) + } + } + + this.ws.onerror = (error) => { + console.error('WebSocket error:', error) + reject(error) + } + + this.ws.onclose = () => { + console.log('WebSocket closed') + this.ws = null + this.attemptReconnect() + } + } catch (error) { + reject(error) + } + }) + } + + /** + * 断开连接 + */ + disconnect() { + if (this.ws) { + this.ws.close() + this.ws = null + } + this.clientId = null + this.userId = null + this.messageHandlers = [] + this.signalHandlers = [] + } + + /** + * 获取ClientID + */ + getClientId(): string | null { + return this.clientId + } + + /** + * 添加消息处理器 + */ + onMessage(handler: MessageHandler) { + this.messageHandlers.push(handler) + } + + /** + * 移除消息处理器 + */ + offMessage(handler: MessageHandler) { + const index = this.messageHandlers.indexOf(handler) + if (index > -1) { + this.messageHandlers.splice(index, 1) + } + } + + /** + * 添加信令处理器 + */ + onSignal(handler: SignalHandler) { + this.signalHandlers.push(handler) + } + + /** + * 移除信令处理器 + */ + offSignal(handler: SignalHandler) { + const index = this.signalHandlers.indexOf(handler) + if (index > -1) { + this.signalHandlers.splice(index, 1) + } + } + + /** + * 处理接收到的消息 + */ + private handleMessage(message: ChatMessage) { + // 信令消息(message_type = 6) + if (message.message_type === 6) { + this.signalHandlers.forEach(handler => handler(message)) + } else { + // 普通消息 + this.messageHandlers.forEach(handler => handler(message)) + } + } + + /** + * 尝试重连 + */ + private attemptReconnect() { + if (this.reconnectAttempts >= this.maxReconnectAttempts) { + console.error('Max reconnect attempts reached') + return + } + + if (!this.userId) { + return + } + + this.reconnectAttempts++ + console.log(`Attempting to reconnect (${this.reconnectAttempts}/${this.maxReconnectAttempts})...`) + + setTimeout(() => { + if (this.userId) { + this.connect(this.userId).catch(console.error) + } + }, this.reconnectDelay) + } +} + +// 单例 +export const wsManager = new WebSocketManager() + diff --git a/src/assets/styles/main.scss b/src/assets/styles/main.scss new file mode 100644 index 0000000..f168068 --- /dev/null +++ b/src/assets/styles/main.scss @@ -0,0 +1,106 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + background-color: #0f172a; + color: #f8fafc; + font-family: 'Segoe UI', system-ui, sans-serif; + overflow: hidden; +} + +/* 滚动条美化 */ +::-webkit-scrollbar { + width: 6px; +} + +::-webkit-scrollbar-thumb { + background: #475569; + border-radius: 3px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +/* 气泡尖角 */ +.bubble-self { + border-top-right-radius: 2px; +} + +.bubble-other { + border-top-left-radius: 2px; +} + +/* 右键菜单 */ +.context-menu { + position: fixed; + z-index: 9999; + background: #334155; + border: 1px solid #475569; + border-radius: 8px; + padding: 6px 0; + min-width: 140px; + box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.5); +} + +.menu-item { + padding: 8px 16px; + font-size: 13px; + cursor: pointer; + display: flex; + align-items: center; + gap: 10px; + color: #e2e8f0; + transition: 0.2s; +} + +.menu-item:hover { + background: #4f46e5; + color: white; +} + +.menu-item.danger:hover { + background: #ef4444; +} + +/* 移动端过渡 */ +.slide-enter-active, +.slide-leave-active { + transition: transform 0.3s ease; +} + +.slide-enter-from, +.slide-leave-to { + transform: translateX(100%); +} + +/* 呼叫动画 */ +.calling-avatar-anim { + animation: pulse-ring 2s infinite; +} + +@keyframes pulse-ring { + 0% { + box-shadow: 0 0 0 0 rgba(99, 102, 241, 0.7); + } + 70% { + box-shadow: 0 0 0 20px rgba(99, 102, 241, 0); + } + 100% { + box-shadow: 0 0 0 0 rgba(99, 102, 241, 0); + } +} + +/* 录音按钮动画 */ +.recording-btn { + transform: scale(1.2); + box-shadow: 0 0 0 10px rgba(239, 68, 68, 0.3); +} + diff --git a/src/components/chat/MessageBubble.vue b/src/components/chat/MessageBubble.vue new file mode 100644 index 0000000..685fe32 --- /dev/null +++ b/src/components/chat/MessageBubble.vue @@ -0,0 +1,35 @@ + + + + diff --git a/src/components/chat/MessageInput.vue b/src/components/chat/MessageInput.vue new file mode 100644 index 0000000..88afcf3 --- /dev/null +++ b/src/components/chat/MessageInput.vue @@ -0,0 +1,181 @@ + + + + diff --git a/src/components/chat/MessageList.vue b/src/components/chat/MessageList.vue new file mode 100644 index 0000000..8427f69 --- /dev/null +++ b/src/components/chat/MessageList.vue @@ -0,0 +1,61 @@ + + + + diff --git a/src/components/chat/bubble/Audio.vue b/src/components/chat/bubble/Audio.vue new file mode 100644 index 0000000..162bfc6 --- /dev/null +++ b/src/components/chat/bubble/Audio.vue @@ -0,0 +1,38 @@ + + + + diff --git a/src/components/chat/bubble/File.vue b/src/components/chat/bubble/File.vue new file mode 100644 index 0000000..12993c9 --- /dev/null +++ b/src/components/chat/bubble/File.vue @@ -0,0 +1,48 @@ + + + + diff --git a/src/components/chat/bubble/Image.vue b/src/components/chat/bubble/Image.vue new file mode 100644 index 0000000..56b1ca2 --- /dev/null +++ b/src/components/chat/bubble/Image.vue @@ -0,0 +1,29 @@ + + + + diff --git a/src/components/chat/bubble/Text.vue b/src/components/chat/bubble/Text.vue new file mode 100644 index 0000000..f225e4c --- /dev/null +++ b/src/components/chat/bubble/Text.vue @@ -0,0 +1,47 @@ + + + + diff --git a/src/components/chat/bubble/Video.vue b/src/components/chat/bubble/Video.vue new file mode 100644 index 0000000..fe3fb0a --- /dev/null +++ b/src/components/chat/bubble/Video.vue @@ -0,0 +1,24 @@ + + + + diff --git a/src/components/common/Avatar.vue b/src/components/common/Avatar.vue new file mode 100644 index 0000000..b2518e5 --- /dev/null +++ b/src/components/common/Avatar.vue @@ -0,0 +1,41 @@ + + + + diff --git a/src/components/common/ContextMenu.vue b/src/components/common/ContextMenu.vue new file mode 100644 index 0000000..191f017 --- /dev/null +++ b/src/components/common/ContextMenu.vue @@ -0,0 +1,70 @@ + + + + + + diff --git a/src/composables/useContextMenu.ts b/src/composables/useContextMenu.ts new file mode 100644 index 0000000..300b217 --- /dev/null +++ b/src/composables/useContextMenu.ts @@ -0,0 +1,47 @@ +import { onMounted, onUnmounted } from 'vue' +import { useContextMenuStore } from '@/stores/contextMenu' +import type { MenuItem } from '@/stores/contextMenu' + +/** + * 右键菜单Hook + */ +export function useContextMenu() { + const contextMenuStore = useContextMenuStore() + + /** + * 显示右键菜单 + */ + function showContextMenu(event: MouseEvent, menuItems: MenuItem[]) { + event.preventDefault() + event.stopPropagation() + contextMenuStore.showMenu(event, menuItems) + } + + /** + * 点击外部区域关闭菜单 + */ + function handleClickOutside(event: MouseEvent) { + if (contextMenuStore.show) { + const target = event.target as HTMLElement + if (!target.closest('.context-menu')) { + contextMenuStore.hideMenu() + } + } + } + + onMounted(() => { + document.addEventListener('click', handleClickOutside) + document.addEventListener('contextmenu', handleClickOutside) + }) + + onUnmounted(() => { + document.removeEventListener('click', handleClickOutside) + document.removeEventListener('contextmenu', handleClickOutside) + }) + + return { + showContextMenu, + hideContextMenu: contextMenuStore.hideMenu, + } +} + diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..e369193 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,13 @@ +import { createApp } from 'vue' +import { createPinia } from 'pinia' +import App from './App.vue' +import router from './router' +import './assets/styles/main.scss' + +const app = createApp(App) + +app.use(createPinia()) +app.use(router) + +app.mount('#app') + diff --git a/src/router/index.ts b/src/router/index.ts new file mode 100644 index 0000000..02da5cc --- /dev/null +++ b/src/router/index.ts @@ -0,0 +1,47 @@ +import { createRouter, createWebHistory } from 'vue-router' +import type { RouteRecordRaw } from 'vue-router' + +const routes: RouteRecordRaw[] = [ + { + path: '/', + redirect: '/chat', + }, + { + path: '/login', + name: 'Login', + component: () => import('@/views/login/LoginView.vue'), + }, + { + path: '/chat', + name: 'Chat', + component: () => import('@/views/chat/ChatView.vue'), + meta: { requiresAuth: true }, + }, + { + path: '/contact', + name: 'Contact', + component: () => import('@/views/contact/ContactView.vue'), + meta: { requiresAuth: true }, + }, +] + +const router = createRouter({ + history: createWebHistory(), + routes, +}) + +// 路由守卫 +router.beforeEach((to, from, next) => { + const token = localStorage.getItem('token') + + if (to.meta.requiresAuth && !token) { + next('/login') + } else if (to.path === '/login' && token) { + next('/chat') + } else { + next() + } +}) + +export default router + diff --git a/src/stores/auth.ts b/src/stores/auth.ts new file mode 100644 index 0000000..46debad --- /dev/null +++ b/src/stores/auth.ts @@ -0,0 +1,101 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import * as authApi from '@/api/modules/auth' +import * as userApi from '@/api/modules/user' +import { storage } from '@/utils/storage' +import type { User, LoginRequest } from '@/types/api' + +export const useAuthStore = defineStore('auth', () => { + const token = ref(storage.getToken()) + const user = ref(storage.getUserInfo()) + const isAuthenticated = ref(!!token.value) + + /** + * 登录 + */ + async function login(data: LoginRequest) { + try { + const response = await authApi.login(data) + token.value = response.token + user.value = response.user + isAuthenticated.value = true + + // 存储到localStorage + storage.setToken(response.token) + storage.setUserId(response.user.id) + storage.setUserInfo(response.user) + if (data.remember) { + storage.setRemember(true) + } + + return response + } catch (error) { + console.error('Login failed:', error) + throw error + } + } + + /** + * 注册 + */ + async function register(data: any) { + try { + await authApi.register(data) + } catch (error) { + console.error('Register failed:', error) + throw error + } + } + + /** + * 登出 + */ + function logout() { + token.value = null + user.value = null + isAuthenticated.value = false + storage.clear() + } + + /** + * 检查Token并获取用户信息 + */ + async function checkAuth() { + if (!token.value) { + return false + } + + try { + await authApi.checkToken() + // Token有效,获取最新用户信息 + const userInfo = await userApi.getMyInfo() + user.value = userInfo + storage.setUserInfo(userInfo) + return true + } catch (error) { + // Token无效,清除 + logout() + return false + } + } + + /** + * 更新用户信息 + */ + function updateUserInfo(userInfo: User) { + user.value = userInfo + storage.setUserInfo(userInfo) + } + + return { + token, + user, + isAuthenticated, + login, + register, + logout, + checkAuth, + updateUserInfo, + } +}) + diff --git a/src/stores/chat.ts b/src/stores/chat.ts new file mode 100644 index 0000000..dc9237f --- /dev/null +++ b/src/stores/chat.ts @@ -0,0 +1,102 @@ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import type { ChatMessage, Contact } from '@/types/api' +import { generateColor } from '@/utils/format' + +export const useChatStore = defineStore('chat', () => { + const currentTarget = ref(null) + const messages = ref>({}) + const contacts = ref([]) + const totalUnread = computed(() => + contacts.value.reduce((acc, c) => acc + (c.unread || 0), 0) + ) + + /** + * 设置当前聊天对象 + */ + function setCurrentTarget(contact: Contact | null) { + currentTarget.value = contact + if (contact) { + contact.unread = 0 + } + } + + /** + * 添加消息 + */ + function addMessage(roomId: string, message: ChatMessage) { + if (!messages.value[roomId]) { + messages.value[roomId] = [] + } + messages.value[roomId].push(message) + } + + /** + * 获取房间消息 + */ + function getRoomMessages(roomId: string): ChatMessage[] { + return messages.value[roomId] || [] + } + + /** + * 设置房间消息 + */ + function setRoomMessages(roomId: string, msgs: ChatMessage[]) { + messages.value[roomId] = msgs + } + + /** + * 更新联系人列表 + */ + function setContacts(newContacts: Contact[]) { + // 为每个联系人生成颜色 + contacts.value = newContacts.map(contact => ({ + ...contact, + color: contact.color || generateColor(contact.user_id || contact.id), + })) + } + + /** + * 更新联系人最后消息 + */ + function updateContactLastMsg(contactId: string, lastMsg: string, lastTime: number) { + const contact = contacts.value.find(c => c.id === contactId || c.user_id === contactId) + if (contact) { + contact.lastMsg = lastMsg + contact.lastTime = lastTime + } + } + + /** + * 增加联系人未读数 + */ + function incrementUnread(contactId: string) { + const contact = contacts.value.find(c => c.id === contactId || c.user_id === contactId) + if (contact && contact.id !== currentTarget.value?.id) { + contact.unread = (contact.unread || 0) + 1 + } + } + + /** + * 清空房间消息 + */ + function clearRoomMessages(roomId: string) { + messages.value[roomId] = [] + } + + return { + currentTarget, + messages, + contacts, + totalUnread, + setCurrentTarget, + addMessage, + getRoomMessages, + setRoomMessages, + setContacts, + updateContactLastMsg, + incrementUnread, + clearRoomMessages, + } +}) + diff --git a/src/stores/contact.ts b/src/stores/contact.ts new file mode 100644 index 0000000..01d11b8 --- /dev/null +++ b/src/stores/contact.ts @@ -0,0 +1,79 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import type { FriendRequest, ContactGroup } from '@/types/api' + +export const useContactStore = defineStore('contact', () => { + const friendRequests = ref([]) + const groups = ref([]) + + /** + * 设置好友申请列表 + */ + function setFriendRequests(requests: FriendRequest[]) { + friendRequests.value = requests + } + + /** + * 添加好友申请 + */ + function addFriendRequest(request: FriendRequest) { + friendRequests.value.push(request) + } + + /** + * 移除好友申请 + */ + function removeFriendRequest(requestId: number) { + const index = friendRequests.value.findIndex(r => r.id === requestId) + if (index > -1) { + friendRequests.value.splice(index, 1) + } + } + + /** + * 设置分组列表 + */ + function setGroups(newGroups: ContactGroup[]) { + groups.value = newGroups + } + + /** + * 添加分组 + */ + function addGroup(group: ContactGroup) { + groups.value.push(group) + } + + /** + * 更新分组 + */ + function updateGroup(group: ContactGroup) { + const index = groups.value.findIndex(g => g.id === group.id) + if (index > -1) { + groups.value[index] = group + } + } + + /** + * 删除分组 + */ + function removeGroup(groupId: number) { + const index = groups.value.findIndex(g => g.id === groupId) + if (index > -1) { + groups.value.splice(index, 1) + } + } + + return { + friendRequests, + groups, + setFriendRequests, + addFriendRequest, + removeFriendRequest, + setGroups, + addGroup, + updateGroup, + removeGroup, + } +}) + diff --git a/src/stores/contextMenu.ts b/src/stores/contextMenu.ts new file mode 100644 index 0000000..4594986 --- /dev/null +++ b/src/stores/contextMenu.ts @@ -0,0 +1,51 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' + +export interface MenuItem { + label: string + icon?: string + action: () => void + danger?: boolean +} + +export const useContextMenuStore = defineStore('contextMenu', () => { + const show = ref(false) + const x = ref(0) + const y = ref(0) + const items = ref([]) + + /** + * 显示右键菜单 + */ + function showMenu(event: MouseEvent, menuItems: MenuItem[]) { + // 关闭旧菜单 + hideMenu() + + // 计算菜单位置(防止超出屏幕) + const xPos = Math.min(event.clientX, window.innerWidth - 150) + const yPos = Math.min(event.clientY, window.innerHeight - 150) + + x.value = xPos + y.value = yPos + items.value = menuItems + show.value = true + } + + /** + * 隐藏右键菜单 + */ + function hideMenu() { + show.value = false + items.value = [] + } + + return { + show, + x, + y, + items, + showMenu, + hideMenu, + } +}) + diff --git a/src/types/api.ts b/src/types/api.ts new file mode 100644 index 0000000..8b7e519 --- /dev/null +++ b/src/types/api.ts @@ -0,0 +1,143 @@ +/** + * API请求和响应的类型定义 + */ + +// 登录请求 +export interface LoginRequest { + account: string // 邮箱或手机号 + password: string + remember?: boolean +} + +// 登录响应 +export interface LoginResponse { + token: string + user: User +} + +// 注册请求 +export interface RegisterRequest { + email: string + phone: string + password: string + confirm_password: string + code: string + agree_terms: boolean +} + +// 发送验证码请求 +export interface SendCodeRequest { + target: string // 邮箱或手机号 + type: 'email' | 'sms' +} + +// 用户信息 +export interface User { + id: string + email: string + phone: string + name: string + avatar: string + desc: string + region: string + created_at: string + updated_at: string +} + +// 联系人信息 +export interface Contact { + id: string + user_id: string + contact_user_id: string + remark_name?: string + group_id?: number + is_top: boolean + is_muted: boolean + user?: User // 关联的用户信息 + last_msg?: string + last_time?: number + unread?: number + color?: string +} + +// 好友申请 +export interface FriendRequest { + id: number + from_user_id: string + to_user_id: string + message: string + status: 'pending' | 'accepted' | 'rejected' + created_at: string + from_user?: User + to_user?: User +} + +// 分组 +export interface ContactGroup { + id: number + user_id: string + group_name: string + sort_order: number + created_at: string +} + +// 房间 +export interface Room { + id: string + room_type: 'p2p' | 'group' + name?: string + avatar?: string + members: string[] + created_at: string +} + +// 聊天消息 +export interface ChatMessage { + id: number + room_id: string + sender_user_id: string + receiver_user_id?: string + message_type: number // 0:文本 1:图片 2:语音 3:视频 8:文件 6:信令 + content: string + duration: number + extra?: string | Record + created_at: string + isSelf?: boolean // 前端标记 +} + +// 发送消息请求 +export interface SendMessageRequest { + sender_client_id?: string + receiver_user_id?: string + room_id: string + message_type: number + content: string + duration?: number + extra?: string + call_id?: string + call_status?: string +} + +// 附件 +export interface Attachment { + id: number + uploader_id: string + file_name: string + file_type: string + file_size: number + file_path: string + file_url: string + mime_type: string + width?: number + height?: number + duration?: number + created_at: string +} + +// ICE服务器配置 +export interface ICEServerConfig { + urls: string[] + username?: string + credential?: string +} + diff --git a/src/types/message.ts b/src/types/message.ts new file mode 100644 index 0000000..fa4e23a --- /dev/null +++ b/src/types/message.ts @@ -0,0 +1,35 @@ +/** + * 消息相关类型定义 + */ + +export enum MessageType { + TEXT = 0, // 文本 + IMAGE = 1, // 图片 + AUDIO = 2, // 语音 + VIDEO = 3, // 视频 + SIGNAL = 6, // WebRTC信令 + FILE = 8, // 文件 +} + +export enum CallStatus { + INVITE = 'invite', + ACCEPTED = 'accepted', + OFFER = 'offer', + ANSWER = 'answer', + CANDIDATE = 'candidate', + HANGUP = 'hangup', + ENDED = 'ended', + ANSWERED_ELSEWHERE = 'answered_elsewhere', +} + +export interface MessageExtra { + url?: string + name?: string + size?: number + duration?: string + title?: string + description?: string + image?: string + type?: 'video' | 'audio' +} + diff --git a/src/types/user.ts b/src/types/user.ts new file mode 100644 index 0000000..730be05 --- /dev/null +++ b/src/types/user.ts @@ -0,0 +1,20 @@ +/** + * 用户相关类型定义 + */ + +export interface UserInfo { + id: string + email: string + phone: string + name: string + avatar: string + desc: string + region: string + created_at: string + updated_at: string +} + +export interface UserListItem extends UserInfo { + color?: string // 前端生成的颜色 +} + diff --git a/src/utils/format.ts b/src/utils/format.ts new file mode 100644 index 0000000..e7e33c1 --- /dev/null +++ b/src/utils/format.ts @@ -0,0 +1,77 @@ +/** + * 格式化工具函数 + */ + +/** + * 格式化时间 + */ +export function formatTime(timestamp: number | string): string { + const date = new Date(typeof timestamp === 'string' ? timestamp : timestamp) + const now = new Date() + const diff = now.getTime() - date.getTime() + const seconds = Math.floor(diff / 1000) + const minutes = Math.floor(seconds / 60) + const hours = Math.floor(minutes / 60) + const days = Math.floor(hours / 24) + + if (days > 0) { + return date.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric' }) + } else if (hours > 0) { + return `${hours}小时前` + } else if (minutes > 0) { + return `${minutes}分钟前` + } else { + return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }) + } +} + +/** + * 格式化文件大小 + */ +export function formatSize(bytes: number): string { + if (!bytes) return '0 B' + const k = 1024 + const sizes = ['B', 'KB', 'MB', 'GB'] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i] +} + +/** + * 格式化时长(秒转MM:SS) + */ +export function formatDuration(seconds: number): string { + const mins = Math.floor(seconds / 60) + const secs = seconds % 60 + return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}` +} + +/** + * 解析文本中的URL + */ +export function parseText(text: string): string { + if (!text) return ' ' + const urlRegex = /(https?:\/\/[^\s]+)/g + return text.replace(urlRegex, '$1') +} + +/** + * 生成用户头像颜色 + */ +export function generateColor(str: string): string { + const colors = [ + '#3b82f6', // blue + '#ec4899', // pink + '#10b981', // green + '#f59e0b', // amber + '#6366f1', // indigo + '#ef4444', // red + '#8b5cf6', // purple + '#06b6d4', // cyan + ] + let hash = 0 + for (let i = 0; i < str.length; i++) { + hash = str.charCodeAt(i) + ((hash << 5) - hash) + } + return colors[Math.abs(hash) % colors.length] +} + diff --git a/src/utils/storage.ts b/src/utils/storage.ts new file mode 100644 index 0000000..3cbefca --- /dev/null +++ b/src/utils/storage.ts @@ -0,0 +1,68 @@ +/** + * 本地存储工具函数 + */ + +const TOKEN_KEY = 'token' +const USER_ID_KEY = 'user_id' +const USER_INFO_KEY = 'user_info' +const REMEMBER_KEY = 'remember' + +export const storage = { + // Token + setToken(token: string) { + localStorage.setItem(TOKEN_KEY, token) + }, + + getToken(): string | null { + return localStorage.getItem(TOKEN_KEY) + }, + + removeToken() { + localStorage.removeItem(TOKEN_KEY) + }, + + // User ID + setUserId(userId: string) { + localStorage.setItem(USER_ID_KEY, userId) + }, + + getUserId(): string | null { + return localStorage.getItem(USER_ID_KEY) + }, + + removeUserId() { + localStorage.removeItem(USER_ID_KEY) + }, + + // User Info + setUserInfo(userInfo: any) { + localStorage.setItem(USER_INFO_KEY, JSON.stringify(userInfo)) + }, + + getUserInfo(): any | null { + const info = localStorage.getItem(USER_INFO_KEY) + return info ? JSON.parse(info) : null + }, + + removeUserInfo() { + localStorage.removeItem(USER_INFO_KEY) + }, + + // Remember + setRemember(remember: boolean) { + localStorage.setItem(REMEMBER_KEY, String(remember)) + }, + + getRemember(): boolean { + return localStorage.getItem(REMEMBER_KEY) === 'true' + }, + + // Clear all + clear() { + this.removeToken() + this.removeUserId() + this.removeUserInfo() + this.setRemember(false) + }, +} + diff --git a/src/views/chat/ChatView.vue b/src/views/chat/ChatView.vue new file mode 100644 index 0000000..c6e3bbd --- /dev/null +++ b/src/views/chat/ChatView.vue @@ -0,0 +1,502 @@ + + + + diff --git a/src/views/contact/ContactView.vue b/src/views/contact/ContactView.vue new file mode 100644 index 0000000..696af56 --- /dev/null +++ b/src/views/contact/ContactView.vue @@ -0,0 +1,164 @@ + + + + diff --git a/src/views/login/LoginView.vue b/src/views/login/LoginView.vue new file mode 100644 index 0000000..655a6e1 --- /dev/null +++ b/src/views/login/LoginView.vue @@ -0,0 +1,229 @@ + + + + diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..f4d7cda --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1,8 @@ +/// + +declare module '*.vue' { + import type { DefineComponent } from 'vue' + const component: DefineComponent<{}, {}, any> + export default component +} + diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..bdde2a0 --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,34 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: [ + "./index.html", + "./src/**/*.{vue,js,ts,jsx,tsx}", + ], + darkMode: 'class', + theme: { + extend: { + colors: { + primary: '#6366f1', // Indigo + dark: '#0f172a', // Slate 900 + panel: '#1e293b', // Slate 800 + input: '#334155', // Slate 700 + 'msg-self': '#4f46e5', + 'msg-other': '#334155', + danger: '#ef4444', + success: '#22c55e' + }, + animation: { + 'ripple': 'ripple 2s cubic-bezier(0, 0.2, 0.8, 1) infinite', + }, + keyframes: { + ripple: { + '0%': { boxShadow: '0 0 0 0 rgba(99, 102, 241, 0.3)' }, + '70%': { boxShadow: '0 0 0 20px rgba(99, 102, 241, 0)' }, + '100%': { boxShadow: '0 0 0 0 rgba(99, 102, 241, 0)' }, + } + } + }, + }, + plugins: [], +} + diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..0e2bfc7 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + + /* Path alias */ + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + }, + "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], + "references": [{ "path": "./tsconfig.node.json" }] +} + diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..e428d50 --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} + diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..1711e51 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,28 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import { resolve } from 'path' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [vue()], + resolve: { + alias: { + '@': resolve(__dirname, 'src'), + }, + }, + server: { + port: 3000, + proxy: { + '/api': { + target: 'http://localhost:12080', + changeOrigin: true, + }, + '/ws': { + target: 'ws://localhost:12080', + ws: true, + changeOrigin: true, + }, + }, + }, +}) +