commit 29a79b1c6b2f0c8abc1440f6d57a7d42d3cbc389
Author: kjqwer <2990346238@qq.com>
Date: Thu Aug 21 10:43:04 2025 +0800
初始化
diff --git a/.eslintrc.js b/.eslintrc.js
new file mode 100644
index 0000000..f816068
--- /dev/null
+++ b/.eslintrc.js
@@ -0,0 +1,25 @@
+module.exports = {
+ env: {
+ es2021: true,
+ node: true,
+ },
+ extends: ['standard'],
+ parserOptions: {
+ ecmaVersion: 12,
+ sourceType: 'module',
+ },
+ rules: {
+ curly: 'off',
+ camelcase: 'off',
+ 'no-case-declarations': 'off',
+ semi: ['error', 'always'],
+ 'comma-dangle': ['error', 'only-multiline'],
+ 'space-before-function-paren': 'off',
+ 'no-tabs': 'off',
+ indent: 'off',
+ eqeqeq: 'off',
+ 'no-async-promise-executor': 'off',
+ 'no-control-regex': 'off',
+ 'prefer-promise-reject-errors': 'off',
+ },
+};
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..b7494f8
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,66 @@
+/test
+/package-lock.json
+
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# TypeScript v1 declaration files
+typings/
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variables file
+.env
+
+# next.js build output
+.next
+
+old/
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 0000000..317e358
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,9 @@
+{
+ "printWidth": 200,
+ "singleQuote": true,
+ "trailingComma": "es5",
+ "arrowParens": "avoid",
+ "useTabs": false,
+ "tabWidth": 2,
+ "semi": true
+}
diff --git a/backend/README.md b/backend/README.md
new file mode 100644
index 0000000..1243fdd
--- /dev/null
+++ b/backend/README.md
@@ -0,0 +1,217 @@
+# Pixiv 后端服务
+
+这是一个优雅的 Pixiv 后端服务架构,提供作品信息获取、作者信息查询、文件下载等功能。
+
+## 🏗️ 项目架构
+
+```
+backend/
+├── server.js # 主服务器文件
+├── core.js # 核心后端逻辑
+├── auth.js # 认证模块
+├── config.js # 代理配置
+├── start.js # 启动脚本
+├── test-login.js # 登录测试脚本
+├── middleware/ # 中间件
+│ ├── auth.js # 认证中间件
+│ └── errorHandler.js # 错误处理中间件
+├── routes/ # 路由模块
+│ ├── auth.js # 认证路由
+│ ├── artwork.js # 作品路由
+│ ├── artist.js # 作者路由
+│ └── download.js # 下载路由
+├── services/ # 服务层
+│ ├── artwork.js # 作品服务
+│ ├── artist.js # 作者服务
+│ └── download.js # 下载服务
+└── utils/ # 工具类
+ └── response.js # 响应工具
+```
+
+## 🚀 快速开始
+
+### 1. 安装依赖
+
+```bash
+npm install
+```
+
+### 2. 启动服务器
+
+```bash
+# 开发模式
+npm run dev
+
+# 生产模式
+npm start
+
+# 或直接运行
+node backend/start.js
+```
+
+### 3. 测试登录
+
+```bash
+node backend/test-login.js
+```
+
+## 📡 API 接口
+
+### 认证相关
+
+- `GET /api/auth/status` - 获取登录状态
+- `GET /api/auth/login-url` - 获取登录URL
+- `POST /api/auth/callback` - 处理登录回调
+- `POST /api/auth/relogin` - 重新登录
+- `POST /api/auth/logout` - 登出
+
+### 作品相关
+
+- `GET /api/artwork/:id` - 获取作品详情
+- `GET /api/artwork/:id/preview` - 获取作品预览
+- `GET /api/artwork/:id/images` - 获取作品图片URL
+- `GET /api/artwork/search` - 搜索作品
+
+### 作者相关
+
+- `GET /api/artist/:id` - 获取作者信息
+- `GET /api/artist/:id/artworks` - 获取作者作品列表
+- `GET /api/artist/:id/following` - 获取作者关注列表
+- `GET /api/artist/:id/followers` - 获取作者粉丝列表
+- `POST /api/artist/:id/follow` - 关注/取消关注作者
+
+### 下载相关
+
+- `POST /api/download/artwork/:id` - 下载单个作品
+- `POST /api/download/artworks` - 批量下载作品
+- `POST /api/download/artist/:id` - 下载作者作品
+- `GET /api/download/progress/:taskId` - 获取下载进度
+- `DELETE /api/download/cancel/:taskId` - 取消下载任务
+- `GET /api/download/history` - 获取下载历史
+
+## 🔧 配置说明
+
+### 代理配置
+
+在 `config.js` 中配置代理设置:
+
+```javascript
+const proxyConfig = {
+ system: {
+ host: '127.0.0.1',
+ port: 7897,
+ protocol: 'http'
+ }
+};
+```
+
+### 环境变量
+
+- `PORT` - 服务器端口 (默认: 3000)
+- `NODE_ENV` - 运行环境 (development/production)
+- `FRONTEND_URL` - 前端URL (用于CORS)
+
+## 📁 文件结构说明
+
+### 核心模块
+
+- **server.js**: 主服务器类,负责初始化、配置和启动服务器
+- **core.js**: 核心后端逻辑,管理认证状态和配置
+- **auth.js**: 认证模块,处理OAuth2.0登录流程
+
+### 中间件
+
+- **auth.js**: 认证中间件,验证用户登录状态
+- **errorHandler.js**: 全局错误处理中间件
+
+### 路由模块
+
+- **auth.js**: 认证相关路由
+- **artwork.js**: 作品相关路由
+- **artist.js**: 作者相关路由
+- **download.js**: 下载相关路由
+
+### 服务层
+
+- **artwork.js**: 作品服务,处理作品API调用
+- **artist.js**: 作者服务,处理作者API调用
+- **download.js**: 下载服务,处理文件下载
+
+### 工具类
+
+- **response.js**: 统一API响应格式工具
+
+## 🎯 主要功能
+
+### 1. 作品信息获取
+- 获取作品详细信息
+- 获取作品预览信息
+- 获取作品图片URL
+- 搜索作品
+
+### 2. 作者信息查询
+- 获取作者基本信息
+- 获取作者作品列表
+- 获取作者关注/粉丝列表
+- 关注/取消关注作者
+
+### 3. 文件下载
+- 下载单个作品
+- 批量下载作品
+- 下载作者作品
+- 下载进度跟踪
+- 下载历史记录
+
+### 4. 认证管理
+- OAuth2.0 登录流程
+- 自动刷新令牌
+- 登录状态管理
+
+## 🔒 安全特性
+
+- 统一的错误处理
+- 请求参数验证
+- 认证中间件保护
+- CORS 配置
+- 代理支持
+
+## 📊 监控和日志
+
+- 请求日志记录
+- 错误日志记录
+- 健康检查接口
+- 下载进度跟踪
+
+## 🛠️ 开发指南
+
+### 添加新路由
+
+1. 在 `routes/` 目录下创建新的路由文件
+2. 在 `server.js` 中注册路由
+3. 添加相应的中间件保护
+
+### 添加新服务
+
+1. 在 `services/` 目录下创建新的服务类
+2. 实现相应的业务逻辑
+3. 在路由中调用服务
+
+### 添加新中间件
+
+1. 在 `middleware/` 目录下创建新的中间件文件
+2. 在 `server.js` 中注册中间件
+
+## 📝 注意事项
+
+1. 确保代理配置正确
+2. 首次使用需要登录获取访问令牌
+3. 下载功能需要足够的磁盘空间
+4. 建议在生产环境中使用PM2等进程管理器
+
+## 🤝 贡献
+
+欢迎提交 Issue 和 Pull Request!
+
+## �� 许可证
+
+MIT License
\ No newline at end of file
diff --git a/backend/auth.js b/backend/auth.js
new file mode 100644
index 0000000..52fc14c
--- /dev/null
+++ b/backend/auth.js
@@ -0,0 +1,274 @@
+const axios = require('axios');
+const Crypto = require('crypto');
+const { Base64 } = require('js-base64');
+const { stringify } = require('qs');
+const moment = require('moment');
+const { ProxyAgent } = require('proxy-agent');
+
+// OAuth 2.0 配置
+const CLIENT_ID = 'MOBrBDS8blbauoSck0ZfDbtuzpyT';
+const CLIENT_SECRET = 'lsACyCD94FhDUtGTXi3QzcFE2uU1hqtDaKeqrdwj';
+const REDIRECT_URI = 'https://app-api.pixiv.net/web/v1/users/auth/pixiv/callback';
+const LOGIN_URL = 'https://app-api.pixiv.net/web/v1/login';
+const HASH_SECRET = '28c1fdd170a5204386cb1313c7077b34f83e4aaf4aa829ce78c231e05b0bae2c';
+
+class PixivAuth {
+ constructor(proxy = null) {
+ this.accessToken = null;
+ this.refreshToken = null;
+ this.user = null;
+ this.proxy = proxy;
+
+ // 创建 axios 实例,支持代理
+ this.axiosInstance = this.createAxiosInstance();
+ }
+
+ /**
+ * 创建支持代理的 axios 实例
+ */
+ createAxiosInstance() {
+ const config = {
+ timeout: 30000, // 30秒超时
+ headers: this.getDefaultHeaders()
+ };
+
+ // 如果设置了代理,添加代理配置
+ if (this.proxy) {
+ console.log('使用代理:', this.proxy);
+ config.httpsAgent = new ProxyAgent(this.proxy);
+ } else {
+ // 尝试使用系统代理
+ const systemProxy = process.env.HTTP_PROXY || process.env.HTTPS_PROXY || process.env.http_proxy || process.env.https_proxy;
+ if (systemProxy) {
+ console.log('使用系统代理:', systemProxy);
+ config.httpsAgent = new ProxyAgent(systemProxy);
+ }
+ }
+
+ return axios.create(config);
+ }
+
+ /**
+ * 设置代理
+ */
+ setProxy(proxy) {
+ this.proxy = proxy;
+ this.axiosInstance = this.createAxiosInstance();
+ }
+
+ /**
+ * 获取默认头部信息
+ */
+ getDefaultHeaders() {
+ const datetime = moment().format();
+ return {
+ 'App-OS': 'android',
+ 'Accept-Language': 'en-us',
+ 'App-OS-Version': '9.0',
+ 'App-Version': '5.0.234',
+ 'User-Agent': 'PixivAndroidApp/5.0.234 (Android 9.0; Pixel 3)',
+ 'X-Client-Time': datetime,
+ 'X-Client-Hash': Crypto.createHash('md5').update(`${datetime}${HASH_SECRET}`).digest('hex')
+ };
+ }
+
+ /**
+ * 生成 PKCE 参数
+ */
+ generatePKCE() {
+ const codeVerifier = Base64.fromUint8Array(Crypto.randomBytes(32), true);
+ const codeChallenge = Base64.encodeURI(Crypto.createHash('sha256').update(codeVerifier).digest());
+
+ return {
+ code_verifier: codeVerifier,
+ code_challenge: codeChallenge
+ };
+ }
+
+ /**
+ * 获取登录URL
+ */
+ getLoginUrl() {
+ const pkce = this.generatePKCE();
+
+ const params = {
+ code_challenge: pkce.code_challenge,
+ code_challenge_method: 'S256',
+ client: 'pixiv-android'
+ };
+
+ const loginUrl = `${LOGIN_URL}?${stringify(params)}`;
+
+ return {
+ login_url: loginUrl,
+ code_verifier: pkce.code_verifier
+ };
+ }
+
+ /**
+ * 使用授权码获取访问令牌
+ */
+ async getAccessToken(code, codeVerifier) {
+ try {
+ console.log('正在获取访问令牌...');
+ console.log('Code:', code);
+ console.log('Code Verifier:', codeVerifier);
+
+ const data = {
+ client_id: CLIENT_ID,
+ client_secret: CLIENT_SECRET,
+ code: code,
+ code_verifier: codeVerifier,
+ redirect_uri: REDIRECT_URI,
+ grant_type: 'authorization_code',
+ include_policy: true
+ };
+
+ console.log('请求数据:', data);
+
+ const headers = {
+ ...this.getDefaultHeaders(),
+ 'Content-Type': 'application/x-www-form-urlencoded'
+ };
+
+ console.log('请求头部:', headers);
+
+ const response = await this.axiosInstance.post('https://oauth.secure.pixiv.net/auth/token',
+ stringify(data),
+ { headers }
+ );
+
+ console.log('响应状态:', response.status);
+ console.log('响应数据:', JSON.stringify(response.data, null, 2));
+
+ const tokenData = response.data.response;
+
+ this.accessToken = tokenData.access_token;
+ this.refreshToken = tokenData.refresh_token;
+ this.user = tokenData.user;
+
+ console.log('获取访问令牌成功');
+ return {
+ success: true,
+ access_token: tokenData.access_token,
+ refresh_token: tokenData.refresh_token,
+ user: tokenData.user
+ };
+
+ } catch (error) {
+ console.error('获取访问令牌失败:');
+ console.error('错误对象:', error);
+ console.error('响应状态:', error.response?.status);
+ console.error('响应数据:', error.response?.data);
+ console.error('错误消息:', error.message);
+
+ return {
+ success: false,
+ error: error.response?.data || error.message
+ };
+ }
+ }
+
+ /**
+ * 使用刷新令牌更新访问令牌
+ */
+ async refreshAccessToken(refreshToken) {
+ try {
+ console.log('正在刷新访问令牌...');
+
+ const data = {
+ client_id: CLIENT_ID,
+ client_secret: CLIENT_SECRET,
+ get_secure_url: true,
+ include_policy: true,
+ grant_type: 'refresh_token',
+ refresh_token: refreshToken
+ };
+
+ const headers = {
+ ...this.getDefaultHeaders(),
+ 'Content-Type': 'application/x-www-form-urlencoded'
+ };
+
+ const response = await this.axiosInstance.post('https://oauth.secure.pixiv.net/auth/token',
+ stringify(data),
+ { headers }
+ );
+
+ const tokenData = response.data.response;
+
+ this.accessToken = tokenData.access_token;
+ this.refreshToken = tokenData.refresh_token;
+
+ console.log('刷新访问令牌成功');
+ return {
+ success: true,
+ access_token: tokenData.access_token,
+ refresh_token: tokenData.refresh_token
+ };
+
+ } catch (error) {
+ console.error('刷新访问令牌失败:', error.response?.data || error.message);
+ return {
+ success: false,
+ error: error.response?.data || error.message
+ };
+ }
+ }
+
+ /**
+ * 获取用户信息
+ */
+ async getUserInfo() {
+ if (!this.accessToken) {
+ return { success: false, error: '未登录' };
+ }
+
+ try {
+ const headers = {
+ ...this.getDefaultHeaders(),
+ 'Authorization': `Bearer ${this.accessToken}`
+ };
+
+ const response = await this.axiosInstance.get('https://app-api.pixiv.net/v1/user/me', {
+ headers
+ });
+
+ return {
+ success: true,
+ user: response.data.user
+ };
+
+ } catch (error) {
+ console.error('获取用户信息失败:', error.response?.data || error.message);
+ return {
+ success: false,
+ error: error.response?.data || error.message
+ };
+ }
+ }
+
+ /**
+ * 登出
+ */
+ logout() {
+ this.accessToken = null;
+ this.refreshToken = null;
+ this.user = null;
+ console.log('已登出');
+ return { success: true };
+ }
+
+ /**
+ * 获取当前状态
+ */
+ getStatus() {
+ return {
+ isLoggedIn: !!this.accessToken,
+ user: this.user,
+ hasRefreshToken: !!this.refreshToken
+ };
+ }
+}
+
+module.exports = PixivAuth;
\ No newline at end of file
diff --git a/backend/config.js b/backend/config.js
new file mode 100644
index 0000000..d48c2c6
--- /dev/null
+++ b/backend/config.js
@@ -0,0 +1,36 @@
+// 代理配置
+const proxyConfig = {
+ // 系统代理配置
+ system: {
+ host: '127.0.0.1',
+ port: 7897,
+ protocol: 'http'
+ },
+
+ // 代理URL格式
+ get proxyUrl() {
+ return `${this.system.protocol}://${this.system.host}:${this.system.port}`;
+ },
+
+ // 环境变量设置
+ setEnvironmentVariables() {
+ process.env.HTTP_PROXY = this.proxyUrl;
+ process.env.HTTPS_PROXY = this.proxyUrl;
+ process.env.http_proxy = this.proxyUrl;
+ process.env.https_proxy = this.proxyUrl;
+
+ console.log('代理环境变量已设置:', this.proxyUrl);
+ },
+
+ // 清除环境变量
+ clearEnvironmentVariables() {
+ delete process.env.HTTP_PROXY;
+ delete process.env.HTTPS_PROXY;
+ delete process.env.http_proxy;
+ delete process.env.https_proxy;
+
+ console.log('代理环境变量已清除');
+ }
+};
+
+module.exports = proxyConfig;
\ No newline at end of file
diff --git a/backend/core.js b/backend/core.js
new file mode 100644
index 0000000..e79adae
--- /dev/null
+++ b/backend/core.js
@@ -0,0 +1,258 @@
+const Fse = require('fs-extra');
+const Path = require('path');
+const PixivAuth = require('./auth');
+
+// 配置文件路径
+const CONFIG_FILE_DIR = require('appdata-path').getAppDataPath('pxder');
+const CONFIG_FILE = Path.resolve(CONFIG_FILE_DIR, 'config.json');
+
+// 默认配置
+const defaultConfig = {
+ download: {
+ thread: 5,
+ timeout: 30,
+ path: null
+ },
+ refresh_token: null,
+ access_token: null,
+ user: null,
+ proxy: null
+};
+
+class PixivBackend {
+ constructor() {
+ this.config = null;
+ this.auth = null;
+ this.isLoggedIn = false;
+ }
+
+ /**
+ * 初始化后端
+ */
+ async init() {
+ console.log('正在初始化 Pixiv 后端...');
+
+ // 初始化配置
+ this.initConfig();
+ this.config = this.readConfig();
+
+ // 创建认证实例,传入代理配置
+ this.auth = new PixivAuth(this.config.proxy);
+
+ // 检查登录状态
+ if (this.config.refresh_token) {
+ console.log('检测到已保存的登录信息,正在验证...');
+ await this.relogin();
+ } else {
+ console.log('未检测到登录信息,需要先登录');
+ }
+
+ return this;
+ }
+
+ /**
+ * 初始化配置文件
+ */
+ initConfig() {
+ Fse.ensureDirSync(CONFIG_FILE_DIR);
+ if (!Fse.existsSync(CONFIG_FILE)) {
+ Fse.writeJSONSync(CONFIG_FILE, defaultConfig);
+ }
+ }
+
+ /**
+ * 读取配置
+ */
+ readConfig() {
+ try {
+ const config = Fse.readJsonSync(CONFIG_FILE);
+ // 合并默认配置
+ return { ...defaultConfig, ...config };
+ } catch (error) {
+ console.error('读取配置文件失败:', error.message);
+ return { ...defaultConfig };
+ }
+ }
+
+ /**
+ * 保存配置
+ */
+ saveConfig() {
+ try {
+ Fse.writeJsonSync(CONFIG_FILE, this.config);
+ console.log('配置已保存');
+ } catch (error) {
+ console.error('保存配置失败:', error.message);
+ }
+ }
+
+ /**
+ * 获取登录URL
+ */
+ getLoginUrl() {
+ const loginData = this.auth.getLoginUrl();
+ this.config.code_verifier = loginData.code_verifier;
+ this.saveConfig();
+
+ return {
+ login_url: loginData.login_url,
+ code_verifier: loginData.code_verifier
+ };
+ }
+
+ /**
+ * 处理登录回调
+ */
+ async handleLoginCallback(code) {
+ try {
+ console.log('正在处理登录回调...');
+
+ if (!this.config.code_verifier) {
+ throw new Error('缺少 code_verifier,请重新获取登录URL');
+ }
+
+ // 使用新的认证模块进行登录
+ const result = await this.auth.getAccessToken(code, this.config.code_verifier);
+
+ if (result.success) {
+ // 保存登录信息
+ this.config.refresh_token = result.refresh_token;
+ this.config.access_token = result.access_token;
+ this.config.user = result.user;
+
+ // 清理临时数据
+ delete this.config.code_verifier;
+
+ this.saveConfig();
+ this.isLoggedIn = true;
+
+ console.log(`登录成功!用户: ${result.user.account}`);
+ return {
+ success: true,
+ user: result.user
+ };
+ } else {
+ throw new Error(result.error);
+ }
+
+ } catch (error) {
+ console.error('登录失败:', error.message);
+ return {
+ success: false,
+ error: error.message
+ };
+ }
+ }
+
+ /**
+ * 重新登录(使用保存的 refresh_token)
+ */
+ async relogin() {
+ try {
+ if (!this.config.refresh_token) {
+ throw new Error('没有保存的登录信息');
+ }
+
+ console.log('正在使用保存的登录信息重新登录...');
+
+ const result = await this.auth.refreshAccessToken(this.config.refresh_token);
+
+ if (result.success) {
+ // 更新配置
+ this.config.access_token = result.access_token;
+ this.config.refresh_token = result.refresh_token;
+ this.saveConfig();
+
+ this.isLoggedIn = true;
+ console.log('重新登录成功!');
+
+ return { success: true };
+ } else {
+ throw new Error(result.error);
+ }
+
+ } catch (error) {
+ console.error('重新登录失败:', error.message);
+ // 清除无效的登录信息
+ this.config.refresh_token = null;
+ this.config.access_token = null;
+ this.config.user = null;
+ this.saveConfig();
+ this.isLoggedIn = false;
+
+ return {
+ success: false,
+ error: error.message
+ };
+ }
+ }
+
+ /**
+ * 登出
+ */
+ logout() {
+ this.auth.logout();
+ this.config.refresh_token = null;
+ this.config.access_token = null;
+ this.config.user = null;
+ this.isLoggedIn = false;
+
+ this.saveConfig();
+ console.log('已登出');
+
+ return { success: true };
+ }
+
+ /**
+ * 获取登录状态
+ */
+ getLoginStatus() {
+ const status = this.auth.getStatus();
+ return {
+ isLoggedIn: status.isLoggedIn,
+ username: this.config.user?.account,
+ user_id: this.config.user?.id
+ };
+ }
+
+ /**
+ * 设置下载路径
+ */
+ setDownloadPath(path) {
+ this.config.download.path = path;
+ this.saveConfig();
+ console.log(`下载路径已设置为: ${path}`);
+ return { success: true };
+ }
+
+ /**
+ * 获取配置信息
+ */
+ getConfig() {
+ return {
+ download: this.config.download,
+ proxy: this.config.proxy,
+ isLoggedIn: this.isLoggedIn
+ };
+ }
+
+ /**
+ * 设置代理
+ */
+ setProxy(proxy) {
+ this.config.proxy = proxy;
+ this.auth.setProxy(proxy);
+ this.saveConfig();
+ console.log(`代理已设置为: ${proxy}`);
+ return { success: true };
+ }
+
+ /**
+ * 获取认证实例(用于后续API调用)
+ */
+ getAuth() {
+ return this.auth;
+ }
+}
+
+module.exports = PixivBackend;
\ No newline at end of file
diff --git a/backend/middleware/auth.js b/backend/middleware/auth.js
new file mode 100644
index 0000000..df7e649
--- /dev/null
+++ b/backend/middleware/auth.js
@@ -0,0 +1,50 @@
+/**
+ * 认证中间件
+ */
+const authMiddleware = (req, res, next) => {
+ try {
+ // 检查后端是否已登录
+ if (!req.backend || !req.backend.isLoggedIn) {
+ return res.status(401).json({
+ error: true,
+ message: 'Authentication required',
+ code: 'AUTH_REQUIRED'
+ });
+ }
+
+ // 检查访问令牌是否有效
+ const auth = req.backend.getAuth();
+ if (!auth || !auth.accessToken) {
+ return res.status(401).json({
+ error: true,
+ message: 'Invalid access token',
+ code: 'INVALID_TOKEN'
+ });
+ }
+
+ next();
+ } catch (error) {
+ next(error);
+ }
+};
+
+/**
+ * 可选的认证中间件(不强制要求登录)
+ */
+const optionalAuthMiddleware = (req, res, next) => {
+ try {
+ // 如果后端已登录,将用户信息添加到请求对象
+ if (req.backend && req.backend.isLoggedIn) {
+ req.user = req.backend.config.user;
+ }
+
+ next();
+ } catch (error) {
+ next(error);
+ }
+};
+
+module.exports = {
+ authMiddleware,
+ optionalAuthMiddleware
+};
\ No newline at end of file
diff --git a/backend/middleware/errorHandler.js b/backend/middleware/errorHandler.js
new file mode 100644
index 0000000..96d3e3d
--- /dev/null
+++ b/backend/middleware/errorHandler.js
@@ -0,0 +1,62 @@
+/**
+ * 全局错误处理中间件
+ */
+const errorHandler = (err, req, res, next) => {
+ console.error('错误详情:', err);
+
+ // 默认错误信息
+ let statusCode = 500;
+ let message = 'Internal Server Error';
+ let details = null;
+
+ // 根据错误类型设置状态码和消息
+ if (err.name === 'ValidationError') {
+ statusCode = 400;
+ message = 'Validation Error';
+ details = err.message;
+ } else if (err.name === 'UnauthorizedError') {
+ statusCode = 401;
+ message = 'Unauthorized';
+ } else if (err.name === 'ForbiddenError') {
+ statusCode = 403;
+ message = 'Forbidden';
+ } else if (err.name === 'NotFoundError') {
+ statusCode = 404;
+ message = 'Not Found';
+ } else if (err.code === 'ENOTFOUND') {
+ statusCode = 503;
+ message = 'Service Unavailable';
+ details = 'Network connection failed';
+ } else if (err.code === 'ECONNREFUSED') {
+ statusCode = 503;
+ message = 'Service Unavailable';
+ details = 'Connection refused';
+ } else if (err.response) {
+ // Axios 错误
+ statusCode = err.response.status || 500;
+ message = err.response.statusText || 'Request Failed';
+ details = err.response.data;
+ } else if (err.message) {
+ message = err.message;
+ }
+
+ // 构建错误响应
+ const errorResponse = {
+ error: true,
+ message,
+ statusCode,
+ timestamp: new Date().toISOString(),
+ path: req.originalUrl,
+ method: req.method
+ };
+
+ // 在开发环境下添加详细信息
+ if (process.env.NODE_ENV === 'development') {
+ errorResponse.details = details;
+ errorResponse.stack = err.stack;
+ }
+
+ res.status(statusCode).json(errorResponse);
+};
+
+module.exports = { errorHandler };
\ No newline at end of file
diff --git a/backend/routes/artist.js b/backend/routes/artist.js
new file mode 100644
index 0000000..1ed5cfa
--- /dev/null
+++ b/backend/routes/artist.js
@@ -0,0 +1,225 @@
+const express = require('express');
+const router = express.Router();
+const ArtistService = require('../services/artist');
+
+/**
+ * 获取作者信息
+ * GET /api/artist/:id
+ */
+router.get('/:id', async (req, res) => {
+ try {
+ const { id } = req.params;
+
+ if (!id || isNaN(parseInt(id))) {
+ return res.status(400).json({
+ success: false,
+ error: 'Invalid artist ID'
+ });
+ }
+
+ const artistService = new ArtistService(req.backend.getAuth());
+ const result = await artistService.getArtistInfo(parseInt(id));
+
+ if (result.success) {
+ res.json({
+ success: true,
+ data: result.data
+ });
+ } else {
+ res.status(404).json({
+ success: false,
+ error: result.error
+ });
+ }
+ } catch (error) {
+ res.status(500).json({
+ success: false,
+ error: error.message
+ });
+ }
+});
+
+/**
+ * 获取作者作品列表
+ * GET /api/artist/:id/artworks
+ */
+router.get('/:id/artworks', async (req, res) => {
+ try {
+ const { id } = req.params;
+ const {
+ type = 'art',
+ filter = 'for_ios',
+ offset = 0,
+ limit = 30
+ } = req.query;
+
+ if (!id || isNaN(parseInt(id))) {
+ return res.status(400).json({
+ success: false,
+ error: 'Invalid artist ID'
+ });
+ }
+
+ const artistService = new ArtistService(req.backend.getAuth());
+ const result = await artistService.getArtistArtworks(parseInt(id), {
+ type,
+ filter,
+ offset: parseInt(offset),
+ limit: parseInt(limit)
+ });
+
+ if (result.success) {
+ res.json({
+ success: true,
+ data: result.data
+ });
+ } else {
+ res.status(404).json({
+ success: false,
+ error: result.error
+ });
+ }
+ } catch (error) {
+ res.status(500).json({
+ success: false,
+ error: error.message
+ });
+ }
+});
+
+/**
+ * 获取作者关注列表
+ * GET /api/artist/:id/following
+ */
+router.get('/:id/following', async (req, res) => {
+ try {
+ const { id } = req.params;
+ const {
+ restrict = 'public',
+ offset = 0,
+ limit = 30
+ } = req.query;
+
+ if (!id || isNaN(parseInt(id))) {
+ return res.status(400).json({
+ success: false,
+ error: 'Invalid artist ID'
+ });
+ }
+
+ const artistService = new ArtistService(req.backend.getAuth());
+ const result = await artistService.getArtistFollowing(parseInt(id), {
+ restrict,
+ offset: parseInt(offset),
+ limit: parseInt(limit)
+ });
+
+ if (result.success) {
+ res.json({
+ success: true,
+ data: result.data
+ });
+ } else {
+ res.status(404).json({
+ success: false,
+ error: result.error
+ });
+ }
+ } catch (error) {
+ res.status(500).json({
+ success: false,
+ error: error.message
+ });
+ }
+});
+
+/**
+ * 获取作者粉丝列表
+ * GET /api/artist/:id/followers
+ */
+router.get('/:id/followers', async (req, res) => {
+ try {
+ const { id } = req.params;
+ const {
+ offset = 0,
+ limit = 30
+ } = req.query;
+
+ if (!id || isNaN(parseInt(id))) {
+ return res.status(400).json({
+ success: false,
+ error: 'Invalid artist ID'
+ });
+ }
+
+ const artistService = new ArtistService(req.backend.getAuth());
+ const result = await artistService.getArtistFollowers(parseInt(id), {
+ offset: parseInt(offset),
+ limit: parseInt(limit)
+ });
+
+ if (result.success) {
+ res.json({
+ success: true,
+ data: result.data
+ });
+ } else {
+ res.status(404).json({
+ success: false,
+ error: result.error
+ });
+ }
+ } catch (error) {
+ res.status(500).json({
+ success: false,
+ error: error.message
+ });
+ }
+});
+
+/**
+ * 关注/取消关注作者
+ * POST /api/artist/:id/follow
+ */
+router.post('/:id/follow', async (req, res) => {
+ try {
+ const { id } = req.params;
+ const { action = 'follow' } = req.body;
+
+ if (!id || isNaN(parseInt(id))) {
+ return res.status(400).json({
+ success: false,
+ error: 'Invalid artist ID'
+ });
+ }
+
+ if (!['follow', 'unfollow'].includes(action)) {
+ return res.status(400).json({
+ success: false,
+ error: 'Invalid action. Must be "follow" or "unfollow"'
+ });
+ }
+
+ const artistService = new ArtistService(req.backend.getAuth());
+ const result = await artistService.followArtist(parseInt(id), action);
+
+ if (result.success) {
+ res.json({
+ success: true,
+ message: `Artist ${action === 'follow' ? 'followed' : 'unfollowed'} successfully`
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ error: result.error
+ });
+ }
+ } catch (error) {
+ res.status(500).json({
+ success: false,
+ error: error.message
+ });
+ }
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/backend/routes/artwork.js b/backend/routes/artwork.js
new file mode 100644
index 0000000..a9ea801
--- /dev/null
+++ b/backend/routes/artwork.js
@@ -0,0 +1,172 @@
+const express = require('express');
+const router = express.Router();
+const ArtworkService = require('../services/artwork');
+
+/**
+ * 搜索作品
+ * GET /api/artwork/search
+ */
+router.get('/search', async (req, res) => {
+ try {
+ const {
+ keyword,
+ type = 'all',
+ sort = 'date_desc',
+ duration = 'all',
+ offset = 0,
+ limit = 30
+ } = req.query;
+
+ if (!keyword) {
+ return res.status(400).json({
+ success: false,
+ error: 'Search keyword is required'
+ });
+ }
+
+ const artworkService = new ArtworkService(req.backend.getAuth());
+ const result = await artworkService.searchArtworks({
+ keyword,
+ type,
+ sort,
+ duration,
+ offset: parseInt(offset),
+ limit: parseInt(limit)
+ });
+
+ if (result.success) {
+ res.json({
+ success: true,
+ data: result.data
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ error: result.error
+ });
+ }
+ } catch (error) {
+ res.status(500).json({
+ success: false,
+ error: error.message
+ });
+ }
+});
+
+/**
+ * 获取作品详情
+ * GET /api/artwork/:id
+ */
+router.get('/:id', async (req, res) => {
+ try {
+ const { id } = req.params;
+ const { include_user = 'true', include_series = 'false' } = req.query;
+
+ if (!id || isNaN(parseInt(id))) {
+ return res.status(400).json({
+ success: false,
+ error: 'Invalid artwork ID'
+ });
+ }
+
+ const artworkService = new ArtworkService(req.backend.getAuth());
+ const result = await artworkService.getArtworkDetail(parseInt(id), {
+ include_user: include_user === 'true',
+ include_series: include_series === 'true'
+ });
+
+ if (result.success) {
+ res.json({
+ success: true,
+ data: result.data
+ });
+ } else {
+ res.status(404).json({
+ success: false,
+ error: result.error
+ });
+ }
+ } catch (error) {
+ res.status(500).json({
+ success: false,
+ error: error.message
+ });
+ }
+});
+
+/**
+ * 获取作品预览信息
+ * GET /api/artwork/:id/preview
+ */
+router.get('/:id/preview', async (req, res) => {
+ try {
+ const { id } = req.params;
+
+ if (!id || isNaN(parseInt(id))) {
+ return res.status(400).json({
+ success: false,
+ error: 'Invalid artwork ID'
+ });
+ }
+
+ const artworkService = new ArtworkService(req.backend.getAuth());
+ const result = await artworkService.getArtworkPreview(parseInt(id));
+
+ if (result.success) {
+ res.json({
+ success: true,
+ data: result.data
+ });
+ } else {
+ res.status(404).json({
+ success: false,
+ error: result.error
+ });
+ }
+ } catch (error) {
+ res.status(500).json({
+ success: false,
+ error: error.message
+ });
+ }
+});
+
+/**
+ * 获取作品图片URL
+ * GET /api/artwork/:id/images
+ */
+router.get('/:id/images', async (req, res) => {
+ try {
+ const { id } = req.params;
+ const { size = 'medium' } = req.query;
+
+ if (!id || isNaN(parseInt(id))) {
+ return res.status(400).json({
+ success: false,
+ error: 'Invalid artwork ID'
+ });
+ }
+
+ const artworkService = new ArtworkService(req.backend.getAuth());
+ const result = await artworkService.getArtworkImages(parseInt(id), size);
+
+ if (result.success) {
+ res.json({
+ success: true,
+ data: result.data
+ });
+ } else {
+ res.status(404).json({
+ success: false,
+ error: result.error
+ });
+ }
+ } catch (error) {
+ res.status(500).json({
+ success: false,
+ error: error.message
+ });
+ }
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/backend/routes/auth.js b/backend/routes/auth.js
new file mode 100644
index 0000000..1961fc6
--- /dev/null
+++ b/backend/routes/auth.js
@@ -0,0 +1,124 @@
+const express = require('express');
+const router = express.Router();
+
+/**
+ * 获取登录状态
+ * GET /api/auth/status
+ */
+router.get('/status', (req, res) => {
+ try {
+ const status = req.backend.getLoginStatus();
+ res.json({
+ success: true,
+ data: status
+ });
+ } catch (error) {
+ res.status(500).json({
+ success: false,
+ error: error.message
+ });
+ }
+});
+
+/**
+ * 获取登录URL
+ * GET /api/auth/login-url
+ */
+router.get('/login-url', (req, res) => {
+ try {
+ const loginData = req.backend.getLoginUrl();
+ res.json({
+ success: true,
+ data: loginData
+ });
+ } catch (error) {
+ res.status(500).json({
+ success: false,
+ error: error.message
+ });
+ }
+});
+
+/**
+ * 处理登录回调
+ * POST /api/auth/callback
+ */
+router.post('/callback', async (req, res) => {
+ try {
+ const { code } = req.body;
+
+ if (!code) {
+ return res.status(400).json({
+ success: false,
+ error: 'Authorization code is required'
+ });
+ }
+
+ const result = await req.backend.handleLoginCallback(code);
+
+ if (result.success) {
+ res.json({
+ success: true,
+ data: result
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ error: result.error
+ });
+ }
+ } catch (error) {
+ res.status(500).json({
+ success: false,
+ error: error.message
+ });
+ }
+});
+
+/**
+ * 重新登录
+ * POST /api/auth/relogin
+ */
+router.post('/relogin', async (req, res) => {
+ try {
+ const result = await req.backend.relogin();
+
+ if (result.success) {
+ res.json({
+ success: true,
+ message: 'Relogin successful'
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ error: result.error
+ });
+ }
+ } catch (error) {
+ res.status(500).json({
+ success: false,
+ error: error.message
+ });
+ }
+});
+
+/**
+ * 登出
+ * POST /api/auth/logout
+ */
+router.post('/logout', (req, res) => {
+ try {
+ const result = req.backend.logout();
+ res.json({
+ success: true,
+ message: 'Logout successful'
+ });
+ } catch (error) {
+ res.status(500).json({
+ success: false,
+ error: error.message
+ });
+ }
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/backend/routes/download.js b/backend/routes/download.js
new file mode 100644
index 0000000..c9b0ee2
--- /dev/null
+++ b/backend/routes/download.js
@@ -0,0 +1,270 @@
+const express = require('express');
+const router = express.Router();
+const DownloadService = require('../services/download');
+
+/**
+ * 下载单个作品
+ * POST /api/download/artwork/:id
+ */
+router.post('/artwork/:id', async (req, res) => {
+ try {
+ const { id } = req.params;
+ const {
+ size = 'original',
+ quality = 'high',
+ format = 'auto'
+ } = req.body;
+
+ if (!id || isNaN(parseInt(id))) {
+ return res.status(400).json({
+ success: false,
+ error: 'Invalid artwork ID'
+ });
+ }
+
+ const downloadService = new DownloadService(req.backend.getAuth());
+ const result = await downloadService.downloadArtwork(parseInt(id), {
+ size,
+ quality,
+ format
+ });
+
+ if (result.success) {
+ res.json({
+ success: true,
+ data: result.data
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ error: result.error
+ });
+ }
+ } catch (error) {
+ res.status(500).json({
+ success: false,
+ error: error.message
+ });
+ }
+});
+
+/**
+ * 批量下载作品
+ * POST /api/download/artworks
+ */
+router.post('/artworks', async (req, res) => {
+ try {
+ const {
+ artworkIds,
+ size = 'original',
+ quality = 'high',
+ format = 'auto',
+ concurrent = 3
+ } = req.body;
+
+ if (!artworkIds || !Array.isArray(artworkIds) || artworkIds.length === 0) {
+ return res.status(400).json({
+ success: false,
+ error: 'Artwork IDs array is required'
+ });
+ }
+
+ if (artworkIds.length > 50) {
+ return res.status(400).json({
+ success: false,
+ error: 'Maximum 50 artworks can be downloaded at once'
+ });
+ }
+
+ const downloadService = new DownloadService(req.backend.getAuth());
+ const result = await downloadService.downloadMultipleArtworks(artworkIds, {
+ size,
+ quality,
+ format,
+ concurrent: parseInt(concurrent)
+ });
+
+ if (result.success) {
+ res.json({
+ success: true,
+ data: result.data
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ error: result.error
+ });
+ }
+ } catch (error) {
+ res.status(500).json({
+ success: false,
+ error: error.message
+ });
+ }
+});
+
+/**
+ * 下载作者作品
+ * POST /api/download/artist/:id
+ */
+router.post('/artist/:id', async (req, res) => {
+ try {
+ const { id } = req.params;
+ const {
+ type = 'art',
+ filter = 'for_ios',
+ size = 'original',
+ quality = 'high',
+ format = 'auto',
+ limit = 50,
+ concurrent = 3
+ } = req.body;
+
+ if (!id || isNaN(parseInt(id))) {
+ return res.status(400).json({
+ success: false,
+ error: 'Invalid artist ID'
+ });
+ }
+
+ const downloadService = new DownloadService(req.backend.getAuth());
+ const result = await downloadService.downloadArtistArtworks(parseInt(id), {
+ type,
+ filter,
+ size,
+ quality,
+ format,
+ limit: parseInt(limit),
+ concurrent: parseInt(concurrent)
+ });
+
+ if (result.success) {
+ res.json({
+ success: true,
+ data: result.data
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ error: result.error
+ });
+ }
+ } catch (error) {
+ res.status(500).json({
+ success: false,
+ error: error.message
+ });
+ }
+});
+
+/**
+ * 获取下载进度
+ * GET /api/download/progress/:taskId
+ */
+router.get('/progress/:taskId', async (req, res) => {
+ try {
+ const { taskId } = req.params;
+
+ if (!taskId) {
+ return res.status(400).json({
+ success: false,
+ error: 'Task ID is required'
+ });
+ }
+
+ const downloadService = new DownloadService(req.backend.getAuth());
+ const result = await downloadService.getDownloadProgress(taskId);
+
+ if (result.success) {
+ res.json({
+ success: true,
+ data: result.data
+ });
+ } else {
+ res.status(404).json({
+ success: false,
+ error: result.error
+ });
+ }
+ } catch (error) {
+ res.status(500).json({
+ success: false,
+ error: error.message
+ });
+ }
+});
+
+/**
+ * 取消下载任务
+ * DELETE /api/download/cancel/:taskId
+ */
+router.delete('/cancel/:taskId', async (req, res) => {
+ try {
+ const { taskId } = req.params;
+
+ if (!taskId) {
+ return res.status(400).json({
+ success: false,
+ error: 'Task ID is required'
+ });
+ }
+
+ const downloadService = new DownloadService(req.backend.getAuth());
+ const result = await downloadService.cancelDownload(taskId);
+
+ if (result.success) {
+ res.json({
+ success: true,
+ message: 'Download task cancelled successfully'
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ error: result.error
+ });
+ }
+ } catch (error) {
+ res.status(500).json({
+ success: false,
+ error: error.message
+ });
+ }
+});
+
+/**
+ * 获取下载历史
+ * GET /api/download/history
+ */
+router.get('/history', async (req, res) => {
+ try {
+ const {
+ offset = 0,
+ limit = 20
+ } = req.query;
+
+ const downloadService = new DownloadService(req.backend.getAuth());
+ const result = await downloadService.getDownloadHistory({
+ offset: parseInt(offset),
+ limit: parseInt(limit)
+ });
+
+ if (result.success) {
+ res.json({
+ success: true,
+ data: result.data
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ error: result.error
+ });
+ }
+ } catch (error) {
+ res.status(500).json({
+ success: false,
+ error: error.message
+ });
+ }
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/backend/server.js b/backend/server.js
new file mode 100644
index 0000000..7d0b6ef
--- /dev/null
+++ b/backend/server.js
@@ -0,0 +1,155 @@
+const express = require('express');
+const cors = require('cors');
+const morgan = require('morgan');
+const path = require('path');
+
+// 导入路由模块 - 临时注释掉来定位问题
+const authRoutes = require('./routes/auth');
+const artworkRoutes = require('./routes/artwork');
+const artistRoutes = require('./routes/artist');
+const downloadRoutes = require('./routes/download');
+
+// 导入中间件 - 临时注释掉来定位问题
+const { errorHandler } = require('./middleware/errorHandler');
+const { authMiddleware } = require('./middleware/auth');
+
+// 导入核心模块
+const PixivBackend = require('./core');
+const proxyConfig = require('./config');
+
+class PixivServer {
+ constructor() {
+ this.app = express();
+ this.backend = null;
+ this.port = process.env.PORT || 3000;
+ }
+
+ /**
+ * 初始化服务器
+ */
+ async init() {
+ console.log('正在初始化 Pixiv 后端服务器...');
+
+ // 设置代理
+ proxyConfig.setEnvironmentVariables();
+
+ // 初始化 Pixiv 后端
+ this.backend = new PixivBackend();
+ await this.backend.init();
+
+ // 配置中间件
+ this.setupMiddleware();
+
+ // 配置路由
+ this.setupRoutes();
+
+ // 配置错误处理 - 临时注释掉
+ this.setupErrorHandling();
+
+ console.log('服务器初始化完成');
+ }
+
+ /**
+ * 配置中间件
+ */
+ setupMiddleware() {
+ // 日志中间件
+ this.app.use(morgan('combined'));
+
+ // CORS 中间件
+ this.app.use(cors({
+ origin: process.env.FRONTEND_URL || 'http://localhost:3001',
+ credentials: true
+ }));
+
+ // JSON 解析中间件
+ this.app.use(express.json({ limit: '10mb' }));
+ this.app.use(express.urlencoded({ extended: true, limit: '10mb' }));
+
+ // 静态文件服务 - 临时注释掉
+ this.app.use('/downloads', express.static(path.join(__dirname, '../downloads')));
+
+ // 将后端实例注入到请求对象中
+ this.app.use((req, res, next) => {
+ req.backend = this.backend;
+ next();
+ });
+ }
+
+ /**
+ * 配置路由
+ */
+ setupRoutes() {
+ // 健康检查
+ this.app.get('/health', (req, res) => {
+ res.json({
+ status: 'ok',
+ timestamp: new Date().toISOString(),
+ backend: {
+ isLoggedIn: req.backend.isLoggedIn,
+ user: req.backend.config.user?.account
+ }
+ });
+ });
+
+ // API 路由 - 临时注释掉来定位问题
+ this.app.use('/api/auth', authRoutes);
+ this.app.use('/api/artwork', authMiddleware, artworkRoutes);
+ this.app.use('/api/artist', authMiddleware, artistRoutes);
+ this.app.use('/api/download', authMiddleware, downloadRoutes);
+
+ // 404 处理
+ this.app.use((req, res) => {
+ res.status(404).json({
+ error: 'Not Found',
+ message: `Route ${req.originalUrl} not found`
+ });
+ });
+ }
+
+ /**
+ * 配置错误处理
+ */
+ setupErrorHandling() {
+ this.app.use(errorHandler);
+ }
+
+ /**
+ * 启动服务器
+ */
+ start() {
+ this.app.listen(this.port, () => {
+ console.log(`🚀 Pixiv 后端服务器已启动`);
+ console.log(`📍 地址: http://localhost:${this.port}`);
+ console.log(`🔗 健康检查: http://localhost:${this.port}/health`);
+ console.log(`📊 登录状态: ${this.backend.isLoggedIn ? '已登录' : '未登录'}`);
+ if (this.backend.isLoggedIn) {
+ console.log(`👤 用户: ${this.backend.config.user?.account}`);
+ }
+ });
+ }
+
+ /**
+ * 优雅关闭
+ */
+ async shutdown() {
+ console.log('正在关闭服务器...');
+ // 清理代理环境变量
+ proxyConfig.clearEnvironmentVariables();
+ process.exit(0);
+ }
+}
+
+// 如果直接运行此文件
+if (require.main === module) {
+ const server = new PixivServer();
+
+ // 处理进程信号
+ process.on('SIGINT', () => server.shutdown());
+ process.on('SIGTERM', () => server.shutdown());
+
+ // 启动服务器
+ server.init().then(() => server.start()).catch(console.error);
+}
+
+module.exports = PixivServer;
\ No newline at end of file
diff --git a/backend/services/artist.js b/backend/services/artist.js
new file mode 100644
index 0000000..6ff055a
--- /dev/null
+++ b/backend/services/artist.js
@@ -0,0 +1,344 @@
+const axios = require('axios');
+const { stringify } = require('qs');
+
+class ArtistService {
+ constructor(auth) {
+ this.auth = auth;
+ this.baseURL = 'https://app-api.pixiv.net';
+ }
+
+ /**
+ * 获取作者信息
+ */
+ async getArtistInfo(artistId) {
+ try {
+ const response = await this.makeRequest(
+ 'GET',
+ '/v1/user/detail',
+ { user_id: artistId }
+ );
+
+ return {
+ success: true,
+ data: response.user
+ };
+
+ } catch (error) {
+ return {
+ success: false,
+ error: error.message
+ };
+ }
+ }
+
+ /**
+ * 获取作者作品列表
+ */
+ async getArtistArtworks(artistId, options = {}) {
+ try {
+ const {
+ type = 'art',
+ filter = 'for_ios',
+ offset = 0,
+ limit = 30
+ } = options;
+
+ const params = {
+ user_id: artistId,
+ type,
+ filter,
+ offset
+ };
+
+ const response = await this.makeRequest(
+ 'GET',
+ `/v1/user/illusts?${stringify(params)}`
+ );
+
+ return {
+ success: true,
+ data: {
+ artworks: response.illusts,
+ next_url: response.next_url,
+ total: response.illusts.length
+ }
+ };
+
+ } catch (error) {
+ return {
+ success: false,
+ error: error.message
+ };
+ }
+ }
+
+ /**
+ * 获取作者关注列表
+ */
+ async getArtistFollowing(artistId, options = {}) {
+ try {
+ const {
+ restrict = 'public',
+ offset = 0,
+ limit = 30
+ } = options;
+
+ const params = {
+ user_id: artistId,
+ restrict,
+ offset
+ };
+
+ const response = await this.makeRequest(
+ 'GET',
+ `/v1/user/following?${stringify(params)}`
+ );
+
+ return {
+ success: true,
+ data: {
+ users: response.user_previews,
+ next_url: response.next_url,
+ total: response.user_previews.length
+ }
+ };
+
+ } catch (error) {
+ return {
+ success: false,
+ error: error.message
+ };
+ }
+ }
+
+ /**
+ * 获取作者粉丝列表
+ */
+ async getArtistFollowers(artistId, options = {}) {
+ try {
+ const {
+ offset = 0,
+ limit = 30
+ } = options;
+
+ const params = {
+ user_id: artistId,
+ offset
+ };
+
+ const response = await this.makeRequest(
+ 'GET',
+ `/v1/user/follower?${stringify(params)}`
+ );
+
+ return {
+ success: true,
+ data: {
+ users: response.user_previews,
+ next_url: response.next_url,
+ total: response.user_previews.length
+ }
+ };
+
+ } catch (error) {
+ return {
+ success: false,
+ error: error.message
+ };
+ }
+ }
+
+ /**
+ * 关注/取消关注作者
+ */
+ async followArtist(artistId, action = 'follow') {
+ try {
+ const data = {
+ user_id: artistId,
+ restrict: 'public'
+ };
+
+ const endpoint = action === 'follow' ? '/v1/user/follow/add' : '/v1/user/follow/delete';
+
+ const response = await this.makeRequest(
+ 'POST',
+ endpoint,
+ data
+ );
+
+ return {
+ success: true,
+ data: response
+ };
+
+ } catch (error) {
+ return {
+ success: false,
+ error: error.message
+ };
+ }
+ }
+
+ /**
+ * 搜索作者
+ */
+ async searchArtists(searchOptions) {
+ try {
+ const {
+ keyword,
+ sort = 'date_desc',
+ duration = 'all',
+ offset = 0,
+ limit = 30
+ } = searchOptions;
+
+ const params = {
+ word: keyword,
+ sort,
+ duration,
+ offset,
+ filter: 'for_ios'
+ };
+
+ const response = await this.makeRequest(
+ 'GET',
+ `/v1/search/user?${stringify(params)}`
+ );
+
+ return {
+ success: true,
+ data: {
+ users: response.user_previews,
+ next_url: response.next_url,
+ search_span_limit: response.search_span_limit,
+ total: response.user_previews.length
+ }
+ };
+
+ } catch (error) {
+ return {
+ success: false,
+ error: error.message
+ };
+ }
+ }
+
+ /**
+ * 获取推荐作者
+ */
+ async getRecommendedArtists(options = {}) {
+ try {
+ const {
+ offset = 0,
+ limit = 30
+ } = options;
+
+ const params = {
+ offset,
+ filter: 'for_ios'
+ };
+
+ const response = await this.makeRequest(
+ 'GET',
+ `/v1/user/recommended?${stringify(params)}`
+ );
+
+ return {
+ success: true,
+ data: {
+ users: response.user_previews,
+ next_url: response.next_url
+ }
+ };
+
+ } catch (error) {
+ return {
+ success: false,
+ error: error.message
+ };
+ }
+ }
+
+ /**
+ * 获取作者统计信息
+ */
+ async getArtistStats(artistId) {
+ try {
+ const response = await this.makeRequest(
+ 'GET',
+ '/v1/user/detail',
+ { user_id: artistId }
+ );
+
+ const user = response.user;
+
+ const stats = {
+ user_id: user.id,
+ total_illusts: user.total_illusts,
+ total_manga: user.total_manga,
+ total_novels: user.total_novels,
+ total_bookmarked_illust: user.total_bookmarked_illust,
+ total_following: user.total_following,
+ total_followers: user.total_followers,
+ total_illust_bookmarks_public: user.total_illust_bookmarks_public,
+ total_illust_series: user.total_illust_series,
+ total_novel_series: user.total_novel_series,
+ background: user.background,
+ twitter_account: user.twitter_account,
+ twitter_url: user.twitter_url,
+ pawoo_url: user.pawoo_url,
+ is_followed: user.is_followed,
+ is_following: user.is_following,
+ is_friend: user.is_friend,
+ is_blocking: user.is_blocking,
+ is_blocked: user.is_blocked,
+ accept_request: user.accept_request
+ };
+
+ return {
+ success: true,
+ data: stats
+ };
+
+ } catch (error) {
+ return {
+ success: false,
+ error: error.message
+ };
+ }
+ }
+
+ /**
+ * 发送API请求
+ */
+ async makeRequest(method, endpoint, data = null) {
+ const headers = {
+ 'Authorization': `Bearer ${this.auth.accessToken}`,
+ 'Accept-Language': 'en-us',
+ 'App-OS': 'android',
+ 'App-OS-Version': '9.0',
+ 'App-Version': '5.0.234',
+ 'User-Agent': 'PixivAndroidApp/5.0.234 (Android 9.0; Pixel 3)'
+ };
+
+ const config = {
+ method,
+ url: `${this.baseURL}${endpoint}`,
+ headers,
+ timeout: 30000
+ };
+
+ if (data) {
+ if (method === 'GET') {
+ config.params = data;
+ } else {
+ config.data = data;
+ }
+ }
+
+ const response = await axios(config);
+ return response.data;
+ }
+}
+
+module.exports = ArtistService;
\ No newline at end of file
diff --git a/backend/services/artwork.js b/backend/services/artwork.js
new file mode 100644
index 0000000..fae13bc
--- /dev/null
+++ b/backend/services/artwork.js
@@ -0,0 +1,307 @@
+const axios = require('axios');
+const { stringify } = require('qs');
+
+class ArtworkService {
+ constructor(auth) {
+ this.auth = auth;
+ this.baseURL = 'https://app-api.pixiv.net';
+ }
+
+ /**
+ * 获取作品详情
+ */
+ async getArtworkDetail(artworkId, options = {}) {
+ try {
+ const { include_user = true, include_series = false } = options;
+
+ const params = {
+ include_user,
+ include_series
+ };
+
+ const response = await this.makeRequest(
+ 'GET',
+ `/v1/illust/detail?${stringify(params)}`,
+ { illust_id: artworkId }
+ );
+
+ return {
+ success: true,
+ data: response.illust
+ };
+
+ } catch (error) {
+ return {
+ success: false,
+ error: error.message
+ };
+ }
+ }
+
+ /**
+ * 获取作品预览信息
+ */
+ async getArtworkPreview(artworkId) {
+ try {
+ const response = await this.makeRequest(
+ 'GET',
+ '/v1/illust/detail',
+ { illust_id: artworkId }
+ );
+
+ const artwork = response.illust;
+
+ // 构建预览信息
+ const preview = {
+ id: artwork.id,
+ title: artwork.title,
+ description: artwork.caption,
+ user: {
+ id: artwork.user.id,
+ name: artwork.user.name,
+ account: artwork.user.account
+ },
+ image_urls: artwork.image_urls,
+ tags: artwork.tags.map(tag => tag.name),
+ create_date: artwork.create_date,
+ update_date: artwork.update_date,
+ type: artwork.type,
+ width: artwork.width,
+ height: artwork.height,
+ page_count: artwork.page_count,
+ is_bookmarked: artwork.is_bookmarked,
+ total_bookmarks: artwork.total_bookmarks,
+ total_view: artwork.total_view,
+ is_muted: artwork.is_muted,
+ meta_single_page: artwork.meta_single_page,
+ meta_pages: artwork.meta_pages
+ };
+
+ return {
+ success: true,
+ data: preview
+ };
+
+ } catch (error) {
+ return {
+ success: false,
+ error: error.message
+ };
+ }
+ }
+
+ /**
+ * 获取作品图片URL
+ */
+ async getArtworkImages(artworkId, size = 'medium') {
+ try {
+ const response = await this.makeRequest(
+ 'GET',
+ '/v1/illust/detail',
+ { illust_id: artworkId }
+ );
+
+ const artwork = response.illust;
+ const images = [];
+
+ if (artwork.meta_single_page && artwork.meta_single_page.original_image_url) {
+ // 单页作品
+ images.push({
+ page: 1,
+ original: artwork.meta_single_page.original_image_url,
+ large: artwork.meta_single_page.large_image_url,
+ medium: artwork.image_urls.medium,
+ square_medium: artwork.image_urls.square_medium
+ });
+ } else if (artwork.meta_pages && artwork.meta_pages.length > 0) {
+ // 多页作品
+ artwork.meta_pages.forEach((page, index) => {
+ images.push({
+ page: index + 1,
+ original: page.image_urls.original,
+ large: page.image_urls.large,
+ medium: page.image_urls.medium,
+ square_medium: page.image_urls.square_medium
+ });
+ });
+ }
+
+ return {
+ success: true,
+ data: {
+ artwork_id: artworkId,
+ total_pages: artwork.page_count,
+ images: images,
+ selected_size: size
+ }
+ };
+
+ } catch (error) {
+ return {
+ success: false,
+ error: error.message
+ };
+ }
+ }
+
+ /**
+ * 搜索作品
+ */
+ async searchArtworks(searchOptions) {
+ try {
+ const {
+ keyword,
+ type = 'all',
+ sort = 'date_desc',
+ duration = 'all',
+ offset = 0,
+ limit = 30
+ } = searchOptions;
+
+ const params = {
+ word: keyword,
+ search_target: type,
+ sort: sort,
+ duration: duration,
+ offset,
+ filter: 'for_ios'
+ };
+
+ const response = await this.makeRequest(
+ 'GET',
+ `/v1/search/illust?${stringify(params)}`
+ );
+
+ return {
+ success: true,
+ data: {
+ artworks: response.illusts,
+ next_url: response.next_url,
+ search_span_limit: response.search_span_limit,
+ total: response.illusts.length
+ }
+ };
+
+ } catch (error) {
+ return {
+ success: false,
+ error: error.message
+ };
+ }
+ }
+
+ /**
+ * 获取推荐作品
+ */
+ async getRecommendedArtworks(options = {}) {
+ try {
+ const {
+ offset = 0,
+ limit = 30,
+ include_ranking_illusts = true,
+ include_privacy_policy = false
+ } = options;
+
+ const params = {
+ offset,
+ include_ranking_illusts,
+ include_privacy_policy,
+ filter: 'for_ios'
+ };
+
+ const response = await this.makeRequest(
+ 'GET',
+ `/v1/illust/recommended?${stringify(params)}`
+ );
+
+ return {
+ success: true,
+ data: {
+ artworks: response.illusts,
+ next_url: response.next_url,
+ ranking_illusts: response.ranking_illusts || []
+ }
+ };
+
+ } catch (error) {
+ return {
+ success: false,
+ error: error.message
+ };
+ }
+ }
+
+ /**
+ * 获取排行榜作品
+ */
+ async getRankingArtworks(options = {}) {
+ try {
+ const {
+ mode = 'day',
+ filter = 'for_ios',
+ offset = 0
+ } = options;
+
+ const params = {
+ mode,
+ filter,
+ offset
+ };
+
+ const response = await this.makeRequest(
+ 'GET',
+ `/v1/illust/ranking?${stringify(params)}`
+ );
+
+ return {
+ success: true,
+ data: {
+ artworks: response.illusts,
+ next_url: response.next_url,
+ mode,
+ date: response.date
+ }
+ };
+
+ } catch (error) {
+ return {
+ success: false,
+ error: error.message
+ };
+ }
+ }
+
+ /**
+ * 发送API请求
+ */
+ async makeRequest(method, endpoint, data = null) {
+ const headers = {
+ 'Authorization': `Bearer ${this.auth.accessToken}`,
+ 'Accept-Language': 'en-us',
+ 'App-OS': 'android',
+ 'App-OS-Version': '9.0',
+ 'App-Version': '5.0.234',
+ 'User-Agent': 'PixivAndroidApp/5.0.234 (Android 9.0; Pixel 3)'
+ };
+
+ const config = {
+ method,
+ url: `${this.baseURL}${endpoint}`,
+ headers,
+ timeout: 30000
+ };
+
+ if (data) {
+ if (method === 'GET') {
+ config.params = data;
+ } else {
+ config.data = data;
+ }
+ }
+
+ const response = await axios(config);
+ return response.data;
+ }
+}
+
+module.exports = ArtworkService;
\ No newline at end of file
diff --git a/backend/services/download.js b/backend/services/download.js
new file mode 100644
index 0000000..2171125
--- /dev/null
+++ b/backend/services/download.js
@@ -0,0 +1,444 @@
+const axios = require('axios');
+const fs = require('fs-extra');
+const path = require('path');
+const { v4: uuidv4 } = require('uuid');
+const ArtworkService = require('./artwork');
+const ArtistService = require('./artist');
+
+class DownloadService {
+ constructor(auth) {
+ this.auth = auth;
+ this.artworkService = new ArtworkService(auth);
+ this.artistService = new ArtistService(auth);
+ this.downloadPath = path.join(__dirname, '../../downloads');
+ this.tasks = new Map(); // 存储下载任务状态
+
+ // 确保下载目录存在
+ this.ensureDownloadDir();
+ }
+
+ /**
+ * 确保下载目录存在
+ */
+ async ensureDownloadDir() {
+ try {
+ await fs.ensureDir(this.downloadPath);
+ console.log('下载目录已创建:', this.downloadPath);
+ } catch (error) {
+ console.error('创建下载目录失败:', error);
+ }
+ }
+
+ /**
+ * 下载单个作品
+ */
+ async downloadArtwork(artworkId, options = {}) {
+ const taskId = uuidv4();
+ const { size = 'original', quality = 'high', format = 'auto' } = options;
+
+ try {
+ // 创建任务记录
+ this.tasks.set(taskId, {
+ id: taskId,
+ type: 'artwork',
+ artwork_id: artworkId,
+ status: 'downloading',
+ progress: 0,
+ total: 1,
+ completed: 0,
+ failed: 0,
+ files: [],
+ start_time: new Date(),
+ end_time: null
+ });
+
+ // 获取作品信息
+ const artworkResult = await this.artworkService.getArtworkDetail(artworkId);
+ if (!artworkResult.success) {
+ throw new Error(`获取作品信息失败: ${artworkResult.error}`);
+ }
+
+ const artwork = artworkResult.data;
+ const artistName = artwork.user.name.replace(/[<>:"/\\|?*]/g, '_');
+ const artworkTitle = artwork.title.replace(/[<>:"/\\|?*]/g, '_');
+
+ // 创建作品目录
+ const artworkDir = path.join(this.downloadPath, `${artistName}_${artworkId}`, artworkTitle);
+ await fs.ensureDir(artworkDir);
+
+ // 获取图片URL
+ const imagesResult = await this.artworkService.getArtworkImages(artworkId, size);
+ if (!imagesResult.success) {
+ throw new Error(`获取图片URL失败: ${imagesResult.error}`);
+ }
+
+ const images = imagesResult.data.images;
+ const task = this.tasks.get(taskId);
+ task.total = images.length;
+
+ // 下载所有图片
+ const downloadPromises = images.map(async (image, index) => {
+ try {
+ const imageUrl = image[size] || image.original;
+ const fileExt = this.getFileExtension(imageUrl);
+ const fileName = `${artworkTitle}_${artworkId}_${index + 1}${fileExt}`;
+ const filePath = path.join(artworkDir, fileName);
+
+ await this.downloadFile(imageUrl, filePath);
+
+ task.completed++;
+ task.progress = Math.round((task.completed / task.total) * 100);
+ task.files.push({
+ path: filePath,
+ url: imageUrl,
+ size: size
+ });
+
+ return { success: true, file: fileName };
+ } catch (error) {
+ task.failed++;
+ console.error(`下载图片失败 ${index + 1}:`, error.message);
+ return { success: false, error: error.message };
+ }
+ });
+
+ await Promise.all(downloadPromises);
+
+ // 保存作品信息
+ const infoPath = path.join(artworkDir, 'artwork_info.json');
+ await fs.writeJson(infoPath, artwork, { spaces: 2 });
+
+ // 更新任务状态
+ task.status = task.failed === 0 ? 'completed' : 'partial';
+ task.end_time = new Date();
+
+ return {
+ success: true,
+ data: {
+ task_id: taskId,
+ artwork_id: artworkId,
+ artist_name: artistName,
+ artwork_title: artworkTitle,
+ download_path: artworkDir,
+ total_files: task.total,
+ completed_files: task.completed,
+ failed_files: task.failed,
+ files: task.files
+ }
+ };
+
+ } catch (error) {
+ const task = this.tasks.get(taskId);
+ if (task) {
+ task.status = 'failed';
+ task.end_time = new Date();
+ }
+
+ return {
+ success: false,
+ error: error.message
+ };
+ }
+ }
+
+ /**
+ * 批量下载作品
+ */
+ async downloadMultipleArtworks(artworkIds, options = {}) {
+ const taskId = uuidv4();
+ const { concurrent = 3, size = 'original', quality = 'high', format = 'auto' } = options;
+
+ try {
+ // 创建任务记录
+ this.tasks.set(taskId, {
+ id: taskId,
+ type: 'batch',
+ artwork_ids: artworkIds,
+ status: 'downloading',
+ progress: 0,
+ total: artworkIds.length,
+ completed: 0,
+ failed: 0,
+ results: [],
+ start_time: new Date(),
+ end_time: null
+ });
+
+ const task = this.tasks.get(taskId);
+ const results = [];
+
+ // 分批下载
+ for (let i = 0; i < artworkIds.length; i += concurrent) {
+ const batch = artworkIds.slice(i, i + concurrent);
+ const batchPromises = batch.map(async (artworkId) => {
+ try {
+ const result = await this.downloadArtwork(artworkId, { size, quality, format });
+ task.completed++;
+ results.push({ artwork_id: artworkId, ...result });
+ return result;
+ } catch (error) {
+ task.failed++;
+ results.push({ artwork_id: artworkId, success: false, error: error.message });
+ return { success: false, error: error.message };
+ }
+ });
+
+ await Promise.all(batchPromises);
+ task.progress = Math.round((task.completed / task.total) * 100);
+ }
+
+ // 更新任务状态
+ task.status = task.failed === 0 ? 'completed' : 'partial';
+ task.end_time = new Date();
+ task.results = results;
+
+ return {
+ success: true,
+ data: {
+ task_id: taskId,
+ total_artworks: task.total,
+ completed_artworks: task.completed,
+ failed_artworks: task.failed,
+ results: results
+ }
+ };
+
+ } catch (error) {
+ const task = this.tasks.get(taskId);
+ if (task) {
+ task.status = 'failed';
+ task.end_time = new Date();
+ }
+
+ return {
+ success: false,
+ error: error.message
+ };
+ }
+ }
+
+ /**
+ * 下载作者作品
+ */
+ async downloadArtistArtworks(artistId, options = {}) {
+ const taskId = uuidv4();
+ const {
+ type = 'art',
+ filter = 'for_ios',
+ size = 'original',
+ quality = 'high',
+ format = 'auto',
+ limit = 50,
+ concurrent = 3
+ } = options;
+
+ try {
+ // 获取作者信息
+ const artistResult = await this.artistService.getArtistInfo(artistId);
+ if (!artistResult.success) {
+ throw new Error(`获取作者信息失败: ${artistResult.error}`);
+ }
+
+ const artist = artistResult.data;
+ const artistName = artist.name.replace(/[<>:"/\\|?*]/g, '_');
+
+ // 获取作者作品列表
+ const artworksResult = await this.artistService.getArtistArtworks(artistId, {
+ type,
+ filter,
+ limit
+ });
+
+ if (!artworksResult.success) {
+ throw new Error(`获取作者作品列表失败: ${artworksResult.error}`);
+ }
+
+ const artworks = artworksResult.data.artworks;
+ const artworkIds = artworks.map(artwork => artwork.id);
+
+ // 创建任务记录
+ this.tasks.set(taskId, {
+ id: taskId,
+ type: 'artist',
+ artist_id: artistId,
+ artist_name: artistName,
+ status: 'downloading',
+ progress: 0,
+ total: artworkIds.length,
+ completed: 0,
+ failed: 0,
+ results: [],
+ start_time: new Date(),
+ end_time: null
+ });
+
+ // 批量下载作品
+ const batchResult = await this.downloadMultipleArtworks(artworkIds, {
+ concurrent,
+ size,
+ quality,
+ format
+ });
+
+ if (batchResult.success) {
+ const task = this.tasks.get(taskId);
+ task.status = batchResult.data.failed_artworks === 0 ? 'completed' : 'partial';
+ task.end_time = new Date();
+ task.results = batchResult.data.results;
+
+ return {
+ success: true,
+ data: {
+ task_id: taskId,
+ artist_id: artistId,
+ artist_name: artistName,
+ total_artworks: batchResult.data.total_artworks,
+ completed_artworks: batchResult.data.completed_artworks,
+ failed_artworks: batchResult.data.failed_artworks,
+ results: batchResult.data.results
+ }
+ };
+ } else {
+ throw new Error(batchResult.error);
+ }
+
+ } catch (error) {
+ const task = this.tasks.get(taskId);
+ if (task) {
+ task.status = 'failed';
+ task.end_time = new Date();
+ }
+
+ return {
+ success: false,
+ error: error.message
+ };
+ }
+ }
+
+ /**
+ * 获取下载进度
+ */
+ async getDownloadProgress(taskId) {
+ const task = this.tasks.get(taskId);
+
+ if (!task) {
+ return {
+ success: false,
+ error: 'Task not found'
+ };
+ }
+
+ return {
+ success: true,
+ data: {
+ id: task.id,
+ type: task.type,
+ status: task.status,
+ progress: task.progress,
+ total: task.total,
+ completed: task.completed,
+ failed: task.failed,
+ start_time: task.start_time,
+ end_time: task.end_time,
+ files: task.files || [],
+ results: task.results || []
+ }
+ };
+ }
+
+ /**
+ * 取消下载任务
+ */
+ async cancelDownload(taskId) {
+ const task = this.tasks.get(taskId);
+
+ if (!task) {
+ return {
+ success: false,
+ error: 'Task not found'
+ };
+ }
+
+ if (task.status === 'completed' || task.status === 'failed') {
+ return {
+ success: false,
+ error: 'Task already finished'
+ };
+ }
+
+ task.status = 'cancelled';
+ task.end_time = new Date();
+
+ return {
+ success: true,
+ message: 'Download task cancelled successfully'
+ };
+ }
+
+ /**
+ * 获取下载历史
+ */
+ async getDownloadHistory(options = {}) {
+ const { offset = 0, limit = 20 } = options;
+
+ try {
+ const tasks = Array.from(this.tasks.values())
+ .filter(task => task.status === 'completed' || task.status === 'partial')
+ .sort((a, b) => b.end_time - a.end_time)
+ .slice(offset, offset + limit);
+
+ return {
+ success: true,
+ data: {
+ tasks: tasks,
+ total: this.tasks.size,
+ offset,
+ limit
+ }
+ };
+
+ } catch (error) {
+ return {
+ success: false,
+ error: error.message
+ };
+ }
+ }
+
+ /**
+ * 下载单个文件
+ */
+ async downloadFile(url, filePath) {
+ const headers = {
+ 'Referer': 'https://app-api.pixiv.net/',
+ 'User-Agent': 'PixivAndroidApp/5.0.234 (Android 9.0; Pixel 3)'
+ };
+
+ const response = await axios({
+ method: 'GET',
+ url: url,
+ headers,
+ responseType: 'stream',
+ timeout: 60000
+ });
+
+ const writer = fs.createWriteStream(filePath);
+ response.data.pipe(writer);
+
+ return new Promise((resolve, reject) => {
+ writer.on('finish', resolve);
+ writer.on('error', reject);
+ });
+ }
+
+ /**
+ * 获取文件扩展名
+ */
+ getFileExtension(url) {
+ const match = url.match(/\.([a-zA-Z0-9]+)(?:\?|$)/);
+ return match ? `.${match[1]}` : '.jpg';
+ }
+}
+
+module.exports = DownloadService;
\ No newline at end of file
diff --git a/backend/start.js b/backend/start.js
new file mode 100644
index 0000000..2fa1f73
--- /dev/null
+++ b/backend/start.js
@@ -0,0 +1,47 @@
+#!/usr/bin/env node
+
+/**
+ * Pixiv 后端服务器启动脚本
+ */
+
+const PixivServer = require('./server');
+
+// 设置环境变量
+process.env.NODE_ENV = process.env.NODE_ENV || 'development';
+
+console.log('🚀 启动 Pixiv 后端服务器...');
+console.log(`📊 环境: ${process.env.NODE_ENV}`);
+console.log(`🌐 端口: ${process.env.PORT || 3000}`);
+
+// 创建服务器实例
+const server = new PixivServer();
+
+// 处理进程信号
+process.on('SIGINT', async () => {
+ console.log('\n🛑 收到 SIGINT 信号,正在关闭服务器...');
+ await server.shutdown();
+});
+
+process.on('SIGTERM', async () => {
+ console.log('\n🛑 收到 SIGTERM 信号,正在关闭服务器...');
+ await server.shutdown();
+});
+
+// 处理未捕获的异常
+process.on('uncaughtException', (error) => {
+ console.error('❌ 未捕获的异常:', error);
+ process.exit(1);
+});
+
+process.on('unhandledRejection', (reason, promise) => {
+ console.error('❌ 未处理的 Promise 拒绝:', reason);
+ process.exit(1);
+});
+
+// 启动服务器
+server.init()
+ .then(() => server.start())
+ .catch((error) => {
+ console.error('❌ 服务器启动失败:', error);
+ process.exit(1);
+ });
\ No newline at end of file
diff --git a/backend/test-login.js b/backend/test-login.js
new file mode 100644
index 0000000..2285277
--- /dev/null
+++ b/backend/test-login.js
@@ -0,0 +1,198 @@
+const PixivBackend = require('./core');
+const proxyConfig = require('./config');
+const readline = require('readline');
+
+// 创建命令行交互接口
+const rl = readline.createInterface({
+ input: process.stdin,
+ output: process.stdout
+});
+
+// 询问用户输入
+function askQuestion(question) {
+ return new Promise((resolve) => {
+ rl.question(question, (answer) => {
+ resolve(answer);
+ });
+ });
+}
+
+// 测试登录流程
+async function testLogin() {
+ console.log('=== Pixiv 登录测试脚本 ===\n');
+
+ try {
+ // 1. 设置代理环境变量
+ console.log('1. 设置代理配置...');
+ proxyConfig.setEnvironmentVariables();
+
+ // 2. 初始化后端
+ console.log('\n2. 初始化 Pixiv 后端...');
+ const backend = new PixivBackend();
+ await backend.init();
+
+ // 3. 检查登录状态
+ console.log('\n3. 检查当前登录状态...');
+ const loginStatus = backend.getLoginStatus();
+ console.log('登录状态:', loginStatus);
+
+ if (loginStatus.isLoggedIn) {
+ console.log('✅ 已登录,用户:', loginStatus.username);
+ return;
+ }
+
+ // 4. 获取登录URL
+ console.log('\n4. 获取登录URL...');
+ const loginData = backend.getLoginUrl();
+ console.log('请访问以下URL进行登录:');
+ console.log(loginData.login_url);
+ console.log('\n登录完成后,请复制回调URL中的code参数');
+
+ // 5. 等待用户输入授权码
+ const code = await askQuestion('\n请输入授权码 (code参数): ');
+
+ if (!code || code.trim() === '') {
+ console.log('❌ 未输入授权码,测试终止');
+ return;
+ }
+
+ // 6. 处理登录回调
+ console.log('\n5. 处理登录回调...');
+ const loginResult = await backend.handleLoginCallback(code.trim());
+
+ if (loginResult.success) {
+ console.log('✅ 登录成功!');
+ console.log('用户信息:', loginResult.user);
+
+ // 7. 再次检查登录状态
+ console.log('\n6. 验证登录状态...');
+ const finalStatus = backend.getLoginStatus();
+ console.log('最终登录状态:', finalStatus);
+
+ // 8. 测试获取用户信息
+ console.log('\n7. 测试获取用户信息...');
+ const auth = backend.getAuth();
+ const userInfo = await auth.getUserInfo();
+
+ if (userInfo.success) {
+ console.log('✅ 获取用户信息成功:', userInfo.user);
+ } else {
+ console.log('❌ 获取用户信息失败:', userInfo.error);
+ }
+
+ } else {
+ console.log('❌ 登录失败:', loginResult.error);
+ }
+
+ } catch (error) {
+ console.error('❌ 测试过程中发生错误:', error.message);
+ console.error('错误详情:', error);
+ } finally {
+ // 清理资源
+ rl.close();
+ console.log('\n=== 测试完成 ===');
+ }
+}
+
+// 测试重新登录功能
+async function testRelogin() {
+ console.log('=== 测试重新登录功能 ===\n');
+
+ try {
+ // 设置代理
+ proxyConfig.setEnvironmentVariables();
+
+ // 初始化后端
+ const backend = new PixivBackend();
+ await backend.init();
+
+ // 检查是否有保存的登录信息
+ const loginStatus = backend.getLoginStatus();
+
+ if (loginStatus.isLoggedIn) {
+ console.log('✅ 检测到已保存的登录信息');
+ console.log('用户:', loginStatus.username);
+ console.log('用户ID:', loginStatus.user_id);
+ } else {
+ console.log('❌ 没有保存的登录信息,无法测试重新登录');
+ }
+
+ } catch (error) {
+ console.error('❌ 重新登录测试失败:', error.message);
+ }
+}
+
+// 测试登出功能
+async function testLogout() {
+ console.log('=== 测试登出功能 ===\n');
+
+ try {
+ // 设置代理
+ proxyConfig.setEnvironmentVariables();
+
+ // 初始化后端
+ const backend = new PixivBackend();
+ await backend.init();
+
+ // 执行登出
+ const logoutResult = backend.logout();
+
+ if (logoutResult.success) {
+ console.log('✅ 登出成功');
+
+ // 验证登出状态
+ const loginStatus = backend.getLoginStatus();
+ console.log('登出后状态:', loginStatus);
+ } else {
+ console.log('❌ 登出失败');
+ }
+
+ } catch (error) {
+ console.error('❌ 登出测试失败:', error.message);
+ }
+}
+
+// 主函数
+async function main() {
+ console.log('请选择测试功能:');
+ console.log('1. 测试完整登录流程');
+ console.log('2. 测试重新登录');
+ console.log('3. 测试登出');
+ console.log('4. 运行所有测试');
+
+ const choice = await askQuestion('\n请输入选择 (1-4): ');
+
+ switch (choice.trim()) {
+ case '1':
+ await testLogin();
+ break;
+ case '2':
+ await testRelogin();
+ break;
+ case '3':
+ await testLogout();
+ break;
+ case '4':
+ console.log('\n=== 运行所有测试 ===\n');
+ await testLogin();
+ console.log('\n' + '='.repeat(50) + '\n');
+ await testRelogin();
+ console.log('\n' + '='.repeat(50) + '\n');
+ await testLogout();
+ break;
+ default:
+ console.log('❌ 无效选择');
+ rl.close();
+ }
+}
+
+// 如果直接运行此脚本
+if (require.main === module) {
+ main().catch(console.error);
+}
+
+module.exports = {
+ testLogin,
+ testRelogin,
+ testLogout
+};
\ No newline at end of file
diff --git a/backend/utils/response.js b/backend/utils/response.js
new file mode 100644
index 0000000..b365a59
--- /dev/null
+++ b/backend/utils/response.js
@@ -0,0 +1,60 @@
+/**
+ * 统一API响应格式工具类
+ */
+class ResponseUtil {
+ /**
+ * 成功响应
+ */
+ static success(data = null, message = 'Success') {
+ return {
+ success: true,
+ message,
+ data,
+ timestamp: new Date().toISOString()
+ };
+ }
+
+ /**
+ * 错误响应
+ */
+ static error(message = 'Error', code = null, details = null) {
+ return {
+ success: false,
+ message,
+ code,
+ details,
+ timestamp: new Date().toISOString()
+ };
+ }
+
+ /**
+ * 分页响应
+ */
+ static paginated(data, page, limit, total) {
+ return {
+ success: true,
+ data,
+ pagination: {
+ page: parseInt(page),
+ limit: parseInt(limit),
+ total,
+ pages: Math.ceil(total / limit)
+ },
+ timestamp: new Date().toISOString()
+ };
+ }
+
+ /**
+ * 列表响应
+ */
+ static list(data, total = null) {
+ return {
+ success: true,
+ data,
+ total: total || (Array.isArray(data) ? data.length : 0),
+ timestamp: new Date().toISOString()
+ };
+ }
+}
+
+module.exports = ResponseUtil;
\ No newline at end of file
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..1482389
--- /dev/null
+++ b/package.json
@@ -0,0 +1,41 @@
+{
+ "name": "pixiv-backend",
+ "version": "1.0.0",
+ "description": "Pixiv 后端服务 - 支持 OAuth 2.0 登录和 API 调用",
+ "main": "backend/start.js",
+ "scripts": {
+ "start": "node backend/start.js",
+ "dev": "node backend/start.js",
+ "test": "node backend/test-login.js",
+ "set-proxy": "node backend/set-proxy.js"
+ },
+ "dependencies": {
+ "appdata-path": "^1.0.0",
+ "axios": "^1.11.0",
+ "cors": "^2.8.5",
+ "crypto": "^1.0.1",
+ "express": "^5.1.0",
+ "fs-extra": "^11.3.1",
+ "js-base64": "^3.7.8",
+ "moment": "^2.30.1",
+ "morgan": "^1.10.1",
+ "proxy-agent": "^6.5.0",
+ "qs": "^6.14.0",
+ "uuid": "^11.1.0"
+ },
+ "devDependencies": {
+ "readline-sync": "^1.4.10"
+ },
+ "keywords": [
+ "pixiv",
+ "oauth",
+ "api",
+ "backend",
+ "download"
+ ],
+ "author": "Your Name",
+ "license": "MIT",
+ "engines": {
+ "node": ">=16.0.0"
+ }
+}
\ No newline at end of file
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
new file mode 100644
index 0000000..d73db40
--- /dev/null
+++ b/pnpm-lock.yaml
@@ -0,0 +1,1047 @@
+lockfileVersion: '9.0'
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+
+importers:
+
+ .:
+ dependencies:
+ appdata-path:
+ specifier: ^1.0.0
+ version: 1.0.0
+ axios:
+ specifier: ^1.11.0
+ version: 1.11.0
+ cors:
+ specifier: ^2.8.5
+ version: 2.8.5
+ crypto:
+ specifier: ^1.0.1
+ version: 1.0.1
+ express:
+ specifier: ^5.1.0
+ version: 5.1.0
+ fs-extra:
+ specifier: ^11.3.1
+ version: 11.3.1
+ js-base64:
+ specifier: ^3.7.8
+ version: 3.7.8
+ moment:
+ specifier: ^2.30.1
+ version: 2.30.1
+ morgan:
+ specifier: ^1.10.1
+ version: 1.10.1
+ proxy-agent:
+ specifier: ^6.5.0
+ version: 6.5.0
+ qs:
+ specifier: ^6.14.0
+ version: 6.14.0
+ uuid:
+ specifier: ^11.1.0
+ version: 11.1.0
+ devDependencies:
+ readline-sync:
+ specifier: ^1.4.10
+ version: 1.4.10
+
+packages:
+
+ '@tootallnate/quickjs-emscripten@0.23.0':
+ resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==}
+
+ accepts@2.0.0:
+ resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==}
+ engines: {node: '>= 0.6'}
+
+ agent-base@7.1.4:
+ resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
+ engines: {node: '>= 14'}
+
+ appdata-path@1.0.0:
+ resolution: {integrity: sha512-ZbH3ezXfnT/YE3NdqduIt4lBV+H0ybvA2Qx3K76gIjQvh8gROpDFdDLpx6B1QJtW7zxisCbpTlCLhKqoR8cDBw==}
+
+ ast-types@0.13.4:
+ resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==}
+ engines: {node: '>=4'}
+
+ asynckit@0.4.0:
+ resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
+
+ axios@1.11.0:
+ resolution: {integrity: sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==}
+
+ basic-auth@2.0.1:
+ resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==}
+ engines: {node: '>= 0.8'}
+
+ basic-ftp@5.0.5:
+ resolution: {integrity: sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==}
+ engines: {node: '>=10.0.0'}
+
+ body-parser@2.2.0:
+ resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==}
+ engines: {node: '>=18'}
+
+ bytes@3.1.2:
+ resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
+ engines: {node: '>= 0.8'}
+
+ call-bind-apply-helpers@1.0.2:
+ resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
+ engines: {node: '>= 0.4'}
+
+ call-bound@1.0.4:
+ resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==}
+ engines: {node: '>= 0.4'}
+
+ combined-stream@1.0.8:
+ resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
+ engines: {node: '>= 0.8'}
+
+ content-disposition@1.0.0:
+ resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==}
+ engines: {node: '>= 0.6'}
+
+ content-type@1.0.5:
+ resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==}
+ engines: {node: '>= 0.6'}
+
+ cookie-signature@1.2.2:
+ resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==}
+ engines: {node: '>=6.6.0'}
+
+ cookie@0.7.2:
+ resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
+ engines: {node: '>= 0.6'}
+
+ cors@2.8.5:
+ resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==}
+ engines: {node: '>= 0.10'}
+
+ crypto@1.0.1:
+ resolution: {integrity: sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==}
+ deprecated: This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in.
+
+ data-uri-to-buffer@6.0.2:
+ resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==}
+ engines: {node: '>= 14'}
+
+ debug@2.6.9:
+ resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+
+ debug@4.4.1:
+ resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
+ engines: {node: '>=6.0'}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+
+ degenerator@5.0.1:
+ resolution: {integrity: sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==}
+ engines: {node: '>= 14'}
+
+ delayed-stream@1.0.0:
+ resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
+ engines: {node: '>=0.4.0'}
+
+ depd@2.0.0:
+ resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
+ engines: {node: '>= 0.8'}
+
+ dunder-proto@1.0.1:
+ resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
+ engines: {node: '>= 0.4'}
+
+ ee-first@1.1.1:
+ resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
+
+ encodeurl@2.0.0:
+ resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
+ engines: {node: '>= 0.8'}
+
+ 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'}
+
+ escape-html@1.0.3:
+ resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
+
+ escodegen@2.1.0:
+ resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==}
+ engines: {node: '>=6.0'}
+ hasBin: true
+
+ esprima@4.0.1:
+ resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==}
+ engines: {node: '>=4'}
+ hasBin: true
+
+ estraverse@5.3.0:
+ resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
+ engines: {node: '>=4.0'}
+
+ esutils@2.0.3:
+ resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
+ engines: {node: '>=0.10.0'}
+
+ etag@1.8.1:
+ resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
+ engines: {node: '>= 0.6'}
+
+ express@5.1.0:
+ resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==}
+ engines: {node: '>= 18'}
+
+ finalhandler@2.1.0:
+ resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==}
+ engines: {node: '>= 0.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.4:
+ resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==}
+ engines: {node: '>= 6'}
+
+ forwarded@0.2.0:
+ resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
+ engines: {node: '>= 0.6'}
+
+ fresh@2.0.0:
+ resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==}
+ engines: {node: '>= 0.8'}
+
+ fs-extra@11.3.1:
+ resolution: {integrity: sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==}
+ engines: {node: '>=14.14'}
+
+ 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'}
+
+ get-uri@6.0.5:
+ resolution: {integrity: sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==}
+ engines: {node: '>= 14'}
+
+ gopd@1.2.0:
+ resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
+ engines: {node: '>= 0.4'}
+
+ graceful-fs@4.2.11:
+ resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
+
+ 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'}
+
+ http-errors@2.0.0:
+ resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
+ engines: {node: '>= 0.8'}
+
+ http-proxy-agent@7.0.2:
+ resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==}
+ engines: {node: '>= 14'}
+
+ https-proxy-agent@7.0.6:
+ resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
+ engines: {node: '>= 14'}
+
+ iconv-lite@0.6.3:
+ resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
+ engines: {node: '>=0.10.0'}
+
+ inherits@2.0.4:
+ resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
+
+ ip-address@10.0.1:
+ resolution: {integrity: sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==}
+ engines: {node: '>= 12'}
+
+ ipaddr.js@1.9.1:
+ resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
+ engines: {node: '>= 0.10'}
+
+ is-promise@4.0.0:
+ resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==}
+
+ js-base64@3.7.8:
+ resolution: {integrity: sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==}
+
+ jsonfile@6.2.0:
+ resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==}
+
+ lru-cache@7.18.3:
+ resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==}
+ engines: {node: '>=12'}
+
+ math-intrinsics@1.1.0:
+ resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
+ engines: {node: '>= 0.4'}
+
+ media-typer@1.1.0:
+ resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==}
+ engines: {node: '>= 0.8'}
+
+ merge-descriptors@2.0.0:
+ resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==}
+ engines: {node: '>=18'}
+
+ mime-db@1.52.0:
+ resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
+ engines: {node: '>= 0.6'}
+
+ mime-db@1.54.0:
+ resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==}
+ engines: {node: '>= 0.6'}
+
+ mime-types@2.1.35:
+ resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
+ engines: {node: '>= 0.6'}
+
+ mime-types@3.0.1:
+ resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==}
+ engines: {node: '>= 0.6'}
+
+ moment@2.30.1:
+ resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==}
+
+ morgan@1.10.1:
+ resolution: {integrity: sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==}
+ engines: {node: '>= 0.8.0'}
+
+ ms@2.0.0:
+ resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
+
+ ms@2.1.3:
+ resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
+
+ negotiator@1.0.0:
+ resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
+ engines: {node: '>= 0.6'}
+
+ netmask@2.0.2:
+ resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==}
+ engines: {node: '>= 0.4.0'}
+
+ object-assign@4.1.1:
+ resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
+ engines: {node: '>=0.10.0'}
+
+ object-inspect@1.13.4:
+ resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
+ engines: {node: '>= 0.4'}
+
+ on-finished@2.3.0:
+ resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==}
+ engines: {node: '>= 0.8'}
+
+ on-finished@2.4.1:
+ resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
+ engines: {node: '>= 0.8'}
+
+ on-headers@1.1.0:
+ resolution: {integrity: sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==}
+ engines: {node: '>= 0.8'}
+
+ once@1.4.0:
+ resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
+
+ pac-proxy-agent@7.2.0:
+ resolution: {integrity: sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==}
+ engines: {node: '>= 14'}
+
+ pac-resolver@7.0.1:
+ resolution: {integrity: sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==}
+ engines: {node: '>= 14'}
+
+ parseurl@1.3.3:
+ resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
+ engines: {node: '>= 0.8'}
+
+ path-to-regexp@8.2.0:
+ resolution: {integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==}
+ engines: {node: '>=16'}
+
+ proxy-addr@2.0.7:
+ resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
+ engines: {node: '>= 0.10'}
+
+ proxy-agent@6.5.0:
+ resolution: {integrity: sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==}
+ engines: {node: '>= 14'}
+
+ proxy-from-env@1.1.0:
+ resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
+
+ qs@6.14.0:
+ resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==}
+ engines: {node: '>=0.6'}
+
+ range-parser@1.2.1:
+ resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
+ engines: {node: '>= 0.6'}
+
+ raw-body@3.0.0:
+ resolution: {integrity: sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==}
+ engines: {node: '>= 0.8'}
+
+ readline-sync@1.4.10:
+ resolution: {integrity: sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw==}
+ engines: {node: '>= 0.8.0'}
+
+ router@2.2.0:
+ resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==}
+ engines: {node: '>= 18'}
+
+ safe-buffer@5.1.2:
+ resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
+
+ safe-buffer@5.2.1:
+ resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
+
+ safer-buffer@2.1.2:
+ resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
+
+ send@1.2.0:
+ resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==}
+ engines: {node: '>= 18'}
+
+ serve-static@2.2.0:
+ resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==}
+ engines: {node: '>= 18'}
+
+ setprototypeof@1.2.0:
+ resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
+
+ side-channel-list@1.0.0:
+ resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==}
+ engines: {node: '>= 0.4'}
+
+ side-channel-map@1.0.1:
+ resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==}
+ engines: {node: '>= 0.4'}
+
+ side-channel-weakmap@1.0.2:
+ resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==}
+ engines: {node: '>= 0.4'}
+
+ side-channel@1.1.0:
+ resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==}
+ engines: {node: '>= 0.4'}
+
+ smart-buffer@4.2.0:
+ resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==}
+ engines: {node: '>= 6.0.0', npm: '>= 3.0.0'}
+
+ socks-proxy-agent@8.0.5:
+ resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==}
+ engines: {node: '>= 14'}
+
+ socks@2.8.7:
+ resolution: {integrity: sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==}
+ engines: {node: '>= 10.0.0', npm: '>= 3.0.0'}
+
+ source-map@0.6.1:
+ resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
+ engines: {node: '>=0.10.0'}
+
+ statuses@2.0.1:
+ resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
+ engines: {node: '>= 0.8'}
+
+ statuses@2.0.2:
+ resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
+ engines: {node: '>= 0.8'}
+
+ toidentifier@1.0.1:
+ resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
+ engines: {node: '>=0.6'}
+
+ tslib@2.8.1:
+ resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
+
+ type-is@2.0.1:
+ resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==}
+ engines: {node: '>= 0.6'}
+
+ universalify@2.0.1:
+ resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
+ engines: {node: '>= 10.0.0'}
+
+ unpipe@1.0.0:
+ resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
+ engines: {node: '>= 0.8'}
+
+ uuid@11.1.0:
+ resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==}
+ hasBin: true
+
+ vary@1.1.2:
+ resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
+ engines: {node: '>= 0.8'}
+
+ wrappy@1.0.2:
+ resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
+
+snapshots:
+
+ '@tootallnate/quickjs-emscripten@0.23.0': {}
+
+ accepts@2.0.0:
+ dependencies:
+ mime-types: 3.0.1
+ negotiator: 1.0.0
+
+ agent-base@7.1.4: {}
+
+ appdata-path@1.0.0: {}
+
+ ast-types@0.13.4:
+ dependencies:
+ tslib: 2.8.1
+
+ asynckit@0.4.0: {}
+
+ axios@1.11.0:
+ dependencies:
+ follow-redirects: 1.15.11
+ form-data: 4.0.4
+ proxy-from-env: 1.1.0
+ transitivePeerDependencies:
+ - debug
+
+ basic-auth@2.0.1:
+ dependencies:
+ safe-buffer: 5.1.2
+
+ basic-ftp@5.0.5: {}
+
+ body-parser@2.2.0:
+ dependencies:
+ bytes: 3.1.2
+ content-type: 1.0.5
+ debug: 4.4.1
+ http-errors: 2.0.0
+ iconv-lite: 0.6.3
+ on-finished: 2.4.1
+ qs: 6.14.0
+ raw-body: 3.0.0
+ type-is: 2.0.1
+ transitivePeerDependencies:
+ - supports-color
+
+ bytes@3.1.2: {}
+
+ call-bind-apply-helpers@1.0.2:
+ dependencies:
+ es-errors: 1.3.0
+ function-bind: 1.1.2
+
+ call-bound@1.0.4:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ get-intrinsic: 1.3.0
+
+ combined-stream@1.0.8:
+ dependencies:
+ delayed-stream: 1.0.0
+
+ content-disposition@1.0.0:
+ dependencies:
+ safe-buffer: 5.2.1
+
+ content-type@1.0.5: {}
+
+ cookie-signature@1.2.2: {}
+
+ cookie@0.7.2: {}
+
+ cors@2.8.5:
+ dependencies:
+ object-assign: 4.1.1
+ vary: 1.1.2
+
+ crypto@1.0.1: {}
+
+ data-uri-to-buffer@6.0.2: {}
+
+ debug@2.6.9:
+ dependencies:
+ ms: 2.0.0
+
+ debug@4.4.1:
+ dependencies:
+ ms: 2.1.3
+
+ degenerator@5.0.1:
+ dependencies:
+ ast-types: 0.13.4
+ escodegen: 2.1.0
+ esprima: 4.0.1
+
+ delayed-stream@1.0.0: {}
+
+ depd@2.0.0: {}
+
+ dunder-proto@1.0.1:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ es-errors: 1.3.0
+ gopd: 1.2.0
+
+ ee-first@1.1.1: {}
+
+ encodeurl@2.0.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
+
+ escape-html@1.0.3: {}
+
+ escodegen@2.1.0:
+ dependencies:
+ esprima: 4.0.1
+ estraverse: 5.3.0
+ esutils: 2.0.3
+ optionalDependencies:
+ source-map: 0.6.1
+
+ esprima@4.0.1: {}
+
+ estraverse@5.3.0: {}
+
+ esutils@2.0.3: {}
+
+ etag@1.8.1: {}
+
+ express@5.1.0:
+ dependencies:
+ accepts: 2.0.0
+ body-parser: 2.2.0
+ content-disposition: 1.0.0
+ content-type: 1.0.5
+ cookie: 0.7.2
+ cookie-signature: 1.2.2
+ debug: 4.4.1
+ encodeurl: 2.0.0
+ escape-html: 1.0.3
+ etag: 1.8.1
+ finalhandler: 2.1.0
+ fresh: 2.0.0
+ http-errors: 2.0.0
+ merge-descriptors: 2.0.0
+ mime-types: 3.0.1
+ on-finished: 2.4.1
+ once: 1.4.0
+ parseurl: 1.3.3
+ proxy-addr: 2.0.7
+ qs: 6.14.0
+ range-parser: 1.2.1
+ router: 2.2.0
+ send: 1.2.0
+ serve-static: 2.2.0
+ statuses: 2.0.2
+ type-is: 2.0.1
+ vary: 1.1.2
+ transitivePeerDependencies:
+ - supports-color
+
+ finalhandler@2.1.0:
+ dependencies:
+ debug: 4.4.1
+ encodeurl: 2.0.0
+ escape-html: 1.0.3
+ on-finished: 2.4.1
+ parseurl: 1.3.3
+ statuses: 2.0.2
+ transitivePeerDependencies:
+ - supports-color
+
+ follow-redirects@1.15.11: {}
+
+ form-data@4.0.4:
+ 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
+
+ forwarded@0.2.0: {}
+
+ fresh@2.0.0: {}
+
+ fs-extra@11.3.1:
+ dependencies:
+ graceful-fs: 4.2.11
+ jsonfile: 6.2.0
+ universalify: 2.0.1
+
+ 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
+
+ get-uri@6.0.5:
+ dependencies:
+ basic-ftp: 5.0.5
+ data-uri-to-buffer: 6.0.2
+ debug: 4.4.1
+ transitivePeerDependencies:
+ - supports-color
+
+ gopd@1.2.0: {}
+
+ graceful-fs@4.2.11: {}
+
+ 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
+
+ http-errors@2.0.0:
+ dependencies:
+ depd: 2.0.0
+ inherits: 2.0.4
+ setprototypeof: 1.2.0
+ statuses: 2.0.1
+ toidentifier: 1.0.1
+
+ http-proxy-agent@7.0.2:
+ dependencies:
+ agent-base: 7.1.4
+ debug: 4.4.1
+ transitivePeerDependencies:
+ - supports-color
+
+ https-proxy-agent@7.0.6:
+ dependencies:
+ agent-base: 7.1.4
+ debug: 4.4.1
+ transitivePeerDependencies:
+ - supports-color
+
+ iconv-lite@0.6.3:
+ dependencies:
+ safer-buffer: 2.1.2
+
+ inherits@2.0.4: {}
+
+ ip-address@10.0.1: {}
+
+ ipaddr.js@1.9.1: {}
+
+ is-promise@4.0.0: {}
+
+ js-base64@3.7.8: {}
+
+ jsonfile@6.2.0:
+ dependencies:
+ universalify: 2.0.1
+ optionalDependencies:
+ graceful-fs: 4.2.11
+
+ lru-cache@7.18.3: {}
+
+ math-intrinsics@1.1.0: {}
+
+ media-typer@1.1.0: {}
+
+ merge-descriptors@2.0.0: {}
+
+ mime-db@1.52.0: {}
+
+ mime-db@1.54.0: {}
+
+ mime-types@2.1.35:
+ dependencies:
+ mime-db: 1.52.0
+
+ mime-types@3.0.1:
+ dependencies:
+ mime-db: 1.54.0
+
+ moment@2.30.1: {}
+
+ morgan@1.10.1:
+ dependencies:
+ basic-auth: 2.0.1
+ debug: 2.6.9
+ depd: 2.0.0
+ on-finished: 2.3.0
+ on-headers: 1.1.0
+ transitivePeerDependencies:
+ - supports-color
+
+ ms@2.0.0: {}
+
+ ms@2.1.3: {}
+
+ negotiator@1.0.0: {}
+
+ netmask@2.0.2: {}
+
+ object-assign@4.1.1: {}
+
+ object-inspect@1.13.4: {}
+
+ on-finished@2.3.0:
+ dependencies:
+ ee-first: 1.1.1
+
+ on-finished@2.4.1:
+ dependencies:
+ ee-first: 1.1.1
+
+ on-headers@1.1.0: {}
+
+ once@1.4.0:
+ dependencies:
+ wrappy: 1.0.2
+
+ pac-proxy-agent@7.2.0:
+ dependencies:
+ '@tootallnate/quickjs-emscripten': 0.23.0
+ agent-base: 7.1.4
+ debug: 4.4.1
+ get-uri: 6.0.5
+ http-proxy-agent: 7.0.2
+ https-proxy-agent: 7.0.6
+ pac-resolver: 7.0.1
+ socks-proxy-agent: 8.0.5
+ transitivePeerDependencies:
+ - supports-color
+
+ pac-resolver@7.0.1:
+ dependencies:
+ degenerator: 5.0.1
+ netmask: 2.0.2
+
+ parseurl@1.3.3: {}
+
+ path-to-regexp@8.2.0: {}
+
+ proxy-addr@2.0.7:
+ dependencies:
+ forwarded: 0.2.0
+ ipaddr.js: 1.9.1
+
+ proxy-agent@6.5.0:
+ dependencies:
+ agent-base: 7.1.4
+ debug: 4.4.1
+ http-proxy-agent: 7.0.2
+ https-proxy-agent: 7.0.6
+ lru-cache: 7.18.3
+ pac-proxy-agent: 7.2.0
+ proxy-from-env: 1.1.0
+ socks-proxy-agent: 8.0.5
+ transitivePeerDependencies:
+ - supports-color
+
+ proxy-from-env@1.1.0: {}
+
+ qs@6.14.0:
+ dependencies:
+ side-channel: 1.1.0
+
+ range-parser@1.2.1: {}
+
+ raw-body@3.0.0:
+ dependencies:
+ bytes: 3.1.2
+ http-errors: 2.0.0
+ iconv-lite: 0.6.3
+ unpipe: 1.0.0
+
+ readline-sync@1.4.10: {}
+
+ router@2.2.0:
+ dependencies:
+ debug: 4.4.1
+ depd: 2.0.0
+ is-promise: 4.0.0
+ parseurl: 1.3.3
+ path-to-regexp: 8.2.0
+ transitivePeerDependencies:
+ - supports-color
+
+ safe-buffer@5.1.2: {}
+
+ safe-buffer@5.2.1: {}
+
+ safer-buffer@2.1.2: {}
+
+ send@1.2.0:
+ dependencies:
+ debug: 4.4.1
+ encodeurl: 2.0.0
+ escape-html: 1.0.3
+ etag: 1.8.1
+ fresh: 2.0.0
+ http-errors: 2.0.0
+ mime-types: 3.0.1
+ ms: 2.1.3
+ on-finished: 2.4.1
+ range-parser: 1.2.1
+ statuses: 2.0.2
+ transitivePeerDependencies:
+ - supports-color
+
+ serve-static@2.2.0:
+ dependencies:
+ encodeurl: 2.0.0
+ escape-html: 1.0.3
+ parseurl: 1.3.3
+ send: 1.2.0
+ transitivePeerDependencies:
+ - supports-color
+
+ setprototypeof@1.2.0: {}
+
+ side-channel-list@1.0.0:
+ dependencies:
+ es-errors: 1.3.0
+ object-inspect: 1.13.4
+
+ side-channel-map@1.0.1:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ get-intrinsic: 1.3.0
+ object-inspect: 1.13.4
+
+ side-channel-weakmap@1.0.2:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ get-intrinsic: 1.3.0
+ object-inspect: 1.13.4
+ side-channel-map: 1.0.1
+
+ side-channel@1.1.0:
+ dependencies:
+ es-errors: 1.3.0
+ object-inspect: 1.13.4
+ side-channel-list: 1.0.0
+ side-channel-map: 1.0.1
+ side-channel-weakmap: 1.0.2
+
+ smart-buffer@4.2.0: {}
+
+ socks-proxy-agent@8.0.5:
+ dependencies:
+ agent-base: 7.1.4
+ debug: 4.4.1
+ socks: 2.8.7
+ transitivePeerDependencies:
+ - supports-color
+
+ socks@2.8.7:
+ dependencies:
+ ip-address: 10.0.1
+ smart-buffer: 4.2.0
+
+ source-map@0.6.1:
+ optional: true
+
+ statuses@2.0.1: {}
+
+ statuses@2.0.2: {}
+
+ toidentifier@1.0.1: {}
+
+ tslib@2.8.1: {}
+
+ type-is@2.0.1:
+ dependencies:
+ content-type: 1.0.5
+ media-typer: 1.1.0
+ mime-types: 3.0.1
+
+ universalify@2.0.1: {}
+
+ unpipe@1.0.0: {}
+
+ uuid@11.1.0: {}
+
+ vary@1.1.2: {}
+
+ wrappy@1.0.2: {}
diff --git a/ui/.editorconfig b/ui/.editorconfig
new file mode 100644
index 0000000..3b510aa
--- /dev/null
+++ b/ui/.editorconfig
@@ -0,0 +1,8 @@
+[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue,css,scss,sass,less,styl}]
+charset = utf-8
+indent_size = 2
+indent_style = space
+insert_final_newline = true
+trim_trailing_whitespace = true
+end_of_line = lf
+max_line_length = 100
diff --git a/ui/.gitattributes b/ui/.gitattributes
new file mode 100644
index 0000000..6313b56
--- /dev/null
+++ b/ui/.gitattributes
@@ -0,0 +1 @@
+* text=auto eol=lf
diff --git a/ui/.gitignore b/ui/.gitignore
new file mode 100644
index 0000000..8ee54e8
--- /dev/null
+++ b/ui/.gitignore
@@ -0,0 +1,30 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+.DS_Store
+dist
+dist-ssr
+coverage
+*.local
+
+/cypress/videos/
+/cypress/screenshots/
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+*.tsbuildinfo
diff --git a/ui/.vscode/extensions.json b/ui/.vscode/extensions.json
new file mode 100644
index 0000000..5efa012
--- /dev/null
+++ b/ui/.vscode/extensions.json
@@ -0,0 +1,7 @@
+{
+ "recommendations": [
+ "Vue.volar",
+ "dbaeumer.vscode-eslint",
+ "EditorConfig.EditorConfig"
+ ]
+}
diff --git a/ui/README.md b/ui/README.md
new file mode 100644
index 0000000..29feda4
--- /dev/null
+++ b/ui/README.md
@@ -0,0 +1,182 @@
+# Pixiv Manager Frontend
+
+一个优雅的 Pixiv 作品管理前端应用,基于 Vue 3 + TypeScript + Vite + Pinia 构建。
+
+## 功能特性
+
+- 🎨 **作品搜索与浏览** - 支持关键词搜索、标签筛选、排序等功能
+- 👨🎨 **作者管理** - 关注喜欢的作者,查看作品列表和统计信息
+- 📥 **下载管理** - 支持单个作品、批量作品、作者作品下载
+- 🔐 **用户认证** - 基于 Pixiv OAuth 2.0 的安全登录系统
+- 📱 **响应式设计** - 完美适配桌面端和移动端
+- ⚡ **现代化技术栈** - Vue 3 + TypeScript + Vite + Pinia
+
+## 技术栈
+
+- **框架**: Vue 3 (Composition API)
+- **语言**: TypeScript
+- **构建工具**: Vite
+- **状态管理**: Pinia
+- **路由**: Vue Router 4
+- **HTTP 客户端**: Axios
+- **样式**: CSS3 + 响应式设计
+
+## 项目结构
+
+```
+src/
+├── components/ # 组件目录
+│ ├── common/ # 通用组件
+│ │ ├── LoadingSpinner.vue
+│ │ └── ErrorMessage.vue
+│ └── artwork/ # 作品相关组件
+│ └── ArtworkCard.vue
+├── views/ # 页面组件
+│ ├── HomeView.vue # 首页
+│ ├── LoginView.vue # 登录页
+│ ├── SearchView.vue # 搜索页
+│ ├── ArtworkView.vue # 作品详情页
+│ ├── ArtistView.vue # 作者详情页
+│ ├── DownloadsView.vue # 下载管理页
+│ └── ArtistsView.vue # 作者管理页
+├── services/ # API 服务层
+│ ├── api.ts # 基础 API 服务
+│ ├── auth.ts # 认证相关 API
+│ ├── artwork.ts # 作品相关 API
+│ ├── artist.ts # 作者相关 API
+│ └── download.ts # 下载相关 API
+├── stores/ # Pinia 状态管理
+│ └── auth.ts # 认证状态管理
+├── types/ # TypeScript 类型定义
+│ └── index.ts # 全局类型定义
+├── router/ # 路由配置
+│ └── index.ts # 路由定义
+├── assets/ # 静态资源
+│ └── main.css # 全局样式
+├── App.vue # 根组件
+└── main.ts # 应用入口
+```
+
+## 开发指南
+
+### 环境要求
+
+- Node.js >= 16
+- pnpm >= 7
+
+### 安装依赖
+
+```bash
+pnpm install
+```
+
+### 开发模式
+
+```bash
+pnpm dev
+```
+
+### 构建生产版本
+
+```bash
+pnpm build
+```
+
+### 代码检查
+
+```bash
+pnpm lint
+```
+
+## 设计原则
+
+### 组件设计
+- **单一职责**: 每个组件只负责一个特定功能
+- **可复用性**: 组件设计时考虑复用性,避免过度耦合
+- **类型安全**: 全面使用 TypeScript 确保类型安全
+
+### 状态管理
+- **集中管理**: 使用 Pinia 进行集中状态管理
+- **响应式**: 状态变化自动触发 UI 更新
+- **持久化**: 关键状态支持本地持久化
+
+### API 设计
+- **服务层**: 所有 API 调用封装在服务层
+- **错误处理**: 统一的错误处理机制
+- **类型安全**: API 响应类型定义完整
+
+### 样式设计
+- **响应式**: 支持多种屏幕尺寸
+- **一致性**: 统一的设计语言和组件库
+- **可维护性**: 模块化的 CSS 结构
+
+## 主要功能模块
+
+### 1. 认证模块
+- Pixiv OAuth 2.0 登录
+- 登录状态管理
+- 自动刷新 Token
+
+### 2. 作品搜索
+- 关键词搜索
+- 标签筛选
+- 排序选项
+- 分页加载
+
+### 3. 作品详情
+- 作品信息展示
+- 多页作品支持
+- 下载功能
+- 作者信息
+
+### 4. 作者管理
+- 作者信息展示
+- 作品列表
+- 关注/取消关注
+- 批量下载
+
+### 5. 下载管理
+- 下载任务创建
+- 进度跟踪
+- 历史记录
+- 任务取消
+
+## 部署说明
+
+### 环境变量
+
+创建 `.env` 文件:
+
+```env
+VITE_API_BASE_URL=http://localhost:3000
+```
+
+### 构建部署
+
+```bash
+# 构建生产版本
+pnpm build
+
+# 部署到静态服务器
+# dist/ 目录包含所有静态文件
+```
+
+## 贡献指南
+
+1. Fork 项目
+2. 创建功能分支 (`git checkout -b feature/AmazingFeature`)
+3. 提交更改 (`git commit -m 'Add some AmazingFeature'`)
+4. 推送到分支 (`git push origin feature/AmazingFeature`)
+5. 打开 Pull Request
+
+## 许可证
+
+本项目仅供学习和个人使用,请遵守 Pixiv 的服务条款。
+
+## 更新日志
+
+### v1.0.0
+- 初始版本发布
+- 基础功能实现
+- 响应式设计
+- TypeScript 支持
diff --git a/ui/env.d.ts b/ui/env.d.ts
new file mode 100644
index 0000000..11f02fe
--- /dev/null
+++ b/ui/env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/ui/eslint.config.ts b/ui/eslint.config.ts
new file mode 100644
index 0000000..edb1bbb
--- /dev/null
+++ b/ui/eslint.config.ts
@@ -0,0 +1,20 @@
+import { globalIgnores } from 'eslint/config'
+import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript'
+import pluginVue from 'eslint-plugin-vue'
+
+// To allow more languages other than `ts` in `.vue` files, uncomment the following lines:
+// import { configureVueProject } from '@vue/eslint-config-typescript'
+// configureVueProject({ scriptLangs: ['ts', 'tsx'] })
+// More info at https://github.com/vuejs/eslint-config-typescript/#advanced-setup
+
+export default defineConfigWithVueTs(
+ {
+ name: 'app/files-to-lint',
+ files: ['**/*.{ts,mts,tsx,vue}'],
+ },
+
+ globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**']),
+
+ pluginVue.configs['flat/essential'],
+ vueTsConfigs.recommended,
+)
diff --git a/ui/index.html b/ui/index.html
new file mode 100644
index 0000000..9e5fc8f
--- /dev/null
+++ b/ui/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ Vite App
+
+
+
+
+
+
diff --git a/ui/package.json b/ui/package.json
new file mode 100644
index 0000000..ee7b163
--- /dev/null
+++ b/ui/package.json
@@ -0,0 +1,38 @@
+{
+ "name": "pixivdownload",
+ "version": "0.0.0",
+ "private": true,
+ "type": "module",
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "scripts": {
+ "dev": "vite",
+ "build": "run-p type-check \"build-only {@}\" --",
+ "preview": "vite preview",
+ "build-only": "vite build",
+ "type-check": "vue-tsc --build",
+ "lint": "eslint . --fix"
+ },
+ "dependencies": {
+ "axios": "^1.11.0",
+ "pinia": "^3.0.3",
+ "vue": "^3.5.18",
+ "vue-router": "^4.5.1"
+ },
+ "devDependencies": {
+ "@tsconfig/node22": "^22.0.2",
+ "@types/node": "^22.16.5",
+ "@vitejs/plugin-vue": "^6.0.1",
+ "@vue/eslint-config-typescript": "^14.6.0",
+ "@vue/tsconfig": "^0.7.0",
+ "eslint": "^9.31.0",
+ "eslint-plugin-vue": "~10.3.0",
+ "jiti": "^2.4.2",
+ "npm-run-all2": "^8.0.4",
+ "typescript": "~5.8.0",
+ "vite": "^7.0.6",
+ "vite-plugin-vue-devtools": "^8.0.0",
+ "vue-tsc": "^3.0.4"
+ }
+}
diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml
new file mode 100644
index 0000000..51dc392
--- /dev/null
+++ b/ui/pnpm-lock.yaml
@@ -0,0 +1,3296 @@
+lockfileVersion: '9.0'
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+
+importers:
+
+ .:
+ dependencies:
+ axios:
+ specifier: ^1.11.0
+ version: 1.11.0
+ pinia:
+ specifier: ^3.0.3
+ version: 3.0.3(typescript@5.8.3)(vue@3.5.18(typescript@5.8.3))
+ vue:
+ specifier: ^3.5.18
+ version: 3.5.18(typescript@5.8.3)
+ vue-router:
+ specifier: ^4.5.1
+ version: 4.5.1(vue@3.5.18(typescript@5.8.3))
+ devDependencies:
+ '@tsconfig/node22':
+ specifier: ^22.0.2
+ version: 22.0.2
+ '@types/node':
+ specifier: ^22.16.5
+ version: 22.17.2
+ '@vitejs/plugin-vue':
+ specifier: ^6.0.1
+ version: 6.0.1(vite@7.1.3(@types/node@22.17.2)(jiti@2.5.1))(vue@3.5.18(typescript@5.8.3))
+ '@vue/eslint-config-typescript':
+ specifier: ^14.6.0
+ version: 14.6.0(eslint-plugin-vue@10.3.0(@typescript-eslint/parser@8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3))(eslint@9.33.0(jiti@2.5.1))(vue-eslint-parser@10.2.0(eslint@9.33.0(jiti@2.5.1))))(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3)
+ '@vue/tsconfig':
+ specifier: ^0.7.0
+ version: 0.7.0(typescript@5.8.3)(vue@3.5.18(typescript@5.8.3))
+ eslint:
+ specifier: ^9.31.0
+ version: 9.33.0(jiti@2.5.1)
+ eslint-plugin-vue:
+ specifier: ~10.3.0
+ version: 10.3.0(@typescript-eslint/parser@8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3))(eslint@9.33.0(jiti@2.5.1))(vue-eslint-parser@10.2.0(eslint@9.33.0(jiti@2.5.1)))
+ jiti:
+ specifier: ^2.4.2
+ version: 2.5.1
+ npm-run-all2:
+ specifier: ^8.0.4
+ version: 8.0.4
+ typescript:
+ specifier: ~5.8.0
+ version: 5.8.3
+ vite:
+ specifier: ^7.0.6
+ version: 7.1.3(@types/node@22.17.2)(jiti@2.5.1)
+ vite-plugin-vue-devtools:
+ specifier: ^8.0.0
+ version: 8.0.0(vite@7.1.3(@types/node@22.17.2)(jiti@2.5.1))(vue@3.5.18(typescript@5.8.3))
+ vue-tsc:
+ specifier: ^3.0.4
+ version: 3.0.6(typescript@5.8.3)
+
+packages:
+
+ '@ampproject/remapping@2.3.0':
+ resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
+ engines: {node: '>=6.0.0'}
+
+ '@babel/code-frame@7.27.1':
+ resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/compat-data@7.28.0':
+ resolution: {integrity: sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/core@7.28.3':
+ resolution: {integrity: sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/generator@7.28.3':
+ resolution: {integrity: sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-annotate-as-pure@7.27.3':
+ resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-compilation-targets@7.27.2':
+ resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-create-class-features-plugin@7.28.3':
+ resolution: {integrity: sha512-V9f6ZFIYSLNEbuGA/92uOvYsGCJNsuA8ESZ4ldc09bWk/j8H8TKiPw8Mk1eG6olpnO0ALHJmYfZvF4MEE4gajg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/helper-globals@7.28.0':
+ resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-member-expression-to-functions@7.27.1':
+ resolution: {integrity: sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-module-imports@7.27.1':
+ resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-module-transforms@7.28.3':
+ resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/helper-optimise-call-expression@7.27.1':
+ resolution: {integrity: sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-plugin-utils@7.27.1':
+ resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-replace-supers@7.27.1':
+ resolution: {integrity: sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/helper-skip-transparent-expression-wrappers@7.27.1':
+ resolution: {integrity: sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-string-parser@7.27.1':
+ resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-identifier@7.27.1':
+ resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-option@7.27.1':
+ resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helpers@7.28.3':
+ resolution: {integrity: sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/parser@7.28.3':
+ resolution: {integrity: sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+
+ '@babel/plugin-proposal-decorators@7.28.0':
+ resolution: {integrity: sha512-zOiZqvANjWDUaUS9xMxbMcK/Zccztbe/6ikvUXaG9nsPH3w6qh5UaPGAnirI/WhIbZ8m3OHU0ReyPrknG+ZKeg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-decorators@7.27.1':
+ resolution: {integrity: sha512-YMq8Z87Lhl8EGkmb0MwYkt36QnxC+fzCgrl66ereamPlYToRpIk5nUjKUY3QKLWq8mwUB1BgbeXcTJhZOCDg5A==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-import-attributes@7.27.1':
+ resolution: {integrity: sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-import-meta@7.10.4':
+ resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-jsx@7.27.1':
+ resolution: {integrity: sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-typescript@7.27.1':
+ resolution: {integrity: sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-typescript@7.28.0':
+ resolution: {integrity: sha512-4AEiDEBPIZvLQaWlc9liCavE0xRM0dNca41WtBeM3jgFptfUOSG9z0uteLhq6+3rq+WB6jIvUwKDTpXEHPJ2Vg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/template@7.27.2':
+ resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/traverse@7.28.3':
+ resolution: {integrity: sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/types@7.28.2':
+ resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==}
+ engines: {node: '>=6.9.0'}
+
+ '@esbuild/aix-ppc64@0.25.9':
+ resolution: {integrity: sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [aix]
+
+ '@esbuild/android-arm64@0.25.9':
+ resolution: {integrity: sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [android]
+
+ '@esbuild/android-arm@0.25.9':
+ resolution: {integrity: sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [android]
+
+ '@esbuild/android-x64@0.25.9':
+ resolution: {integrity: sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [android]
+
+ '@esbuild/darwin-arm64@0.25.9':
+ resolution: {integrity: sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@esbuild/darwin-x64@0.25.9':
+ resolution: {integrity: sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@esbuild/freebsd-arm64@0.25.9':
+ resolution: {integrity: sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@esbuild/freebsd-x64@0.25.9':
+ resolution: {integrity: sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@esbuild/linux-arm64@0.25.9':
+ resolution: {integrity: sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@esbuild/linux-arm@0.25.9':
+ resolution: {integrity: sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [linux]
+
+ '@esbuild/linux-ia32@0.25.9':
+ resolution: {integrity: sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [linux]
+
+ '@esbuild/linux-loong64@0.25.9':
+ resolution: {integrity: sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==}
+ engines: {node: '>=18'}
+ cpu: [loong64]
+ os: [linux]
+
+ '@esbuild/linux-mips64el@0.25.9':
+ resolution: {integrity: sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==}
+ engines: {node: '>=18'}
+ cpu: [mips64el]
+ os: [linux]
+
+ '@esbuild/linux-ppc64@0.25.9':
+ resolution: {integrity: sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@esbuild/linux-riscv64@0.25.9':
+ resolution: {integrity: sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==}
+ engines: {node: '>=18'}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@esbuild/linux-s390x@0.25.9':
+ resolution: {integrity: sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==}
+ engines: {node: '>=18'}
+ cpu: [s390x]
+ os: [linux]
+
+ '@esbuild/linux-x64@0.25.9':
+ resolution: {integrity: sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [linux]
+
+ '@esbuild/netbsd-arm64@0.25.9':
+ resolution: {integrity: sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [netbsd]
+
+ '@esbuild/netbsd-x64@0.25.9':
+ resolution: {integrity: sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [netbsd]
+
+ '@esbuild/openbsd-arm64@0.25.9':
+ resolution: {integrity: sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openbsd]
+
+ '@esbuild/openbsd-x64@0.25.9':
+ resolution: {integrity: sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [openbsd]
+
+ '@esbuild/openharmony-arm64@0.25.9':
+ resolution: {integrity: sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@esbuild/sunos-x64@0.25.9':
+ resolution: {integrity: sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [sunos]
+
+ '@esbuild/win32-arm64@0.25.9':
+ resolution: {integrity: sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@esbuild/win32-ia32@0.25.9':
+ resolution: {integrity: sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [win32]
+
+ '@esbuild/win32-x64@0.25.9':
+ resolution: {integrity: sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [win32]
+
+ '@eslint-community/eslint-utils@4.7.0':
+ resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ peerDependencies:
+ eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
+
+ '@eslint-community/regexpp@4.12.1':
+ resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==}
+ engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
+
+ '@eslint/config-array@0.21.0':
+ resolution: {integrity: sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/config-helpers@0.3.1':
+ resolution: {integrity: sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/core@0.15.2':
+ resolution: {integrity: sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/eslintrc@3.3.1':
+ resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/js@9.33.0':
+ resolution: {integrity: sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/object-schema@2.1.6':
+ resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/plugin-kit@0.3.5':
+ resolution: {integrity: sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@humanfs/core@0.19.1':
+ resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
+ engines: {node: '>=18.18.0'}
+
+ '@humanfs/node@0.16.6':
+ resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==}
+ engines: {node: '>=18.18.0'}
+
+ '@humanwhocodes/module-importer@1.0.1':
+ resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==}
+ engines: {node: '>=12.22'}
+
+ '@humanwhocodes/retry@0.3.1':
+ resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==}
+ engines: {node: '>=18.18'}
+
+ '@humanwhocodes/retry@0.4.3':
+ resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
+ engines: {node: '>=18.18'}
+
+ '@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.30':
+ resolution: {integrity: sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==}
+
+ '@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'}
+
+ '@polka/url@1.0.0-next.29':
+ resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
+
+ '@rolldown/pluginutils@1.0.0-beta.29':
+ resolution: {integrity: sha512-NIJgOsMjbxAXvoGq/X0gD7VPMQ8j9g0BiDaNjVNVjvl+iKXxL3Jre0v31RmBYeLEmkbj2s02v8vFTbUXi5XS2Q==}
+
+ '@rollup/rollup-android-arm-eabi@4.46.4':
+ resolution: {integrity: sha512-B2wfzCJ+ps/OBzRjeds7DlJumCU3rXMxJJS1vzURyj7+KBHGONm7c9q1TfdBl4vCuNMkDvARn3PBl2wZzuR5mw==}
+ cpu: [arm]
+ os: [android]
+
+ '@rollup/rollup-android-arm64@4.46.4':
+ resolution: {integrity: sha512-FGJYXvYdn8Bs6lAlBZYT5n+4x0ciEp4cmttsvKAZc/c8/JiPaQK8u0c/86vKX8lA7OY/+37lIQSe0YoAImvBAA==}
+ cpu: [arm64]
+ os: [android]
+
+ '@rollup/rollup-darwin-arm64@4.46.4':
+ resolution: {integrity: sha512-/9qwE/BM7ATw/W/OFEMTm3dmywbJyLQb4f4v5nmOjgYxPIGpw7HaxRi6LnD4Pjn/q7k55FGeHe1/OD02w63apA==}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@rollup/rollup-darwin-x64@4.46.4':
+ resolution: {integrity: sha512-QkWfNbeRuzFnv2d0aPlrzcA3Ebq2mE8kX/5Pl7VdRShbPBjSnom7dbT8E3Jmhxo2RL784hyqGvR5KHavCJQciw==}
+ cpu: [x64]
+ os: [darwin]
+
+ '@rollup/rollup-freebsd-arm64@4.46.4':
+ resolution: {integrity: sha512-+ToyOMYnSfV8D+ckxO6NthPln/PDNp1P6INcNypfZ7muLmEvPKXqduUiD8DlJpMMT8LxHcE5W0dK9kXfJke9Zw==}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@rollup/rollup-freebsd-x64@4.46.4':
+ resolution: {integrity: sha512-cGT6ey/W+sje6zywbLiqmkfkO210FgRz7tepWAzzEVgQU8Hn91JJmQWNqs55IuglG8sJdzk7XfNgmGRtcYlo1w==}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@rollup/rollup-linux-arm-gnueabihf@4.46.4':
+ resolution: {integrity: sha512-9fhTJyOb275w5RofPSl8lpr4jFowd+H4oQKJ9XTYzD1JWgxdZKE8bA6d4npuiMemkecQOcigX01FNZNCYnQBdA==}
+ cpu: [arm]
+ os: [linux]
+
+ '@rollup/rollup-linux-arm-musleabihf@4.46.4':
+ resolution: {integrity: sha512-+6kCIM5Zjvz2HwPl/udgVs07tPMIp1VU2Y0c72ezjOvSvEfAIWsUgpcSDvnC7g9NrjYR6X9bZT92mZZ90TfvXw==}
+ cpu: [arm]
+ os: [linux]
+
+ '@rollup/rollup-linux-arm64-gnu@4.46.4':
+ resolution: {integrity: sha512-SWuXdnsayCZL4lXoo6jn0yyAj7TTjWE4NwDVt9s7cmu6poMhtiras5c8h6Ih6Y0Zk6Z+8t/mLumvpdSPTWub2Q==}
+ cpu: [arm64]
+ os: [linux]
+
+ '@rollup/rollup-linux-arm64-musl@4.46.4':
+ resolution: {integrity: sha512-vDknMDqtMhrrroa5kyX6tuC0aRZZlQ+ipDfbXd2YGz5HeV2t8HOl/FDAd2ynhs7Ki5VooWiiZcCtxiZ4IjqZwQ==}
+ cpu: [arm64]
+ os: [linux]
+
+ '@rollup/rollup-linux-loongarch64-gnu@4.46.4':
+ resolution: {integrity: sha512-mCBkjRZWhvjtl/x+Bd4fQkWZT8canStKDxGrHlBiTnZmJnWygGcvBylzLVCZXka4dco5ymkWhZlLwKCGFF4ivw==}
+ cpu: [loong64]
+ os: [linux]
+
+ '@rollup/rollup-linux-ppc64-gnu@4.46.4':
+ resolution: {integrity: sha512-YMdz2phOTFF+Z66dQfGf0gmeDSi5DJzY5bpZyeg9CPBkV9QDzJ1yFRlmi/j7WWRf3hYIWrOaJj5jsfwgc8GTHQ==}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@rollup/rollup-linux-riscv64-gnu@4.46.4':
+ resolution: {integrity: sha512-r0WKLSfFAK8ucG024v2yiLSJMedoWvk8yWqfNICX28NHDGeu3F/wBf8KG6mclghx4FsLePxJr/9N8rIj1PtCnw==}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@rollup/rollup-linux-riscv64-musl@4.46.4':
+ resolution: {integrity: sha512-IaizpPP2UQU3MNyPH1u0Xxbm73D+4OupL0bjo4Hm0496e2wg3zuvoAIhubkD1NGy9fXILEExPQy87mweujEatA==}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@rollup/rollup-linux-s390x-gnu@4.46.4':
+ resolution: {integrity: sha512-aCM29orANR0a8wk896p6UEgIfupReupnmISz6SUwMIwTGaTI8MuKdE0OD2LvEg8ondDyZdMvnaN3bW4nFbATPA==}
+ cpu: [s390x]
+ os: [linux]
+
+ '@rollup/rollup-linux-x64-gnu@4.46.4':
+ resolution: {integrity: sha512-0Xj1vZE3cbr/wda8d/m+UeuSL+TDpuozzdD4QaSzu/xSOMK0Su5RhIkF7KVHFQsobemUNHPLEcYllL7ZTCP/Cg==}
+ cpu: [x64]
+ os: [linux]
+
+ '@rollup/rollup-linux-x64-musl@4.46.4':
+ resolution: {integrity: sha512-kM/orjpolfA5yxsx84kI6bnK47AAZuWxglGKcNmokw2yy9i5eHY5UAjcX45jemTJnfHAWo3/hOoRqEeeTdL5hw==}
+ cpu: [x64]
+ os: [linux]
+
+ '@rollup/rollup-win32-arm64-msvc@4.46.4':
+ resolution: {integrity: sha512-cNLH4psMEsWKILW0isbpQA2OvjXLbKvnkcJFmqAptPQbtLrobiapBJVj6RoIvg6UXVp5w0wnIfd/Q56cNpF+Ew==}
+ cpu: [arm64]
+ os: [win32]
+
+ '@rollup/rollup-win32-ia32-msvc@4.46.4':
+ resolution: {integrity: sha512-OiEa5lRhiANpv4SfwYVgQ3opYWi/QmPDC5ve21m8G9pf6ZO+aX1g2EEF1/IFaM1xPSP7mK0msTRXlPs6mIagkg==}
+ cpu: [ia32]
+ os: [win32]
+
+ '@rollup/rollup-win32-x64-msvc@4.46.4':
+ resolution: {integrity: sha512-IKL9mewGZ5UuuX4NQlwOmxPyqielvkAPUS2s1cl6yWjjQvyN3h5JTdVFGD5Jr5xMjRC8setOfGQDVgX8V+dkjg==}
+ cpu: [x64]
+ os: [win32]
+
+ '@sec-ant/readable-stream@0.4.1':
+ resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==}
+
+ '@sindresorhus/merge-streams@4.0.0':
+ resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==}
+ engines: {node: '>=18'}
+
+ '@tsconfig/node22@22.0.2':
+ resolution: {integrity: sha512-Kmwj4u8sDRDrMYRoN9FDEcXD8UpBSaPQQ24Gz+Gamqfm7xxn+GBR7ge/Z7pK8OXNGyUzbSwJj+TH6B+DS/epyA==}
+
+ '@types/estree@1.0.8':
+ resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
+
+ '@types/json-schema@7.0.15':
+ resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
+
+ '@types/node@22.17.2':
+ resolution: {integrity: sha512-gL6z5N9Jm9mhY+U2KXZpteb+09zyffliRkZyZOHODGATyC5B1Jt/7TzuuiLkFsSUMLbS1OLmlj/E+/3KF4Q/4w==}
+
+ '@typescript-eslint/eslint-plugin@8.40.0':
+ resolution: {integrity: sha512-w/EboPlBwnmOBtRbiOvzjD+wdiZdgFeo17lkltrtn7X37vagKKWJABvyfsJXTlHe6XBzugmYgd4A4nW+k8Mixw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ '@typescript-eslint/parser': ^8.40.0
+ eslint: ^8.57.0 || ^9.0.0
+ typescript: '>=4.8.4 <6.0.0'
+
+ '@typescript-eslint/parser@8.40.0':
+ resolution: {integrity: sha512-jCNyAuXx8dr5KJMkecGmZ8KI61KBUhkCob+SD+C+I5+Y1FWI2Y3QmY4/cxMCC5WAsZqoEtEETVhUiUMIGCf6Bw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0
+ typescript: '>=4.8.4 <6.0.0'
+
+ '@typescript-eslint/project-service@8.40.0':
+ resolution: {integrity: sha512-/A89vz7Wf5DEXsGVvcGdYKbVM9F7DyFXj52lNYUDS1L9yJfqjW/fIp5PgMuEJL/KeqVTe2QSbXAGUZljDUpArw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ typescript: '>=4.8.4 <6.0.0'
+
+ '@typescript-eslint/scope-manager@8.40.0':
+ resolution: {integrity: sha512-y9ObStCcdCiZKzwqsE8CcpyuVMwRouJbbSrNuThDpv16dFAj429IkM6LNb1dZ2m7hK5fHyzNcErZf7CEeKXR4w==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@typescript-eslint/tsconfig-utils@8.40.0':
+ resolution: {integrity: sha512-jtMytmUaG9d/9kqSl/W3E3xaWESo4hFDxAIHGVW/WKKtQhesnRIJSAJO6XckluuJ6KDB5woD1EiqknriCtAmcw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ typescript: '>=4.8.4 <6.0.0'
+
+ '@typescript-eslint/type-utils@8.40.0':
+ resolution: {integrity: sha512-eE60cK4KzAc6ZrzlJnflXdrMqOBaugeukWICO2rB0KNvwdIMaEaYiywwHMzA1qFpTxrLhN9Lp4E/00EgWcD3Ow==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0
+ typescript: '>=4.8.4 <6.0.0'
+
+ '@typescript-eslint/types@8.40.0':
+ resolution: {integrity: sha512-ETdbFlgbAmXHyFPwqUIYrfc12ArvpBhEVgGAxVYSwli26dn8Ko+lIo4Su9vI9ykTZdJn+vJprs/0eZU0YMAEQg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@typescript-eslint/typescript-estree@8.40.0':
+ resolution: {integrity: sha512-k1z9+GJReVVOkc1WfVKs1vBrR5MIKKbdAjDTPvIK3L8De6KbFfPFt6BKpdkdk7rZS2GtC/m6yI5MYX+UsuvVYQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ typescript: '>=4.8.4 <6.0.0'
+
+ '@typescript-eslint/utils@8.40.0':
+ resolution: {integrity: sha512-Cgzi2MXSZyAUOY+BFwGs17s7ad/7L+gKt6Y8rAVVWS+7o6wrjeFN4nVfTpbE25MNcxyJ+iYUXflbs2xR9h4UBg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0
+ typescript: '>=4.8.4 <6.0.0'
+
+ '@typescript-eslint/visitor-keys@8.40.0':
+ resolution: {integrity: sha512-8CZ47QwalyRjsypfwnbI3hKy5gJDPmrkLjkgMxhi0+DZZ2QNx2naS6/hWoVYUHU7LU2zleF68V9miaVZvhFfTA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@vitejs/plugin-vue@6.0.1':
+ resolution: {integrity: sha512-+MaE752hU0wfPFJEUAIxqw18+20euHHdxVtMvbFcOEpjEyfqXH/5DCoTHiVJ0J29EhTJdoTkjEv5YBKU9dnoTw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ peerDependencies:
+ vite: ^5.0.0 || ^6.0.0 || ^7.0.0
+ vue: ^3.2.25
+
+ '@volar/language-core@2.4.23':
+ resolution: {integrity: sha512-hEEd5ET/oSmBC6pi1j6NaNYRWoAiDhINbT8rmwtINugR39loROSlufGdYMF9TaKGfz+ViGs1Idi3mAhnuPcoGQ==}
+
+ '@volar/source-map@2.4.23':
+ resolution: {integrity: sha512-Z1Uc8IB57Lm6k7q6KIDu/p+JWtf3xsXJqAX/5r18hYOTpJyBn0KXUR8oTJ4WFYOcDzWC9n3IflGgHowx6U6z9Q==}
+
+ '@volar/typescript@2.4.23':
+ resolution: {integrity: sha512-lAB5zJghWxVPqfcStmAP1ZqQacMpe90UrP5RJ3arDyrhy4aCUQqmxPPLB2PWDKugvylmO41ljK7vZ+t6INMTag==}
+
+ '@vue/babel-helper-vue-transform-on@1.5.0':
+ resolution: {integrity: sha512-0dAYkerNhhHutHZ34JtTl2czVQHUNWv6xEbkdF5W+Yrv5pCWsqjeORdOgbtW2I9gWlt+wBmVn+ttqN9ZxR5tzA==}
+
+ '@vue/babel-plugin-jsx@1.5.0':
+ resolution: {integrity: sha512-mneBhw1oOqCd2247O0Yw/mRwC9jIGACAJUlawkmMBiNmL4dGA2eMzuNZVNqOUfYTa6vqmND4CtOPzmEEEqLKFw==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ peerDependenciesMeta:
+ '@babel/core':
+ optional: true
+
+ '@vue/babel-plugin-resolve-type@1.5.0':
+ resolution: {integrity: sha512-Wm/60o+53JwJODm4Knz47dxJnLDJ9FnKnGZJbUUf8nQRAtt6P+undLUAVU3Ha33LxOJe6IPoifRQ6F/0RrU31w==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@vue/compiler-core@3.5.18':
+ resolution: {integrity: sha512-3slwjQrrV1TO8MoXgy3aynDQ7lslj5UqDxuHnrzHtpON5CBinhWjJETciPngpin/T3OuW3tXUf86tEurusnztw==}
+
+ '@vue/compiler-dom@3.5.18':
+ resolution: {integrity: sha512-RMbU6NTU70++B1JyVJbNbeFkK+A+Q7y9XKE2EM4NLGm2WFR8x9MbAtWxPPLdm0wUkuZv9trpwfSlL6tjdIa1+A==}
+
+ '@vue/compiler-sfc@3.5.18':
+ resolution: {integrity: sha512-5aBjvGqsWs+MoxswZPoTB9nSDb3dhd1x30xrrltKujlCxo48j8HGDNj3QPhF4VIS0VQDUrA1xUfp2hEa+FNyXA==}
+
+ '@vue/compiler-ssr@3.5.18':
+ resolution: {integrity: sha512-xM16Ak7rSWHkM3m22NlmcdIM+K4BMyFARAfV9hYFl+SFuRzrZ3uGMNW05kA5pmeMa0X9X963Kgou7ufdbpOP9g==}
+
+ '@vue/compiler-vue2@2.7.16':
+ resolution: {integrity: sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==}
+
+ '@vue/devtools-api@6.6.4':
+ resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==}
+
+ '@vue/devtools-api@7.7.7':
+ resolution: {integrity: sha512-lwOnNBH2e7x1fIIbVT7yF5D+YWhqELm55/4ZKf45R9T8r9dE2AIOy8HKjfqzGsoTHFbWbr337O4E0A0QADnjBg==}
+
+ '@vue/devtools-core@8.0.0':
+ resolution: {integrity: sha512-5bPtF0jAFnaGs4C/4+3vGRR5U+cf6Y8UWK0nJflutEDGepHxl5L9JRaPdHQYCUgrzUaf4cY4waNBEEGXrfcs3A==}
+ peerDependencies:
+ vue: ^3.0.0
+
+ '@vue/devtools-kit@7.7.7':
+ resolution: {integrity: sha512-wgoZtxcTta65cnZ1Q6MbAfePVFxfM+gq0saaeytoph7nEa7yMXoi6sCPy4ufO111B9msnw0VOWjPEFCXuAKRHA==}
+
+ '@vue/devtools-kit@8.0.0':
+ resolution: {integrity: sha512-b11OeQODkE0bctdT0RhL684pEV2DPXJ80bjpywVCbFn1PxuL3bmMPDoJKjbMnnoWbrnUYXYzFfmMWBZAMhORkQ==}
+
+ '@vue/devtools-shared@7.7.7':
+ resolution: {integrity: sha512-+udSj47aRl5aKb0memBvcUG9koarqnxNM5yjuREvqwK6T3ap4mn3Zqqc17QrBFTqSMjr3HK1cvStEZpMDpfdyw==}
+
+ '@vue/devtools-shared@8.0.0':
+ resolution: {integrity: sha512-jrKnbjshQCiOAJanoeJjTU7WaCg0Dz2BUal6SaR6VM/P3hiFdX5Q6Pxl73ZMnrhCxNK9nAg5hvvRGqs+6dtU1g==}
+
+ '@vue/eslint-config-typescript@14.6.0':
+ resolution: {integrity: sha512-UpiRY/7go4Yps4mYCjkvlIbVWmn9YvPGQDxTAlcKLphyaD77LjIu3plH4Y9zNT0GB4f3K5tMmhhtRhPOgrQ/bQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^9.10.0
+ eslint-plugin-vue: ^9.28.0 || ^10.0.0
+ typescript: '>=4.8.4'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
+ '@vue/language-core@3.0.6':
+ resolution: {integrity: sha512-e2RRzYWm+qGm8apUHW1wA5RQxzNhkqbbKdbKhiDUcmMrNAZGyM8aTiL3UrTqkaFI5s7wJRGGrp4u3jgusuBp2A==}
+ peerDependencies:
+ typescript: '*'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
+ '@vue/reactivity@3.5.18':
+ resolution: {integrity: sha512-x0vPO5Imw+3sChLM5Y+B6G1zPjwdOri9e8V21NnTnlEvkxatHEH5B5KEAJcjuzQ7BsjGrKtfzuQ5eQwXh8HXBg==}
+
+ '@vue/runtime-core@3.5.18':
+ resolution: {integrity: sha512-DUpHa1HpeOQEt6+3nheUfqVXRog2kivkXHUhoqJiKR33SO4x+a5uNOMkV487WPerQkL0vUuRvq/7JhRgLW3S+w==}
+
+ '@vue/runtime-dom@3.5.18':
+ resolution: {integrity: sha512-YwDj71iV05j4RnzZnZtGaXwPoUWeRsqinblgVJwR8XTXYZ9D5PbahHQgsbmzUvCWNF6x7siQ89HgnX5eWkr3mw==}
+
+ '@vue/server-renderer@3.5.18':
+ resolution: {integrity: sha512-PvIHLUoWgSbDG7zLHqSqaCoZvHi6NNmfVFOqO+OnwvqMz/tqQr3FuGWS8ufluNddk7ZLBJYMrjcw1c6XzR12mA==}
+ peerDependencies:
+ vue: 3.5.18
+
+ '@vue/shared@3.5.18':
+ resolution: {integrity: sha512-cZy8Dq+uuIXbxCZpuLd2GJdeSO/lIzIspC2WtkqIpje5QyFbvLaI5wZtdUjLHjGZrlVX6GilejatWwVYYRc8tA==}
+
+ '@vue/tsconfig@0.7.0':
+ resolution: {integrity: sha512-ku2uNz5MaZ9IerPPUyOHzyjhXoX2kVJaVf7hL315DC17vS6IiZRmmCPfggNbU16QTvM80+uYYy3eYJB59WCtvg==}
+ peerDependencies:
+ typescript: 5.x
+ vue: ^3.4.0
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+ vue:
+ optional: true
+
+ acorn-jsx@5.3.2:
+ resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
+ peerDependencies:
+ acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
+
+ acorn@8.15.0:
+ resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==}
+ engines: {node: '>=0.4.0'}
+ hasBin: true
+
+ ajv@6.12.6:
+ resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
+
+ alien-signals@2.0.7:
+ resolution: {integrity: sha512-wE7y3jmYeb0+h6mr5BOovuqhFv22O/MV9j5p0ndJsa7z1zJNPGQ4ph5pQk/kTTCWRC3xsA4SmtwmkzQO+7NCNg==}
+
+ ansi-styles@4.3.0:
+ resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
+ engines: {node: '>=8'}
+
+ ansi-styles@6.2.1:
+ resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
+ engines: {node: '>=12'}
+
+ ansis@4.1.0:
+ resolution: {integrity: sha512-BGcItUBWSMRgOCe+SVZJ+S7yTRG0eGt9cXAHev72yuGcY23hnLA7Bky5L/xLyPINoSN95geovfBkqoTlNZYa7w==}
+ engines: {node: '>=14'}
+
+ argparse@2.0.1:
+ resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
+
+ asynckit@0.4.0:
+ resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
+
+ axios@1.11.0:
+ resolution: {integrity: sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==}
+
+ balanced-match@1.0.2:
+ resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
+
+ birpc@2.5.0:
+ resolution: {integrity: sha512-VSWO/W6nNQdyP520F1mhf+Lc2f8pjGQOtoHHm7Ze8Go1kX7akpVIrtTa0fn+HB0QJEDVacl6aO08YE0PgXfdnQ==}
+
+ boolbase@1.0.0:
+ resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
+
+ brace-expansion@1.1.12:
+ resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
+
+ brace-expansion@2.0.2:
+ resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
+
+ braces@3.0.3:
+ resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
+ engines: {node: '>=8'}
+
+ browserslist@4.25.3:
+ resolution: {integrity: sha512-cDGv1kkDI4/0e5yON9yM5G/0A5u8sf5TnmdX5C9qHzI9PPu++sQ9zjm1k9NiOrf3riY4OkK0zSGqfvJyJsgCBQ==}
+ engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
+ hasBin: true
+
+ bundle-name@4.1.0:
+ resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==}
+ engines: {node: '>=18'}
+
+ call-bind-apply-helpers@1.0.2:
+ resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
+ engines: {node: '>= 0.4'}
+
+ callsites@3.1.0:
+ resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
+ engines: {node: '>=6'}
+
+ caniuse-lite@1.0.30001735:
+ resolution: {integrity: sha512-EV/laoX7Wq2J9TQlyIXRxTJqIw4sxfXS4OYgudGxBYRuTv0q7AM6yMEpU/Vo1I94thg9U6EZ2NfZx9GJq83u7w==}
+
+ chalk@4.1.2:
+ resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
+ engines: {node: '>=10'}
+
+ color-convert@2.0.1:
+ resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
+ engines: {node: '>=7.0.0'}
+
+ color-name@1.1.4:
+ resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
+
+ combined-stream@1.0.8:
+ resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
+ engines: {node: '>= 0.8'}
+
+ concat-map@0.0.1:
+ resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
+
+ convert-source-map@2.0.0:
+ resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
+
+ copy-anything@3.0.5:
+ resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==}
+ engines: {node: '>=12.13'}
+
+ cross-spawn@7.0.6:
+ resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
+ engines: {node: '>= 8'}
+
+ cssesc@3.0.0:
+ resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
+ engines: {node: '>=4'}
+ hasBin: true
+
+ csstype@3.1.3:
+ resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
+
+ de-indent@1.0.2:
+ resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==}
+
+ debug@4.4.1:
+ resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
+ engines: {node: '>=6.0'}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+
+ deep-is@0.1.4:
+ resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
+
+ default-browser-id@5.0.0:
+ resolution: {integrity: sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==}
+ engines: {node: '>=18'}
+
+ default-browser@5.2.1:
+ resolution: {integrity: sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==}
+ engines: {node: '>=18'}
+
+ define-lazy-prop@3.0.0:
+ resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==}
+ engines: {node: '>=12'}
+
+ delayed-stream@1.0.0:
+ resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
+ engines: {node: '>=0.4.0'}
+
+ dunder-proto@1.0.1:
+ resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
+ engines: {node: '>= 0.4'}
+
+ electron-to-chromium@1.5.207:
+ resolution: {integrity: sha512-mryFrrL/GXDTmAtIVMVf+eIXM09BBPlO5IQ7lUyKmK8d+A4VpRGG+M3ofoVef6qyF8s60rJei8ymlJxjUA8Faw==}
+
+ entities@4.5.0:
+ resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
+ engines: {node: '>=0.12'}
+
+ error-stack-parser-es@1.0.5:
+ resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==}
+
+ 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.25.9:
+ resolution: {integrity: sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ escalade@3.2.0:
+ resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
+ engines: {node: '>=6'}
+
+ escape-string-regexp@4.0.0:
+ resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
+ engines: {node: '>=10'}
+
+ eslint-plugin-vue@10.3.0:
+ resolution: {integrity: sha512-A0u9snqjCfYaPnqqOaH6MBLVWDUIN4trXn8J3x67uDcXvR7X6Ut8p16N+nYhMCQ9Y7edg2BIRGzfyZsY0IdqoQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ '@typescript-eslint/parser': ^7.0.0 || ^8.0.0
+ eslint: ^8.57.0 || ^9.0.0
+ vue-eslint-parser: ^10.0.0
+ peerDependenciesMeta:
+ '@typescript-eslint/parser':
+ optional: true
+
+ eslint-scope@8.4.0:
+ resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ eslint-visitor-keys@3.4.3:
+ resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+
+ eslint-visitor-keys@4.2.1:
+ resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ eslint@9.33.0:
+ resolution: {integrity: sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ hasBin: true
+ peerDependencies:
+ jiti: '*'
+ peerDependenciesMeta:
+ jiti:
+ optional: true
+
+ espree@10.4.0:
+ resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ esquery@1.6.0:
+ resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==}
+ engines: {node: '>=0.10'}
+
+ esrecurse@4.3.0:
+ resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==}
+ engines: {node: '>=4.0'}
+
+ estraverse@5.3.0:
+ resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
+ engines: {node: '>=4.0'}
+
+ estree-walker@2.0.2:
+ resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
+
+ esutils@2.0.3:
+ resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
+ engines: {node: '>=0.10.0'}
+
+ execa@9.6.0:
+ resolution: {integrity: sha512-jpWzZ1ZhwUmeWRhS7Qv3mhpOhLfwI+uAX4e5fOcXqwMR7EcJ0pj2kV1CVzHVMX/LphnKWD3LObjZCoJ71lKpHw==}
+ engines: {node: ^18.19.0 || >=20.5.0}
+
+ fast-deep-equal@3.1.3:
+ resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
+
+ fast-glob@3.3.3:
+ resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==}
+ engines: {node: '>=8.6.0'}
+
+ fast-json-stable-stringify@2.1.0:
+ resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
+
+ fast-levenshtein@2.0.6:
+ resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
+
+ fastq@1.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
+
+ figures@6.1.0:
+ resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==}
+ engines: {node: '>=18'}
+
+ file-entry-cache@8.0.0:
+ resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
+ engines: {node: '>=16.0.0'}
+
+ fill-range@7.1.1:
+ resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
+ engines: {node: '>=8'}
+
+ find-up@5.0.0:
+ resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
+ engines: {node: '>=10'}
+
+ flat-cache@4.0.1:
+ resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==}
+ engines: {node: '>=16'}
+
+ flatted@3.3.3:
+ resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
+
+ 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.4:
+ resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==}
+ engines: {node: '>= 6'}
+
+ fsevents@2.3.3:
+ resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+
+ function-bind@1.1.2:
+ resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
+
+ gensync@1.0.0-beta.2:
+ resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
+ engines: {node: '>=6.9.0'}
+
+ 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'}
+
+ get-stream@9.0.1:
+ resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==}
+ engines: {node: '>=18'}
+
+ 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'}
+
+ globals@14.0.0:
+ resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
+ engines: {node: '>=18'}
+
+ gopd@1.2.0:
+ resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
+ engines: {node: '>= 0.4'}
+
+ graphemer@1.4.0:
+ resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
+
+ has-flag@4.0.0:
+ resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
+ engines: {node: '>=8'}
+
+ 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
+
+ hookable@5.5.3:
+ resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
+
+ human-signals@8.0.1:
+ resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==}
+ engines: {node: '>=18.18.0'}
+
+ ignore@5.3.2:
+ resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
+ engines: {node: '>= 4'}
+
+ ignore@7.0.5:
+ resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
+ engines: {node: '>= 4'}
+
+ import-fresh@3.3.1:
+ resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
+ engines: {node: '>=6'}
+
+ imurmurhash@0.1.4:
+ resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
+ engines: {node: '>=0.8.19'}
+
+ is-docker@3.0.0:
+ resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==}
+ engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+ hasBin: true
+
+ 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-inside-container@1.0.0:
+ resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==}
+ engines: {node: '>=14.16'}
+ hasBin: true
+
+ is-number@7.0.0:
+ resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
+ engines: {node: '>=0.12.0'}
+
+ is-plain-obj@4.1.0:
+ resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==}
+ engines: {node: '>=12'}
+
+ is-stream@4.0.1:
+ resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==}
+ engines: {node: '>=18'}
+
+ is-unicode-supported@2.1.0:
+ resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==}
+ engines: {node: '>=18'}
+
+ is-what@4.1.16:
+ resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==}
+ engines: {node: '>=12.13'}
+
+ is-wsl@3.1.0:
+ resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==}
+ engines: {node: '>=16'}
+
+ isexe@2.0.0:
+ resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
+
+ isexe@3.1.1:
+ resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==}
+ engines: {node: '>=16'}
+
+ jiti@2.5.1:
+ resolution: {integrity: sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==}
+ hasBin: true
+
+ js-tokens@4.0.0:
+ resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
+
+ js-yaml@4.1.0:
+ resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
+ hasBin: true
+
+ jsesc@3.1.0:
+ resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
+ engines: {node: '>=6'}
+ hasBin: true
+
+ json-buffer@3.0.1:
+ resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
+
+ json-parse-even-better-errors@4.0.0:
+ resolution: {integrity: sha512-lR4MXjGNgkJc7tkQ97kb2nuEMnNCyU//XYVH0MKTGcXEiSudQ5MKGKen3C5QubYy0vmq+JGitUg92uuywGEwIA==}
+ engines: {node: ^18.17.0 || >=20.5.0}
+
+ json-schema-traverse@0.4.1:
+ resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
+
+ json-stable-stringify-without-jsonify@1.0.1:
+ resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
+
+ json5@2.2.3:
+ resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
+ engines: {node: '>=6'}
+ hasBin: true
+
+ keyv@4.5.4:
+ resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
+
+ kolorist@1.8.0:
+ resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==}
+
+ levn@0.4.1:
+ resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
+ engines: {node: '>= 0.8.0'}
+
+ locate-path@6.0.0:
+ resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
+ engines: {node: '>=10'}
+
+ lodash.merge@4.6.2:
+ resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
+
+ lru-cache@5.1.1:
+ resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
+
+ magic-string@0.30.17:
+ resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==}
+
+ math-intrinsics@1.1.0:
+ resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
+ engines: {node: '>= 0.4'}
+
+ memorystream@0.3.1:
+ resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==}
+ engines: {node: '>= 0.10.0'}
+
+ 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@3.1.2:
+ resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
+
+ minimatch@9.0.5:
+ resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
+ engines: {node: '>=16 || 14 >=14.17'}
+
+ mitt@3.0.1:
+ resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
+
+ mrmime@2.0.1:
+ resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==}
+ engines: {node: '>=10'}
+
+ ms@2.1.3:
+ resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
+
+ muggle-string@0.4.1:
+ resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==}
+
+ nanoid@3.3.11:
+ resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+ hasBin: true
+
+ nanoid@5.1.5:
+ resolution: {integrity: sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==}
+ engines: {node: ^18 || >=20}
+ hasBin: true
+
+ natural-compare@1.4.0:
+ resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
+
+ node-releases@2.0.19:
+ resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==}
+
+ npm-normalize-package-bin@4.0.0:
+ resolution: {integrity: sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w==}
+ engines: {node: ^18.17.0 || >=20.5.0}
+
+ npm-run-all2@8.0.4:
+ resolution: {integrity: sha512-wdbB5My48XKp2ZfJUlhnLVihzeuA1hgBnqB2J9ahV77wLS+/YAJAlN8I+X3DIFIPZ3m5L7nplmlbhNiFDmXRDA==}
+ engines: {node: ^20.5.0 || >=22.0.0, npm: '>= 10'}
+ hasBin: true
+
+ npm-run-path@6.0.0:
+ resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==}
+ engines: {node: '>=18'}
+
+ nth-check@2.1.1:
+ resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
+
+ ohash@2.0.11:
+ resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==}
+
+ open@10.2.0:
+ resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==}
+ engines: {node: '>=18'}
+
+ optionator@0.9.4:
+ resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
+ engines: {node: '>= 0.8.0'}
+
+ p-limit@3.1.0:
+ resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
+ engines: {node: '>=10'}
+
+ p-locate@5.0.0:
+ resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
+ engines: {node: '>=10'}
+
+ parent-module@1.0.1:
+ resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
+ engines: {node: '>=6'}
+
+ parse-ms@4.0.0:
+ resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==}
+ engines: {node: '>=18'}
+
+ path-browserify@1.0.1:
+ resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
+
+ path-exists@4.0.0:
+ resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
+ engines: {node: '>=8'}
+
+ path-key@3.1.1:
+ resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
+ engines: {node: '>=8'}
+
+ path-key@4.0.0:
+ resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==}
+ engines: {node: '>=12'}
+
+ pathe@2.0.3:
+ resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
+
+ perfect-debounce@1.0.0:
+ resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==}
+
+ 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'}
+
+ pidtree@0.6.0:
+ resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==}
+ engines: {node: '>=0.10'}
+ hasBin: true
+
+ pinia@3.0.3:
+ resolution: {integrity: sha512-ttXO/InUULUXkMHpTdp9Fj4hLpD/2AoJdmAbAeW2yu1iy1k+pkFekQXw5VpC0/5p51IOR/jDaDRfRWRnMMsGOA==}
+ peerDependencies:
+ typescript: '>=4.4.4'
+ vue: ^2.7.0 || ^3.5.11
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
+ postcss-selector-parser@6.1.2:
+ resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==}
+ engines: {node: '>=4'}
+
+ postcss@8.5.6:
+ resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
+ engines: {node: ^10 || ^12 || >=14}
+
+ prelude-ls@1.2.1:
+ resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
+ engines: {node: '>= 0.8.0'}
+
+ pretty-ms@9.2.0:
+ resolution: {integrity: sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg==}
+ engines: {node: '>=18'}
+
+ proxy-from-env@1.1.0:
+ resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
+
+ punycode@2.3.1:
+ resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
+ engines: {node: '>=6'}
+
+ queue-microtask@1.2.3:
+ resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
+
+ read-package-json-fast@4.0.0:
+ resolution: {integrity: sha512-qpt8EwugBWDw2cgE2W+/3oxC+KTez2uSVR8JU9Q36TXPAGCaozfQUs59v4j4GFpWTaw0i6hAZSvOmu1J0uOEUg==}
+ engines: {node: ^18.17.0 || >=20.5.0}
+
+ resolve-from@4.0.0:
+ resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
+ engines: {node: '>=4'}
+
+ reusify@1.1.0:
+ resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
+ engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
+
+ rfdc@1.4.1:
+ resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
+
+ rollup@4.46.4:
+ resolution: {integrity: sha512-YbxoxvoqNg9zAmw4+vzh1FkGAiZRK+LhnSrbSrSXMdZYsRPDWoshcSd/pldKRO6lWzv/e9TiJAVQyirYIeSIPQ==}
+ engines: {node: '>=18.0.0', npm: '>=8.0.0'}
+ hasBin: true
+
+ run-applescript@7.0.0:
+ resolution: {integrity: sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==}
+ engines: {node: '>=18'}
+
+ run-parallel@1.2.0:
+ resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
+
+ semver@6.3.1:
+ resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
+ hasBin: true
+
+ semver@7.7.2:
+ resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==}
+ engines: {node: '>=10'}
+ hasBin: true
+
+ shebang-command@2.0.0:
+ resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
+ engines: {node: '>=8'}
+
+ shebang-regex@3.0.0:
+ resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
+ engines: {node: '>=8'}
+
+ shell-quote@1.8.3:
+ resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==}
+ engines: {node: '>= 0.4'}
+
+ signal-exit@4.1.0:
+ resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
+ engines: {node: '>=14'}
+
+ sirv@3.0.1:
+ resolution: {integrity: sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==}
+ engines: {node: '>=18'}
+
+ source-map-js@1.2.1:
+ resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
+ engines: {node: '>=0.10.0'}
+
+ speakingurl@14.0.1:
+ resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==}
+ engines: {node: '>=0.10.0'}
+
+ strip-final-newline@4.0.0:
+ resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==}
+ engines: {node: '>=18'}
+
+ strip-json-comments@3.1.1:
+ resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
+ engines: {node: '>=8'}
+
+ superjson@2.2.2:
+ resolution: {integrity: sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==}
+ engines: {node: '>=16'}
+
+ supports-color@7.2.0:
+ resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
+ engines: {node: '>=8'}
+
+ tinyglobby@0.2.14:
+ resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==}
+ engines: {node: '>=12.0.0'}
+
+ to-regex-range@5.0.1:
+ resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
+ engines: {node: '>=8.0'}
+
+ totalist@3.0.1:
+ resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
+ engines: {node: '>=6'}
+
+ ts-api-utils@2.1.0:
+ resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==}
+ engines: {node: '>=18.12'}
+ peerDependencies:
+ typescript: '>=4.8.4'
+
+ type-check@0.4.0:
+ resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
+ engines: {node: '>= 0.8.0'}
+
+ typescript-eslint@8.40.0:
+ resolution: {integrity: sha512-Xvd2l+ZmFDPEt4oj1QEXzA4A2uUK6opvKu3eGN9aGjB8au02lIVcLyi375w94hHyejTOmzIU77L8ol2sRg9n7Q==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0
+ typescript: '>=4.8.4 <6.0.0'
+
+ typescript@5.8.3:
+ resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==}
+ engines: {node: '>=14.17'}
+ hasBin: true
+
+ undici-types@6.21.0:
+ resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
+
+ unicorn-magic@0.3.0:
+ resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==}
+ engines: {node: '>=18'}
+
+ unplugin-utils@0.2.5:
+ resolution: {integrity: sha512-gwXJnPRewT4rT7sBi/IvxKTjsms7jX7QIDLOClApuZwR49SXbrB1z2NLUZ+vDHyqCj/n58OzRRqaW+B8OZi8vg==}
+ engines: {node: '>=18.12.0'}
+
+ update-browserslist-db@1.1.3:
+ resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==}
+ hasBin: true
+ peerDependencies:
+ browserslist: '>= 4.21.0'
+
+ uri-js@4.4.1:
+ resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
+
+ util-deprecate@1.0.2:
+ resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
+
+ vite-dev-rpc@1.1.0:
+ resolution: {integrity: sha512-pKXZlgoXGoE8sEKiKJSng4hI1sQ4wi5YT24FCrwrLt6opmkjlqPPVmiPWWJn8M8byMxRGzp1CrFuqQs4M/Z39A==}
+ peerDependencies:
+ vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.1 || ^7.0.0-0
+
+ vite-hot-client@2.1.0:
+ resolution: {integrity: sha512-7SpgZmU7R+dDnSmvXE1mfDtnHLHQSisdySVR7lO8ceAXvM0otZeuQQ6C8LrS5d/aYyP/QZ0hI0L+dIPrm4YlFQ==}
+ peerDependencies:
+ vite: ^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0
+
+ vite-plugin-inspect@11.3.2:
+ resolution: {integrity: sha512-nzwvyFQg58XSMAmKVLr2uekAxNYvAbz1lyPmCAFVIBncCgN9S/HPM+2UM9Q9cvc4JEbC5ZBgwLAdaE2onmQuKg==}
+ engines: {node: '>=14'}
+ peerDependencies:
+ '@nuxt/kit': '*'
+ vite: ^6.0.0 || ^7.0.0-0
+ peerDependenciesMeta:
+ '@nuxt/kit':
+ optional: true
+
+ vite-plugin-vue-devtools@8.0.0:
+ resolution: {integrity: sha512-9bWQig8UMu3nPbxX86NJv56aelpFYoBHxB5+pxuQz3pa3Tajc1ezRidj/0dnADA4/UHuVIfwIVYHOvMXYcPshg==}
+ engines: {node: '>=v14.21.3'}
+ peerDependencies:
+ vite: ^6.0.0 || ^7.0.0-0
+
+ vite-plugin-vue-inspector@5.3.2:
+ resolution: {integrity: sha512-YvEKooQcSiBTAs0DoYLfefNja9bLgkFM7NI2b07bE2SruuvX0MEa9cMaxjKVMkeCp5Nz9FRIdcN1rOdFVBeL6Q==}
+ peerDependencies:
+ vite: ^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0
+
+ vite@7.1.3:
+ resolution: {integrity: sha512-OOUi5zjkDxYrKhTV3V7iKsoS37VUM7v40+HuwEmcrsf11Cdx9y3DIr2Px6liIcZFwt3XSRpQvFpL3WVy7ApkGw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+ peerDependencies:
+ '@types/node': ^20.19.0 || >=22.12.0
+ jiti: '>=1.21.0'
+ less: ^4.0.0
+ lightningcss: ^1.21.0
+ sass: ^1.70.0
+ sass-embedded: ^1.70.0
+ stylus: '>=0.54.8'
+ sugarss: ^5.0.0
+ terser: ^5.16.0
+ tsx: ^4.8.1
+ yaml: ^2.4.2
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+ jiti:
+ optional: true
+ less:
+ optional: true
+ lightningcss:
+ optional: true
+ sass:
+ optional: true
+ sass-embedded:
+ optional: true
+ stylus:
+ optional: true
+ sugarss:
+ optional: true
+ terser:
+ optional: true
+ tsx:
+ optional: true
+ yaml:
+ optional: true
+
+ vscode-uri@3.1.0:
+ resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==}
+
+ vue-eslint-parser@10.2.0:
+ resolution: {integrity: sha512-CydUvFOQKD928UzZhTp4pr2vWz1L+H99t7Pkln2QSPdvmURT0MoC4wUccfCnuEaihNsu9aYYyk+bep8rlfkUXw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0
+
+ vue-router@4.5.1:
+ resolution: {integrity: sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==}
+ peerDependencies:
+ vue: ^3.2.0
+
+ vue-tsc@3.0.6:
+ resolution: {integrity: sha512-Tbs8Whd43R2e2nxez4WXPvvdjGbW24rOSgRhLOHXzWiT4pcP4G7KeWh0YCn18rF4bVwv7tggLLZ6MJnO6jXPBg==}
+ hasBin: true
+ peerDependencies:
+ typescript: '>=5.0.0'
+
+ vue@3.5.18:
+ resolution: {integrity: sha512-7W4Y4ZbMiQ3SEo+m9lnoNpV9xG7QVMLa+/0RFwwiAVkeYoyGXqWE85jabU4pllJNUzqfLShJ5YLptewhCWUgNA==}
+ peerDependencies:
+ typescript: '*'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
+ which@2.0.2:
+ resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
+ engines: {node: '>= 8'}
+ hasBin: true
+
+ which@5.0.0:
+ resolution: {integrity: sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==}
+ engines: {node: ^18.17.0 || >=20.5.0}
+ hasBin: true
+
+ word-wrap@1.2.5:
+ resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
+ engines: {node: '>=0.10.0'}
+
+ wsl-utils@0.1.0:
+ resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==}
+ engines: {node: '>=18'}
+
+ xml-name-validator@4.0.0:
+ resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==}
+ engines: {node: '>=12'}
+
+ yallist@3.1.1:
+ resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
+
+ yocto-queue@0.1.0:
+ resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
+ engines: {node: '>=10'}
+
+ yoctocolors@2.1.2:
+ resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==}
+ engines: {node: '>=18'}
+
+snapshots:
+
+ '@ampproject/remapping@2.3.0':
+ dependencies:
+ '@jridgewell/gen-mapping': 0.3.13
+ '@jridgewell/trace-mapping': 0.3.30
+
+ '@babel/code-frame@7.27.1':
+ dependencies:
+ '@babel/helper-validator-identifier': 7.27.1
+ js-tokens: 4.0.0
+ picocolors: 1.1.1
+
+ '@babel/compat-data@7.28.0': {}
+
+ '@babel/core@7.28.3':
+ dependencies:
+ '@ampproject/remapping': 2.3.0
+ '@babel/code-frame': 7.27.1
+ '@babel/generator': 7.28.3
+ '@babel/helper-compilation-targets': 7.27.2
+ '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.3)
+ '@babel/helpers': 7.28.3
+ '@babel/parser': 7.28.3
+ '@babel/template': 7.27.2
+ '@babel/traverse': 7.28.3
+ '@babel/types': 7.28.2
+ convert-source-map: 2.0.0
+ debug: 4.4.1
+ gensync: 1.0.0-beta.2
+ json5: 2.2.3
+ semver: 6.3.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/generator@7.28.3':
+ dependencies:
+ '@babel/parser': 7.28.3
+ '@babel/types': 7.28.2
+ '@jridgewell/gen-mapping': 0.3.13
+ '@jridgewell/trace-mapping': 0.3.30
+ jsesc: 3.1.0
+
+ '@babel/helper-annotate-as-pure@7.27.3':
+ dependencies:
+ '@babel/types': 7.28.2
+
+ '@babel/helper-compilation-targets@7.27.2':
+ dependencies:
+ '@babel/compat-data': 7.28.0
+ '@babel/helper-validator-option': 7.27.1
+ browserslist: 4.25.3
+ lru-cache: 5.1.1
+ semver: 6.3.1
+
+ '@babel/helper-create-class-features-plugin@7.28.3(@babel/core@7.28.3)':
+ dependencies:
+ '@babel/core': 7.28.3
+ '@babel/helper-annotate-as-pure': 7.27.3
+ '@babel/helper-member-expression-to-functions': 7.27.1
+ '@babel/helper-optimise-call-expression': 7.27.1
+ '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.3)
+ '@babel/helper-skip-transparent-expression-wrappers': 7.27.1
+ '@babel/traverse': 7.28.3
+ semver: 6.3.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-globals@7.28.0': {}
+
+ '@babel/helper-member-expression-to-functions@7.27.1':
+ dependencies:
+ '@babel/traverse': 7.28.3
+ '@babel/types': 7.28.2
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-module-imports@7.27.1':
+ dependencies:
+ '@babel/traverse': 7.28.3
+ '@babel/types': 7.28.2
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.3)':
+ dependencies:
+ '@babel/core': 7.28.3
+ '@babel/helper-module-imports': 7.27.1
+ '@babel/helper-validator-identifier': 7.27.1
+ '@babel/traverse': 7.28.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-optimise-call-expression@7.27.1':
+ dependencies:
+ '@babel/types': 7.28.2
+
+ '@babel/helper-plugin-utils@7.27.1': {}
+
+ '@babel/helper-replace-supers@7.27.1(@babel/core@7.28.3)':
+ dependencies:
+ '@babel/core': 7.28.3
+ '@babel/helper-member-expression-to-functions': 7.27.1
+ '@babel/helper-optimise-call-expression': 7.27.1
+ '@babel/traverse': 7.28.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-skip-transparent-expression-wrappers@7.27.1':
+ dependencies:
+ '@babel/traverse': 7.28.3
+ '@babel/types': 7.28.2
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-string-parser@7.27.1': {}
+
+ '@babel/helper-validator-identifier@7.27.1': {}
+
+ '@babel/helper-validator-option@7.27.1': {}
+
+ '@babel/helpers@7.28.3':
+ dependencies:
+ '@babel/template': 7.27.2
+ '@babel/types': 7.28.2
+
+ '@babel/parser@7.28.3':
+ dependencies:
+ '@babel/types': 7.28.2
+
+ '@babel/plugin-proposal-decorators@7.28.0(@babel/core@7.28.3)':
+ dependencies:
+ '@babel/core': 7.28.3
+ '@babel/helper-create-class-features-plugin': 7.28.3(@babel/core@7.28.3)
+ '@babel/helper-plugin-utils': 7.27.1
+ '@babel/plugin-syntax-decorators': 7.27.1(@babel/core@7.28.3)
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-syntax-decorators@7.27.1(@babel/core@7.28.3)':
+ dependencies:
+ '@babel/core': 7.28.3
+ '@babel/helper-plugin-utils': 7.27.1
+
+ '@babel/plugin-syntax-import-attributes@7.27.1(@babel/core@7.28.3)':
+ dependencies:
+ '@babel/core': 7.28.3
+ '@babel/helper-plugin-utils': 7.27.1
+
+ '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.28.3)':
+ dependencies:
+ '@babel/core': 7.28.3
+ '@babel/helper-plugin-utils': 7.27.1
+
+ '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.28.3)':
+ dependencies:
+ '@babel/core': 7.28.3
+ '@babel/helper-plugin-utils': 7.27.1
+
+ '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.28.3)':
+ dependencies:
+ '@babel/core': 7.28.3
+ '@babel/helper-plugin-utils': 7.27.1
+
+ '@babel/plugin-transform-typescript@7.28.0(@babel/core@7.28.3)':
+ dependencies:
+ '@babel/core': 7.28.3
+ '@babel/helper-annotate-as-pure': 7.27.3
+ '@babel/helper-create-class-features-plugin': 7.28.3(@babel/core@7.28.3)
+ '@babel/helper-plugin-utils': 7.27.1
+ '@babel/helper-skip-transparent-expression-wrappers': 7.27.1
+ '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.3)
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/template@7.27.2':
+ dependencies:
+ '@babel/code-frame': 7.27.1
+ '@babel/parser': 7.28.3
+ '@babel/types': 7.28.2
+
+ '@babel/traverse@7.28.3':
+ dependencies:
+ '@babel/code-frame': 7.27.1
+ '@babel/generator': 7.28.3
+ '@babel/helper-globals': 7.28.0
+ '@babel/parser': 7.28.3
+ '@babel/template': 7.27.2
+ '@babel/types': 7.28.2
+ debug: 4.4.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/types@7.28.2':
+ dependencies:
+ '@babel/helper-string-parser': 7.27.1
+ '@babel/helper-validator-identifier': 7.27.1
+
+ '@esbuild/aix-ppc64@0.25.9':
+ optional: true
+
+ '@esbuild/android-arm64@0.25.9':
+ optional: true
+
+ '@esbuild/android-arm@0.25.9':
+ optional: true
+
+ '@esbuild/android-x64@0.25.9':
+ optional: true
+
+ '@esbuild/darwin-arm64@0.25.9':
+ optional: true
+
+ '@esbuild/darwin-x64@0.25.9':
+ optional: true
+
+ '@esbuild/freebsd-arm64@0.25.9':
+ optional: true
+
+ '@esbuild/freebsd-x64@0.25.9':
+ optional: true
+
+ '@esbuild/linux-arm64@0.25.9':
+ optional: true
+
+ '@esbuild/linux-arm@0.25.9':
+ optional: true
+
+ '@esbuild/linux-ia32@0.25.9':
+ optional: true
+
+ '@esbuild/linux-loong64@0.25.9':
+ optional: true
+
+ '@esbuild/linux-mips64el@0.25.9':
+ optional: true
+
+ '@esbuild/linux-ppc64@0.25.9':
+ optional: true
+
+ '@esbuild/linux-riscv64@0.25.9':
+ optional: true
+
+ '@esbuild/linux-s390x@0.25.9':
+ optional: true
+
+ '@esbuild/linux-x64@0.25.9':
+ optional: true
+
+ '@esbuild/netbsd-arm64@0.25.9':
+ optional: true
+
+ '@esbuild/netbsd-x64@0.25.9':
+ optional: true
+
+ '@esbuild/openbsd-arm64@0.25.9':
+ optional: true
+
+ '@esbuild/openbsd-x64@0.25.9':
+ optional: true
+
+ '@esbuild/openharmony-arm64@0.25.9':
+ optional: true
+
+ '@esbuild/sunos-x64@0.25.9':
+ optional: true
+
+ '@esbuild/win32-arm64@0.25.9':
+ optional: true
+
+ '@esbuild/win32-ia32@0.25.9':
+ optional: true
+
+ '@esbuild/win32-x64@0.25.9':
+ optional: true
+
+ '@eslint-community/eslint-utils@4.7.0(eslint@9.33.0(jiti@2.5.1))':
+ dependencies:
+ eslint: 9.33.0(jiti@2.5.1)
+ eslint-visitor-keys: 3.4.3
+
+ '@eslint-community/regexpp@4.12.1': {}
+
+ '@eslint/config-array@0.21.0':
+ dependencies:
+ '@eslint/object-schema': 2.1.6
+ debug: 4.4.1
+ minimatch: 3.1.2
+ transitivePeerDependencies:
+ - supports-color
+
+ '@eslint/config-helpers@0.3.1': {}
+
+ '@eslint/core@0.15.2':
+ dependencies:
+ '@types/json-schema': 7.0.15
+
+ '@eslint/eslintrc@3.3.1':
+ dependencies:
+ ajv: 6.12.6
+ debug: 4.4.1
+ espree: 10.4.0
+ globals: 14.0.0
+ ignore: 5.3.2
+ import-fresh: 3.3.1
+ js-yaml: 4.1.0
+ minimatch: 3.1.2
+ strip-json-comments: 3.1.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@eslint/js@9.33.0': {}
+
+ '@eslint/object-schema@2.1.6': {}
+
+ '@eslint/plugin-kit@0.3.5':
+ dependencies:
+ '@eslint/core': 0.15.2
+ levn: 0.4.1
+
+ '@humanfs/core@0.19.1': {}
+
+ '@humanfs/node@0.16.6':
+ dependencies:
+ '@humanfs/core': 0.19.1
+ '@humanwhocodes/retry': 0.3.1
+
+ '@humanwhocodes/module-importer@1.0.1': {}
+
+ '@humanwhocodes/retry@0.3.1': {}
+
+ '@humanwhocodes/retry@0.4.3': {}
+
+ '@jridgewell/gen-mapping@0.3.13':
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.5.5
+ '@jridgewell/trace-mapping': 0.3.30
+
+ '@jridgewell/resolve-uri@3.1.2': {}
+
+ '@jridgewell/sourcemap-codec@1.5.5': {}
+
+ '@jridgewell/trace-mapping@0.3.30':
+ 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
+
+ '@polka/url@1.0.0-next.29': {}
+
+ '@rolldown/pluginutils@1.0.0-beta.29': {}
+
+ '@rollup/rollup-android-arm-eabi@4.46.4':
+ optional: true
+
+ '@rollup/rollup-android-arm64@4.46.4':
+ optional: true
+
+ '@rollup/rollup-darwin-arm64@4.46.4':
+ optional: true
+
+ '@rollup/rollup-darwin-x64@4.46.4':
+ optional: true
+
+ '@rollup/rollup-freebsd-arm64@4.46.4':
+ optional: true
+
+ '@rollup/rollup-freebsd-x64@4.46.4':
+ optional: true
+
+ '@rollup/rollup-linux-arm-gnueabihf@4.46.4':
+ optional: true
+
+ '@rollup/rollup-linux-arm-musleabihf@4.46.4':
+ optional: true
+
+ '@rollup/rollup-linux-arm64-gnu@4.46.4':
+ optional: true
+
+ '@rollup/rollup-linux-arm64-musl@4.46.4':
+ optional: true
+
+ '@rollup/rollup-linux-loongarch64-gnu@4.46.4':
+ optional: true
+
+ '@rollup/rollup-linux-ppc64-gnu@4.46.4':
+ optional: true
+
+ '@rollup/rollup-linux-riscv64-gnu@4.46.4':
+ optional: true
+
+ '@rollup/rollup-linux-riscv64-musl@4.46.4':
+ optional: true
+
+ '@rollup/rollup-linux-s390x-gnu@4.46.4':
+ optional: true
+
+ '@rollup/rollup-linux-x64-gnu@4.46.4':
+ optional: true
+
+ '@rollup/rollup-linux-x64-musl@4.46.4':
+ optional: true
+
+ '@rollup/rollup-win32-arm64-msvc@4.46.4':
+ optional: true
+
+ '@rollup/rollup-win32-ia32-msvc@4.46.4':
+ optional: true
+
+ '@rollup/rollup-win32-x64-msvc@4.46.4':
+ optional: true
+
+ '@sec-ant/readable-stream@0.4.1': {}
+
+ '@sindresorhus/merge-streams@4.0.0': {}
+
+ '@tsconfig/node22@22.0.2': {}
+
+ '@types/estree@1.0.8': {}
+
+ '@types/json-schema@7.0.15': {}
+
+ '@types/node@22.17.2':
+ dependencies:
+ undici-types: 6.21.0
+
+ '@typescript-eslint/eslint-plugin@8.40.0(@typescript-eslint/parser@8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3))(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3)':
+ dependencies:
+ '@eslint-community/regexpp': 4.12.1
+ '@typescript-eslint/parser': 8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3)
+ '@typescript-eslint/scope-manager': 8.40.0
+ '@typescript-eslint/type-utils': 8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3)
+ '@typescript-eslint/utils': 8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3)
+ '@typescript-eslint/visitor-keys': 8.40.0
+ eslint: 9.33.0(jiti@2.5.1)
+ graphemer: 1.4.0
+ ignore: 7.0.5
+ natural-compare: 1.4.0
+ ts-api-utils: 2.1.0(typescript@5.8.3)
+ typescript: 5.8.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/parser@8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3)':
+ dependencies:
+ '@typescript-eslint/scope-manager': 8.40.0
+ '@typescript-eslint/types': 8.40.0
+ '@typescript-eslint/typescript-estree': 8.40.0(typescript@5.8.3)
+ '@typescript-eslint/visitor-keys': 8.40.0
+ debug: 4.4.1
+ eslint: 9.33.0(jiti@2.5.1)
+ typescript: 5.8.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/project-service@8.40.0(typescript@5.8.3)':
+ dependencies:
+ '@typescript-eslint/tsconfig-utils': 8.40.0(typescript@5.8.3)
+ '@typescript-eslint/types': 8.40.0
+ debug: 4.4.1
+ typescript: 5.8.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/scope-manager@8.40.0':
+ dependencies:
+ '@typescript-eslint/types': 8.40.0
+ '@typescript-eslint/visitor-keys': 8.40.0
+
+ '@typescript-eslint/tsconfig-utils@8.40.0(typescript@5.8.3)':
+ dependencies:
+ typescript: 5.8.3
+
+ '@typescript-eslint/type-utils@8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3)':
+ dependencies:
+ '@typescript-eslint/types': 8.40.0
+ '@typescript-eslint/typescript-estree': 8.40.0(typescript@5.8.3)
+ '@typescript-eslint/utils': 8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3)
+ debug: 4.4.1
+ eslint: 9.33.0(jiti@2.5.1)
+ ts-api-utils: 2.1.0(typescript@5.8.3)
+ typescript: 5.8.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/types@8.40.0': {}
+
+ '@typescript-eslint/typescript-estree@8.40.0(typescript@5.8.3)':
+ dependencies:
+ '@typescript-eslint/project-service': 8.40.0(typescript@5.8.3)
+ '@typescript-eslint/tsconfig-utils': 8.40.0(typescript@5.8.3)
+ '@typescript-eslint/types': 8.40.0
+ '@typescript-eslint/visitor-keys': 8.40.0
+ debug: 4.4.1
+ fast-glob: 3.3.3
+ is-glob: 4.0.3
+ minimatch: 9.0.5
+ semver: 7.7.2
+ ts-api-utils: 2.1.0(typescript@5.8.3)
+ typescript: 5.8.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/utils@8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3)':
+ dependencies:
+ '@eslint-community/eslint-utils': 4.7.0(eslint@9.33.0(jiti@2.5.1))
+ '@typescript-eslint/scope-manager': 8.40.0
+ '@typescript-eslint/types': 8.40.0
+ '@typescript-eslint/typescript-estree': 8.40.0(typescript@5.8.3)
+ eslint: 9.33.0(jiti@2.5.1)
+ typescript: 5.8.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/visitor-keys@8.40.0':
+ dependencies:
+ '@typescript-eslint/types': 8.40.0
+ eslint-visitor-keys: 4.2.1
+
+ '@vitejs/plugin-vue@6.0.1(vite@7.1.3(@types/node@22.17.2)(jiti@2.5.1))(vue@3.5.18(typescript@5.8.3))':
+ dependencies:
+ '@rolldown/pluginutils': 1.0.0-beta.29
+ vite: 7.1.3(@types/node@22.17.2)(jiti@2.5.1)
+ vue: 3.5.18(typescript@5.8.3)
+
+ '@volar/language-core@2.4.23':
+ dependencies:
+ '@volar/source-map': 2.4.23
+
+ '@volar/source-map@2.4.23': {}
+
+ '@volar/typescript@2.4.23':
+ dependencies:
+ '@volar/language-core': 2.4.23
+ path-browserify: 1.0.1
+ vscode-uri: 3.1.0
+
+ '@vue/babel-helper-vue-transform-on@1.5.0': {}
+
+ '@vue/babel-plugin-jsx@1.5.0(@babel/core@7.28.3)':
+ dependencies:
+ '@babel/helper-module-imports': 7.27.1
+ '@babel/helper-plugin-utils': 7.27.1
+ '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.3)
+ '@babel/template': 7.27.2
+ '@babel/traverse': 7.28.3
+ '@babel/types': 7.28.2
+ '@vue/babel-helper-vue-transform-on': 1.5.0
+ '@vue/babel-plugin-resolve-type': 1.5.0(@babel/core@7.28.3)
+ '@vue/shared': 3.5.18
+ optionalDependencies:
+ '@babel/core': 7.28.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@vue/babel-plugin-resolve-type@1.5.0(@babel/core@7.28.3)':
+ dependencies:
+ '@babel/code-frame': 7.27.1
+ '@babel/core': 7.28.3
+ '@babel/helper-module-imports': 7.27.1
+ '@babel/helper-plugin-utils': 7.27.1
+ '@babel/parser': 7.28.3
+ '@vue/compiler-sfc': 3.5.18
+ transitivePeerDependencies:
+ - supports-color
+
+ '@vue/compiler-core@3.5.18':
+ dependencies:
+ '@babel/parser': 7.28.3
+ '@vue/shared': 3.5.18
+ entities: 4.5.0
+ estree-walker: 2.0.2
+ source-map-js: 1.2.1
+
+ '@vue/compiler-dom@3.5.18':
+ dependencies:
+ '@vue/compiler-core': 3.5.18
+ '@vue/shared': 3.5.18
+
+ '@vue/compiler-sfc@3.5.18':
+ dependencies:
+ '@babel/parser': 7.28.3
+ '@vue/compiler-core': 3.5.18
+ '@vue/compiler-dom': 3.5.18
+ '@vue/compiler-ssr': 3.5.18
+ '@vue/shared': 3.5.18
+ estree-walker: 2.0.2
+ magic-string: 0.30.17
+ postcss: 8.5.6
+ source-map-js: 1.2.1
+
+ '@vue/compiler-ssr@3.5.18':
+ dependencies:
+ '@vue/compiler-dom': 3.5.18
+ '@vue/shared': 3.5.18
+
+ '@vue/compiler-vue2@2.7.16':
+ dependencies:
+ de-indent: 1.0.2
+ he: 1.2.0
+
+ '@vue/devtools-api@6.6.4': {}
+
+ '@vue/devtools-api@7.7.7':
+ dependencies:
+ '@vue/devtools-kit': 7.7.7
+
+ '@vue/devtools-core@8.0.0(vite@7.1.3(@types/node@22.17.2)(jiti@2.5.1))(vue@3.5.18(typescript@5.8.3))':
+ dependencies:
+ '@vue/devtools-kit': 8.0.0
+ '@vue/devtools-shared': 8.0.0
+ mitt: 3.0.1
+ nanoid: 5.1.5
+ pathe: 2.0.3
+ vite-hot-client: 2.1.0(vite@7.1.3(@types/node@22.17.2)(jiti@2.5.1))
+ vue: 3.5.18(typescript@5.8.3)
+ transitivePeerDependencies:
+ - vite
+
+ '@vue/devtools-kit@7.7.7':
+ dependencies:
+ '@vue/devtools-shared': 7.7.7
+ birpc: 2.5.0
+ hookable: 5.5.3
+ mitt: 3.0.1
+ perfect-debounce: 1.0.0
+ speakingurl: 14.0.1
+ superjson: 2.2.2
+
+ '@vue/devtools-kit@8.0.0':
+ dependencies:
+ '@vue/devtools-shared': 8.0.0
+ birpc: 2.5.0
+ hookable: 5.5.3
+ mitt: 3.0.1
+ perfect-debounce: 1.0.0
+ speakingurl: 14.0.1
+ superjson: 2.2.2
+
+ '@vue/devtools-shared@7.7.7':
+ dependencies:
+ rfdc: 1.4.1
+
+ '@vue/devtools-shared@8.0.0':
+ dependencies:
+ rfdc: 1.4.1
+
+ '@vue/eslint-config-typescript@14.6.0(eslint-plugin-vue@10.3.0(@typescript-eslint/parser@8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3))(eslint@9.33.0(jiti@2.5.1))(vue-eslint-parser@10.2.0(eslint@9.33.0(jiti@2.5.1))))(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3)':
+ dependencies:
+ '@typescript-eslint/utils': 8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3)
+ eslint: 9.33.0(jiti@2.5.1)
+ eslint-plugin-vue: 10.3.0(@typescript-eslint/parser@8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3))(eslint@9.33.0(jiti@2.5.1))(vue-eslint-parser@10.2.0(eslint@9.33.0(jiti@2.5.1)))
+ fast-glob: 3.3.3
+ typescript-eslint: 8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3)
+ vue-eslint-parser: 10.2.0(eslint@9.33.0(jiti@2.5.1))
+ optionalDependencies:
+ typescript: 5.8.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@vue/language-core@3.0.6(typescript@5.8.3)':
+ dependencies:
+ '@volar/language-core': 2.4.23
+ '@vue/compiler-dom': 3.5.18
+ '@vue/compiler-vue2': 2.7.16
+ '@vue/shared': 3.5.18
+ alien-signals: 2.0.7
+ muggle-string: 0.4.1
+ path-browserify: 1.0.1
+ picomatch: 4.0.3
+ optionalDependencies:
+ typescript: 5.8.3
+
+ '@vue/reactivity@3.5.18':
+ dependencies:
+ '@vue/shared': 3.5.18
+
+ '@vue/runtime-core@3.5.18':
+ dependencies:
+ '@vue/reactivity': 3.5.18
+ '@vue/shared': 3.5.18
+
+ '@vue/runtime-dom@3.5.18':
+ dependencies:
+ '@vue/reactivity': 3.5.18
+ '@vue/runtime-core': 3.5.18
+ '@vue/shared': 3.5.18
+ csstype: 3.1.3
+
+ '@vue/server-renderer@3.5.18(vue@3.5.18(typescript@5.8.3))':
+ dependencies:
+ '@vue/compiler-ssr': 3.5.18
+ '@vue/shared': 3.5.18
+ vue: 3.5.18(typescript@5.8.3)
+
+ '@vue/shared@3.5.18': {}
+
+ '@vue/tsconfig@0.7.0(typescript@5.8.3)(vue@3.5.18(typescript@5.8.3))':
+ optionalDependencies:
+ typescript: 5.8.3
+ vue: 3.5.18(typescript@5.8.3)
+
+ acorn-jsx@5.3.2(acorn@8.15.0):
+ dependencies:
+ acorn: 8.15.0
+
+ acorn@8.15.0: {}
+
+ ajv@6.12.6:
+ dependencies:
+ fast-deep-equal: 3.1.3
+ fast-json-stable-stringify: 2.1.0
+ json-schema-traverse: 0.4.1
+ uri-js: 4.4.1
+
+ alien-signals@2.0.7: {}
+
+ ansi-styles@4.3.0:
+ dependencies:
+ color-convert: 2.0.1
+
+ ansi-styles@6.2.1: {}
+
+ ansis@4.1.0: {}
+
+ argparse@2.0.1: {}
+
+ asynckit@0.4.0: {}
+
+ axios@1.11.0:
+ dependencies:
+ follow-redirects: 1.15.11
+ form-data: 4.0.4
+ proxy-from-env: 1.1.0
+ transitivePeerDependencies:
+ - debug
+
+ balanced-match@1.0.2: {}
+
+ birpc@2.5.0: {}
+
+ boolbase@1.0.0: {}
+
+ brace-expansion@1.1.12:
+ dependencies:
+ balanced-match: 1.0.2
+ concat-map: 0.0.1
+
+ brace-expansion@2.0.2:
+ dependencies:
+ balanced-match: 1.0.2
+
+ braces@3.0.3:
+ dependencies:
+ fill-range: 7.1.1
+
+ browserslist@4.25.3:
+ dependencies:
+ caniuse-lite: 1.0.30001735
+ electron-to-chromium: 1.5.207
+ node-releases: 2.0.19
+ update-browserslist-db: 1.1.3(browserslist@4.25.3)
+
+ bundle-name@4.1.0:
+ dependencies:
+ run-applescript: 7.0.0
+
+ call-bind-apply-helpers@1.0.2:
+ dependencies:
+ es-errors: 1.3.0
+ function-bind: 1.1.2
+
+ callsites@3.1.0: {}
+
+ caniuse-lite@1.0.30001735: {}
+
+ chalk@4.1.2:
+ dependencies:
+ ansi-styles: 4.3.0
+ supports-color: 7.2.0
+
+ color-convert@2.0.1:
+ dependencies:
+ color-name: 1.1.4
+
+ color-name@1.1.4: {}
+
+ combined-stream@1.0.8:
+ dependencies:
+ delayed-stream: 1.0.0
+
+ concat-map@0.0.1: {}
+
+ convert-source-map@2.0.0: {}
+
+ copy-anything@3.0.5:
+ dependencies:
+ is-what: 4.1.16
+
+ cross-spawn@7.0.6:
+ dependencies:
+ path-key: 3.1.1
+ shebang-command: 2.0.0
+ which: 2.0.2
+
+ cssesc@3.0.0: {}
+
+ csstype@3.1.3: {}
+
+ de-indent@1.0.2: {}
+
+ debug@4.4.1:
+ dependencies:
+ ms: 2.1.3
+
+ deep-is@0.1.4: {}
+
+ default-browser-id@5.0.0: {}
+
+ default-browser@5.2.1:
+ dependencies:
+ bundle-name: 4.1.0
+ default-browser-id: 5.0.0
+
+ define-lazy-prop@3.0.0: {}
+
+ delayed-stream@1.0.0: {}
+
+ 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.207: {}
+
+ entities@4.5.0: {}
+
+ error-stack-parser-es@1.0.5: {}
+
+ 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.25.9:
+ optionalDependencies:
+ '@esbuild/aix-ppc64': 0.25.9
+ '@esbuild/android-arm': 0.25.9
+ '@esbuild/android-arm64': 0.25.9
+ '@esbuild/android-x64': 0.25.9
+ '@esbuild/darwin-arm64': 0.25.9
+ '@esbuild/darwin-x64': 0.25.9
+ '@esbuild/freebsd-arm64': 0.25.9
+ '@esbuild/freebsd-x64': 0.25.9
+ '@esbuild/linux-arm': 0.25.9
+ '@esbuild/linux-arm64': 0.25.9
+ '@esbuild/linux-ia32': 0.25.9
+ '@esbuild/linux-loong64': 0.25.9
+ '@esbuild/linux-mips64el': 0.25.9
+ '@esbuild/linux-ppc64': 0.25.9
+ '@esbuild/linux-riscv64': 0.25.9
+ '@esbuild/linux-s390x': 0.25.9
+ '@esbuild/linux-x64': 0.25.9
+ '@esbuild/netbsd-arm64': 0.25.9
+ '@esbuild/netbsd-x64': 0.25.9
+ '@esbuild/openbsd-arm64': 0.25.9
+ '@esbuild/openbsd-x64': 0.25.9
+ '@esbuild/openharmony-arm64': 0.25.9
+ '@esbuild/sunos-x64': 0.25.9
+ '@esbuild/win32-arm64': 0.25.9
+ '@esbuild/win32-ia32': 0.25.9
+ '@esbuild/win32-x64': 0.25.9
+
+ escalade@3.2.0: {}
+
+ escape-string-regexp@4.0.0: {}
+
+ eslint-plugin-vue@10.3.0(@typescript-eslint/parser@8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3))(eslint@9.33.0(jiti@2.5.1))(vue-eslint-parser@10.2.0(eslint@9.33.0(jiti@2.5.1))):
+ dependencies:
+ '@eslint-community/eslint-utils': 4.7.0(eslint@9.33.0(jiti@2.5.1))
+ eslint: 9.33.0(jiti@2.5.1)
+ natural-compare: 1.4.0
+ nth-check: 2.1.1
+ postcss-selector-parser: 6.1.2
+ semver: 7.7.2
+ vue-eslint-parser: 10.2.0(eslint@9.33.0(jiti@2.5.1))
+ xml-name-validator: 4.0.0
+ optionalDependencies:
+ '@typescript-eslint/parser': 8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3)
+
+ eslint-scope@8.4.0:
+ dependencies:
+ esrecurse: 4.3.0
+ estraverse: 5.3.0
+
+ eslint-visitor-keys@3.4.3: {}
+
+ eslint-visitor-keys@4.2.1: {}
+
+ eslint@9.33.0(jiti@2.5.1):
+ dependencies:
+ '@eslint-community/eslint-utils': 4.7.0(eslint@9.33.0(jiti@2.5.1))
+ '@eslint-community/regexpp': 4.12.1
+ '@eslint/config-array': 0.21.0
+ '@eslint/config-helpers': 0.3.1
+ '@eslint/core': 0.15.2
+ '@eslint/eslintrc': 3.3.1
+ '@eslint/js': 9.33.0
+ '@eslint/plugin-kit': 0.3.5
+ '@humanfs/node': 0.16.6
+ '@humanwhocodes/module-importer': 1.0.1
+ '@humanwhocodes/retry': 0.4.3
+ '@types/estree': 1.0.8
+ '@types/json-schema': 7.0.15
+ ajv: 6.12.6
+ chalk: 4.1.2
+ cross-spawn: 7.0.6
+ debug: 4.4.1
+ escape-string-regexp: 4.0.0
+ eslint-scope: 8.4.0
+ eslint-visitor-keys: 4.2.1
+ espree: 10.4.0
+ esquery: 1.6.0
+ esutils: 2.0.3
+ fast-deep-equal: 3.1.3
+ file-entry-cache: 8.0.0
+ find-up: 5.0.0
+ glob-parent: 6.0.2
+ ignore: 5.3.2
+ imurmurhash: 0.1.4
+ is-glob: 4.0.3
+ json-stable-stringify-without-jsonify: 1.0.1
+ lodash.merge: 4.6.2
+ minimatch: 3.1.2
+ natural-compare: 1.4.0
+ optionator: 0.9.4
+ optionalDependencies:
+ jiti: 2.5.1
+ transitivePeerDependencies:
+ - supports-color
+
+ espree@10.4.0:
+ dependencies:
+ acorn: 8.15.0
+ acorn-jsx: 5.3.2(acorn@8.15.0)
+ eslint-visitor-keys: 4.2.1
+
+ esquery@1.6.0:
+ dependencies:
+ estraverse: 5.3.0
+
+ esrecurse@4.3.0:
+ dependencies:
+ estraverse: 5.3.0
+
+ estraverse@5.3.0: {}
+
+ estree-walker@2.0.2: {}
+
+ esutils@2.0.3: {}
+
+ execa@9.6.0:
+ dependencies:
+ '@sindresorhus/merge-streams': 4.0.0
+ cross-spawn: 7.0.6
+ figures: 6.1.0
+ get-stream: 9.0.1
+ human-signals: 8.0.1
+ is-plain-obj: 4.1.0
+ is-stream: 4.0.1
+ npm-run-path: 6.0.0
+ pretty-ms: 9.2.0
+ signal-exit: 4.1.0
+ strip-final-newline: 4.0.0
+ yoctocolors: 2.1.2
+
+ fast-deep-equal@3.1.3: {}
+
+ fast-glob@3.3.3:
+ dependencies:
+ '@nodelib/fs.stat': 2.0.5
+ '@nodelib/fs.walk': 1.2.8
+ glob-parent: 5.1.2
+ merge2: 1.4.1
+ micromatch: 4.0.8
+
+ fast-json-stable-stringify@2.1.0: {}
+
+ fast-levenshtein@2.0.6: {}
+
+ fastq@1.19.1:
+ dependencies:
+ reusify: 1.1.0
+
+ fdir@6.5.0(picomatch@4.0.3):
+ optionalDependencies:
+ picomatch: 4.0.3
+
+ figures@6.1.0:
+ dependencies:
+ is-unicode-supported: 2.1.0
+
+ file-entry-cache@8.0.0:
+ dependencies:
+ flat-cache: 4.0.1
+
+ fill-range@7.1.1:
+ dependencies:
+ to-regex-range: 5.0.1
+
+ find-up@5.0.0:
+ dependencies:
+ locate-path: 6.0.0
+ path-exists: 4.0.0
+
+ flat-cache@4.0.1:
+ dependencies:
+ flatted: 3.3.3
+ keyv: 4.5.4
+
+ flatted@3.3.3: {}
+
+ follow-redirects@1.15.11: {}
+
+ form-data@4.0.4:
+ 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
+
+ fsevents@2.3.3:
+ optional: true
+
+ function-bind@1.1.2: {}
+
+ gensync@1.0.0-beta.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
+
+ get-stream@9.0.1:
+ dependencies:
+ '@sec-ant/readable-stream': 0.4.1
+ is-stream: 4.0.1
+
+ glob-parent@5.1.2:
+ dependencies:
+ is-glob: 4.0.3
+
+ glob-parent@6.0.2:
+ dependencies:
+ is-glob: 4.0.3
+
+ globals@14.0.0: {}
+
+ gopd@1.2.0: {}
+
+ graphemer@1.4.0: {}
+
+ has-flag@4.0.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: {}
+
+ hookable@5.5.3: {}
+
+ human-signals@8.0.1: {}
+
+ ignore@5.3.2: {}
+
+ ignore@7.0.5: {}
+
+ import-fresh@3.3.1:
+ dependencies:
+ parent-module: 1.0.1
+ resolve-from: 4.0.0
+
+ imurmurhash@0.1.4: {}
+
+ is-docker@3.0.0: {}
+
+ is-extglob@2.1.1: {}
+
+ is-glob@4.0.3:
+ dependencies:
+ is-extglob: 2.1.1
+
+ is-inside-container@1.0.0:
+ dependencies:
+ is-docker: 3.0.0
+
+ is-number@7.0.0: {}
+
+ is-plain-obj@4.1.0: {}
+
+ is-stream@4.0.1: {}
+
+ is-unicode-supported@2.1.0: {}
+
+ is-what@4.1.16: {}
+
+ is-wsl@3.1.0:
+ dependencies:
+ is-inside-container: 1.0.0
+
+ isexe@2.0.0: {}
+
+ isexe@3.1.1: {}
+
+ jiti@2.5.1: {}
+
+ js-tokens@4.0.0: {}
+
+ js-yaml@4.1.0:
+ dependencies:
+ argparse: 2.0.1
+
+ jsesc@3.1.0: {}
+
+ json-buffer@3.0.1: {}
+
+ json-parse-even-better-errors@4.0.0: {}
+
+ json-schema-traverse@0.4.1: {}
+
+ json-stable-stringify-without-jsonify@1.0.1: {}
+
+ json5@2.2.3: {}
+
+ keyv@4.5.4:
+ dependencies:
+ json-buffer: 3.0.1
+
+ kolorist@1.8.0: {}
+
+ levn@0.4.1:
+ dependencies:
+ prelude-ls: 1.2.1
+ type-check: 0.4.0
+
+ locate-path@6.0.0:
+ dependencies:
+ p-locate: 5.0.0
+
+ lodash.merge@4.6.2: {}
+
+ lru-cache@5.1.1:
+ dependencies:
+ yallist: 3.1.1
+
+ magic-string@0.30.17:
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.5.5
+
+ math-intrinsics@1.1.0: {}
+
+ memorystream@0.3.1: {}
+
+ 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@3.1.2:
+ dependencies:
+ brace-expansion: 1.1.12
+
+ minimatch@9.0.5:
+ dependencies:
+ brace-expansion: 2.0.2
+
+ mitt@3.0.1: {}
+
+ mrmime@2.0.1: {}
+
+ ms@2.1.3: {}
+
+ muggle-string@0.4.1: {}
+
+ nanoid@3.3.11: {}
+
+ nanoid@5.1.5: {}
+
+ natural-compare@1.4.0: {}
+
+ node-releases@2.0.19: {}
+
+ npm-normalize-package-bin@4.0.0: {}
+
+ npm-run-all2@8.0.4:
+ dependencies:
+ ansi-styles: 6.2.1
+ cross-spawn: 7.0.6
+ memorystream: 0.3.1
+ picomatch: 4.0.3
+ pidtree: 0.6.0
+ read-package-json-fast: 4.0.0
+ shell-quote: 1.8.3
+ which: 5.0.0
+
+ npm-run-path@6.0.0:
+ dependencies:
+ path-key: 4.0.0
+ unicorn-magic: 0.3.0
+
+ nth-check@2.1.1:
+ dependencies:
+ boolbase: 1.0.0
+
+ ohash@2.0.11: {}
+
+ open@10.2.0:
+ dependencies:
+ default-browser: 5.2.1
+ define-lazy-prop: 3.0.0
+ is-inside-container: 1.0.0
+ wsl-utils: 0.1.0
+
+ optionator@0.9.4:
+ dependencies:
+ deep-is: 0.1.4
+ fast-levenshtein: 2.0.6
+ levn: 0.4.1
+ prelude-ls: 1.2.1
+ type-check: 0.4.0
+ word-wrap: 1.2.5
+
+ p-limit@3.1.0:
+ dependencies:
+ yocto-queue: 0.1.0
+
+ p-locate@5.0.0:
+ dependencies:
+ p-limit: 3.1.0
+
+ parent-module@1.0.1:
+ dependencies:
+ callsites: 3.1.0
+
+ parse-ms@4.0.0: {}
+
+ path-browserify@1.0.1: {}
+
+ path-exists@4.0.0: {}
+
+ path-key@3.1.1: {}
+
+ path-key@4.0.0: {}
+
+ pathe@2.0.3: {}
+
+ perfect-debounce@1.0.0: {}
+
+ picocolors@1.1.1: {}
+
+ picomatch@2.3.1: {}
+
+ picomatch@4.0.3: {}
+
+ pidtree@0.6.0: {}
+
+ pinia@3.0.3(typescript@5.8.3)(vue@3.5.18(typescript@5.8.3)):
+ dependencies:
+ '@vue/devtools-api': 7.7.7
+ vue: 3.5.18(typescript@5.8.3)
+ optionalDependencies:
+ typescript: 5.8.3
+
+ postcss-selector-parser@6.1.2:
+ dependencies:
+ cssesc: 3.0.0
+ util-deprecate: 1.0.2
+
+ postcss@8.5.6:
+ dependencies:
+ nanoid: 3.3.11
+ picocolors: 1.1.1
+ source-map-js: 1.2.1
+
+ prelude-ls@1.2.1: {}
+
+ pretty-ms@9.2.0:
+ dependencies:
+ parse-ms: 4.0.0
+
+ proxy-from-env@1.1.0: {}
+
+ punycode@2.3.1: {}
+
+ queue-microtask@1.2.3: {}
+
+ read-package-json-fast@4.0.0:
+ dependencies:
+ json-parse-even-better-errors: 4.0.0
+ npm-normalize-package-bin: 4.0.0
+
+ resolve-from@4.0.0: {}
+
+ reusify@1.1.0: {}
+
+ rfdc@1.4.1: {}
+
+ rollup@4.46.4:
+ dependencies:
+ '@types/estree': 1.0.8
+ optionalDependencies:
+ '@rollup/rollup-android-arm-eabi': 4.46.4
+ '@rollup/rollup-android-arm64': 4.46.4
+ '@rollup/rollup-darwin-arm64': 4.46.4
+ '@rollup/rollup-darwin-x64': 4.46.4
+ '@rollup/rollup-freebsd-arm64': 4.46.4
+ '@rollup/rollup-freebsd-x64': 4.46.4
+ '@rollup/rollup-linux-arm-gnueabihf': 4.46.4
+ '@rollup/rollup-linux-arm-musleabihf': 4.46.4
+ '@rollup/rollup-linux-arm64-gnu': 4.46.4
+ '@rollup/rollup-linux-arm64-musl': 4.46.4
+ '@rollup/rollup-linux-loongarch64-gnu': 4.46.4
+ '@rollup/rollup-linux-ppc64-gnu': 4.46.4
+ '@rollup/rollup-linux-riscv64-gnu': 4.46.4
+ '@rollup/rollup-linux-riscv64-musl': 4.46.4
+ '@rollup/rollup-linux-s390x-gnu': 4.46.4
+ '@rollup/rollup-linux-x64-gnu': 4.46.4
+ '@rollup/rollup-linux-x64-musl': 4.46.4
+ '@rollup/rollup-win32-arm64-msvc': 4.46.4
+ '@rollup/rollup-win32-ia32-msvc': 4.46.4
+ '@rollup/rollup-win32-x64-msvc': 4.46.4
+ fsevents: 2.3.3
+
+ run-applescript@7.0.0: {}
+
+ run-parallel@1.2.0:
+ dependencies:
+ queue-microtask: 1.2.3
+
+ semver@6.3.1: {}
+
+ semver@7.7.2: {}
+
+ shebang-command@2.0.0:
+ dependencies:
+ shebang-regex: 3.0.0
+
+ shebang-regex@3.0.0: {}
+
+ shell-quote@1.8.3: {}
+
+ signal-exit@4.1.0: {}
+
+ sirv@3.0.1:
+ dependencies:
+ '@polka/url': 1.0.0-next.29
+ mrmime: 2.0.1
+ totalist: 3.0.1
+
+ source-map-js@1.2.1: {}
+
+ speakingurl@14.0.1: {}
+
+ strip-final-newline@4.0.0: {}
+
+ strip-json-comments@3.1.1: {}
+
+ superjson@2.2.2:
+ dependencies:
+ copy-anything: 3.0.5
+
+ supports-color@7.2.0:
+ dependencies:
+ has-flag: 4.0.0
+
+ tinyglobby@0.2.14:
+ 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
+
+ totalist@3.0.1: {}
+
+ ts-api-utils@2.1.0(typescript@5.8.3):
+ dependencies:
+ typescript: 5.8.3
+
+ type-check@0.4.0:
+ dependencies:
+ prelude-ls: 1.2.1
+
+ typescript-eslint@8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3):
+ dependencies:
+ '@typescript-eslint/eslint-plugin': 8.40.0(@typescript-eslint/parser@8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3))(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3)
+ '@typescript-eslint/parser': 8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3)
+ '@typescript-eslint/typescript-estree': 8.40.0(typescript@5.8.3)
+ '@typescript-eslint/utils': 8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3)
+ eslint: 9.33.0(jiti@2.5.1)
+ typescript: 5.8.3
+ transitivePeerDependencies:
+ - supports-color
+
+ typescript@5.8.3: {}
+
+ undici-types@6.21.0: {}
+
+ unicorn-magic@0.3.0: {}
+
+ unplugin-utils@0.2.5:
+ dependencies:
+ pathe: 2.0.3
+ picomatch: 4.0.3
+
+ update-browserslist-db@1.1.3(browserslist@4.25.3):
+ dependencies:
+ browserslist: 4.25.3
+ escalade: 3.2.0
+ picocolors: 1.1.1
+
+ uri-js@4.4.1:
+ dependencies:
+ punycode: 2.3.1
+
+ util-deprecate@1.0.2: {}
+
+ vite-dev-rpc@1.1.0(vite@7.1.3(@types/node@22.17.2)(jiti@2.5.1)):
+ dependencies:
+ birpc: 2.5.0
+ vite: 7.1.3(@types/node@22.17.2)(jiti@2.5.1)
+ vite-hot-client: 2.1.0(vite@7.1.3(@types/node@22.17.2)(jiti@2.5.1))
+
+ vite-hot-client@2.1.0(vite@7.1.3(@types/node@22.17.2)(jiti@2.5.1)):
+ dependencies:
+ vite: 7.1.3(@types/node@22.17.2)(jiti@2.5.1)
+
+ vite-plugin-inspect@11.3.2(vite@7.1.3(@types/node@22.17.2)(jiti@2.5.1)):
+ dependencies:
+ ansis: 4.1.0
+ debug: 4.4.1
+ error-stack-parser-es: 1.0.5
+ ohash: 2.0.11
+ open: 10.2.0
+ perfect-debounce: 1.0.0
+ sirv: 3.0.1
+ unplugin-utils: 0.2.5
+ vite: 7.1.3(@types/node@22.17.2)(jiti@2.5.1)
+ vite-dev-rpc: 1.1.0(vite@7.1.3(@types/node@22.17.2)(jiti@2.5.1))
+ transitivePeerDependencies:
+ - supports-color
+
+ vite-plugin-vue-devtools@8.0.0(vite@7.1.3(@types/node@22.17.2)(jiti@2.5.1))(vue@3.5.18(typescript@5.8.3)):
+ dependencies:
+ '@vue/devtools-core': 8.0.0(vite@7.1.3(@types/node@22.17.2)(jiti@2.5.1))(vue@3.5.18(typescript@5.8.3))
+ '@vue/devtools-kit': 8.0.0
+ '@vue/devtools-shared': 8.0.0
+ execa: 9.6.0
+ sirv: 3.0.1
+ vite: 7.1.3(@types/node@22.17.2)(jiti@2.5.1)
+ vite-plugin-inspect: 11.3.2(vite@7.1.3(@types/node@22.17.2)(jiti@2.5.1))
+ vite-plugin-vue-inspector: 5.3.2(vite@7.1.3(@types/node@22.17.2)(jiti@2.5.1))
+ transitivePeerDependencies:
+ - '@nuxt/kit'
+ - supports-color
+ - vue
+
+ vite-plugin-vue-inspector@5.3.2(vite@7.1.3(@types/node@22.17.2)(jiti@2.5.1)):
+ dependencies:
+ '@babel/core': 7.28.3
+ '@babel/plugin-proposal-decorators': 7.28.0(@babel/core@7.28.3)
+ '@babel/plugin-syntax-import-attributes': 7.27.1(@babel/core@7.28.3)
+ '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.28.3)
+ '@babel/plugin-transform-typescript': 7.28.0(@babel/core@7.28.3)
+ '@vue/babel-plugin-jsx': 1.5.0(@babel/core@7.28.3)
+ '@vue/compiler-dom': 3.5.18
+ kolorist: 1.8.0
+ magic-string: 0.30.17
+ vite: 7.1.3(@types/node@22.17.2)(jiti@2.5.1)
+ transitivePeerDependencies:
+ - supports-color
+
+ vite@7.1.3(@types/node@22.17.2)(jiti@2.5.1):
+ dependencies:
+ esbuild: 0.25.9
+ fdir: 6.5.0(picomatch@4.0.3)
+ picomatch: 4.0.3
+ postcss: 8.5.6
+ rollup: 4.46.4
+ tinyglobby: 0.2.14
+ optionalDependencies:
+ '@types/node': 22.17.2
+ fsevents: 2.3.3
+ jiti: 2.5.1
+
+ vscode-uri@3.1.0: {}
+
+ vue-eslint-parser@10.2.0(eslint@9.33.0(jiti@2.5.1)):
+ dependencies:
+ debug: 4.4.1
+ eslint: 9.33.0(jiti@2.5.1)
+ eslint-scope: 8.4.0
+ eslint-visitor-keys: 4.2.1
+ espree: 10.4.0
+ esquery: 1.6.0
+ semver: 7.7.2
+ transitivePeerDependencies:
+ - supports-color
+
+ vue-router@4.5.1(vue@3.5.18(typescript@5.8.3)):
+ dependencies:
+ '@vue/devtools-api': 6.6.4
+ vue: 3.5.18(typescript@5.8.3)
+
+ vue-tsc@3.0.6(typescript@5.8.3):
+ dependencies:
+ '@volar/typescript': 2.4.23
+ '@vue/language-core': 3.0.6(typescript@5.8.3)
+ typescript: 5.8.3
+
+ vue@3.5.18(typescript@5.8.3):
+ dependencies:
+ '@vue/compiler-dom': 3.5.18
+ '@vue/compiler-sfc': 3.5.18
+ '@vue/runtime-dom': 3.5.18
+ '@vue/server-renderer': 3.5.18(vue@3.5.18(typescript@5.8.3))
+ '@vue/shared': 3.5.18
+ optionalDependencies:
+ typescript: 5.8.3
+
+ which@2.0.2:
+ dependencies:
+ isexe: 2.0.0
+
+ which@5.0.0:
+ dependencies:
+ isexe: 3.1.1
+
+ word-wrap@1.2.5: {}
+
+ wsl-utils@0.1.0:
+ dependencies:
+ is-wsl: 3.1.0
+
+ xml-name-validator@4.0.0: {}
+
+ yallist@3.1.1: {}
+
+ yocto-queue@0.1.0: {}
+
+ yoctocolors@2.1.2: {}
diff --git a/ui/public/favicon.ico b/ui/public/favicon.ico
new file mode 100644
index 0000000..df36fcf
Binary files /dev/null and b/ui/public/favicon.ico differ
diff --git a/ui/src/App.vue b/ui/src/App.vue
new file mode 100644
index 0000000..4a2c269
--- /dev/null
+++ b/ui/src/App.vue
@@ -0,0 +1,225 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/src/assets/logo.svg b/ui/src/assets/logo.svg
new file mode 100644
index 0000000..7565660
--- /dev/null
+++ b/ui/src/assets/logo.svg
@@ -0,0 +1 @@
+
diff --git a/ui/src/assets/main.css b/ui/src/assets/main.css
new file mode 100644
index 0000000..91b3a2c
--- /dev/null
+++ b/ui/src/assets/main.css
@@ -0,0 +1,281 @@
+/* 全局样式重置 */
+* {
+ box-sizing: border-box;
+}
+
+html, body {
+ margin: 0;
+ padding: 0;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
+ 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
+ sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ line-height: 1.6;
+ color: #1f2937;
+}
+
+/* 全局按钮样式 */
+.btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0.75rem 1.5rem;
+ border-radius: 0.5rem;
+ font-weight: 600;
+ text-decoration: none;
+ transition: all 0.2s;
+ border: none;
+ cursor: pointer;
+ font-size: 1rem;
+}
+
+.btn:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+}
+
+.btn-primary {
+ background: #3b82f6;
+ color: white;
+}
+
+.btn-primary:hover:not(:disabled) {
+ background: #2563eb;
+ transform: translateY(-1px);
+}
+
+.btn-secondary {
+ background: #f3f4f6;
+ color: #374151;
+ border: 1px solid #d1d5db;
+}
+
+.btn-secondary:hover:not(:disabled) {
+ background: #e5e7eb;
+}
+
+.btn-danger {
+ background: #ef4444;
+ color: white;
+}
+
+.btn-danger:hover:not(:disabled) {
+ background: #dc2626;
+}
+
+.btn-text {
+ background: none;
+ color: #6b7280;
+ padding: 0.5rem 1rem;
+}
+
+.btn-text:hover {
+ color: #374151;
+ background: #f3f4f6;
+}
+
+/* 全局容器样式 */
+.container {
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 0 2rem;
+}
+
+/* 响应式设计 */
+@media (max-width: 768px) {
+ .container {
+ padding: 0 1rem;
+ }
+}
+
+/* 滚动条样式 */
+::-webkit-scrollbar {
+ width: 8px;
+ height: 8px;
+}
+
+::-webkit-scrollbar-track {
+ background: #f1f1f1;
+ border-radius: 4px;
+}
+
+::-webkit-scrollbar-thumb {
+ background: #c1c1c1;
+ border-radius: 4px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background: #a8a8a8;
+}
+
+/* 工具类 */
+.text-center {
+ text-align: center;
+}
+
+.text-left {
+ text-align: left;
+}
+
+.text-right {
+ text-align: right;
+}
+
+.mt-1 { margin-top: 0.25rem; }
+.mt-2 { margin-top: 0.5rem; }
+.mt-3 { margin-top: 0.75rem; }
+.mt-4 { margin-top: 1rem; }
+.mt-5 { margin-top: 1.25rem; }
+.mt-6 { margin-top: 1.5rem; }
+
+.mb-1 { margin-bottom: 0.25rem; }
+.mb-2 { margin-bottom: 0.5rem; }
+.mb-3 { margin-bottom: 0.75rem; }
+.mb-4 { margin-bottom: 1rem; }
+.mb-5 { margin-bottom: 1.25rem; }
+.mb-6 { margin-bottom: 1.5rem; }
+
+.ml-1 { margin-left: 0.25rem; }
+.ml-2 { margin-left: 0.5rem; }
+.ml-3 { margin-left: 0.75rem; }
+.ml-4 { margin-left: 1rem; }
+
+.mr-1 { margin-right: 0.25rem; }
+.mr-2 { margin-right: 0.5rem; }
+.mr-3 { margin-right: 0.75rem; }
+.mr-4 { margin-right: 1rem; }
+
+.p-1 { padding: 0.25rem; }
+.p-2 { padding: 0.5rem; }
+.p-3 { padding: 0.75rem; }
+.p-4 { padding: 1rem; }
+.p-5 { padding: 1.25rem; }
+.p-6 { padding: 1.5rem; }
+
+.flex {
+ display: flex;
+}
+
+.flex-col {
+ flex-direction: column;
+}
+
+.items-center {
+ align-items: center;
+}
+
+.justify-center {
+ justify-content: center;
+}
+
+.justify-between {
+ justify-content: space-between;
+}
+
+.gap-1 { gap: 0.25rem; }
+.gap-2 { gap: 0.5rem; }
+.gap-3 { gap: 0.75rem; }
+.gap-4 { gap: 1rem; }
+.gap-5 { gap: 1.25rem; }
+.gap-6 { gap: 1.5rem; }
+
+.w-full {
+ width: 100%;
+}
+
+.h-full {
+ height: 100%;
+}
+
+.rounded {
+ border-radius: 0.25rem;
+}
+
+.rounded-lg {
+ border-radius: 0.5rem;
+}
+
+.rounded-xl {
+ border-radius: 0.75rem;
+}
+
+.shadow {
+ box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
+}
+
+.shadow-lg {
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
+}
+
+.bg-white {
+ background-color: white;
+}
+
+.bg-gray-50 {
+ background-color: #f9fafb;
+}
+
+.bg-gray-100 {
+ background-color: #f3f4f6;
+}
+
+.text-gray-500 {
+ color: #6b7280;
+}
+
+.text-gray-600 {
+ color: #4b5563;
+}
+
+.text-gray-700 {
+ color: #374151;
+}
+
+.text-gray-900 {
+ color: #111827;
+}
+
+.text-blue-600 {
+ color: #2563eb;
+}
+
+.text-red-600 {
+ color: #dc2626;
+}
+
+.text-green-600 {
+ color: #059669;
+}
+
+.text-sm {
+ font-size: 0.875rem;
+}
+
+.text-base {
+ font-size: 1rem;
+}
+
+.text-lg {
+ font-size: 1.125rem;
+}
+
+.text-xl {
+ font-size: 1.25rem;
+}
+
+.text-2xl {
+ font-size: 1.5rem;
+}
+
+.font-medium {
+ font-weight: 500;
+}
+
+.font-semibold {
+ font-weight: 600;
+}
+
+.font-bold {
+ font-weight: 700;
+}
diff --git a/ui/src/components/TheWelcome.vue b/ui/src/components/TheWelcome.vue
new file mode 100644
index 0000000..6092dff
--- /dev/null
+++ b/ui/src/components/TheWelcome.vue
@@ -0,0 +1,94 @@
+
+
+
+
+
+
+
+ Documentation
+
+ Vue’s
+ official documentation
+ provides you with all information you need to get started.
+
+
+
+
+
+
+ Tooling
+
+ This project is served and bundled with
+ Vite. The
+ recommended IDE setup is
+ VSCode
+ +
+ Vue - Official. If
+ you need to test your components and web pages, check out
+ Vitest
+ and
+ Cypress
+ /
+ Playwright.
+
+
+
+ More instructions are available in
+ README.md.
+
+
+
+
+
+
+ Ecosystem
+
+ Get official tools and libraries for your project:
+ Pinia,
+ Vue Router,
+ Vue Test Utils, and
+ Vue Dev Tools. If
+ you need more resources, we suggest paying
+ Awesome Vue
+ a visit.
+
+
+
+
+
+
+ Community
+
+ Got stuck? Ask your question on
+ Vue Land
+ (our official Discord server), or
+ StackOverflow. You should also follow the official
+ @vuejs.org
+ Bluesky account or the
+ @vuejs
+ X account for latest news in the Vue world.
+
+
+
+
+
+
+ Support Vue
+
+ As an independent project, Vue relies on community backing for its sustainability. You can help
+ us by
+ becoming a sponsor.
+
+
diff --git a/ui/src/components/WelcomeItem.vue b/ui/src/components/WelcomeItem.vue
new file mode 100644
index 0000000..6d7086a
--- /dev/null
+++ b/ui/src/components/WelcomeItem.vue
@@ -0,0 +1,87 @@
+
+
+
+
+
diff --git a/ui/src/components/artwork/ArtworkCard.vue b/ui/src/components/artwork/ArtworkCard.vue
new file mode 100644
index 0000000..8e2bad7
--- /dev/null
+++ b/ui/src/components/artwork/ArtworkCard.vue
@@ -0,0 +1,227 @@
+
+
+
+
![]()
+
+
+
+
+ 图片加载失败
+
+
+
+
+
+ {{ artwork.title }}
+
+
+
+
+
+
+ {{ tag.name }}
+
+
+ +{{ artwork.tags.length - 3 }}
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ui/src/components/common/ErrorMessage.vue b/ui/src/components/common/ErrorMessage.vue
new file mode 100644
index 0000000..a14ae91
--- /dev/null
+++ b/ui/src/components/common/ErrorMessage.vue
@@ -0,0 +1,110 @@
+
+
+
+
+
{{ title }}
+
{{ error }}
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ui/src/components/common/LoadingSpinner.vue b/ui/src/components/common/LoadingSpinner.vue
new file mode 100644
index 0000000..e0e91c5
--- /dev/null
+++ b/ui/src/components/common/LoadingSpinner.vue
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ui/src/components/icons/IconCommunity.vue b/ui/src/components/icons/IconCommunity.vue
new file mode 100644
index 0000000..2dc8b05
--- /dev/null
+++ b/ui/src/components/icons/IconCommunity.vue
@@ -0,0 +1,7 @@
+
+
+
diff --git a/ui/src/components/icons/IconDocumentation.vue b/ui/src/components/icons/IconDocumentation.vue
new file mode 100644
index 0000000..6d4791c
--- /dev/null
+++ b/ui/src/components/icons/IconDocumentation.vue
@@ -0,0 +1,7 @@
+
+
+
diff --git a/ui/src/components/icons/IconEcosystem.vue b/ui/src/components/icons/IconEcosystem.vue
new file mode 100644
index 0000000..c3a4f07
--- /dev/null
+++ b/ui/src/components/icons/IconEcosystem.vue
@@ -0,0 +1,7 @@
+
+
+
diff --git a/ui/src/components/icons/IconSupport.vue b/ui/src/components/icons/IconSupport.vue
new file mode 100644
index 0000000..7452834
--- /dev/null
+++ b/ui/src/components/icons/IconSupport.vue
@@ -0,0 +1,7 @@
+
+
+
diff --git a/ui/src/components/icons/IconTooling.vue b/ui/src/components/icons/IconTooling.vue
new file mode 100644
index 0000000..660598d
--- /dev/null
+++ b/ui/src/components/icons/IconTooling.vue
@@ -0,0 +1,19 @@
+
+
+
+
diff --git a/ui/src/main.ts b/ui/src/main.ts
new file mode 100644
index 0000000..5dcad83
--- /dev/null
+++ b/ui/src/main.ts
@@ -0,0 +1,14 @@
+import './assets/main.css'
+
+import { createApp } from 'vue'
+import { createPinia } from 'pinia'
+
+import App from './App.vue'
+import router from './router'
+
+const app = createApp(App)
+
+app.use(createPinia())
+app.use(router)
+
+app.mount('#app')
diff --git a/ui/src/router/index.ts b/ui/src/router/index.ts
new file mode 100644
index 0000000..5d1cfcd
--- /dev/null
+++ b/ui/src/router/index.ts
@@ -0,0 +1,63 @@
+import { createRouter, createWebHistory } from 'vue-router'
+import HomeView from '../views/HomeView.vue'
+import LoginView from '../views/LoginView.vue'
+
+const router = createRouter({
+ history: createWebHistory(import.meta.env.BASE_URL),
+ routes: [
+ {
+ path: '/',
+ name: 'home',
+ component: HomeView
+ },
+ {
+ path: '/login',
+ name: 'login',
+ component: LoginView
+ },
+ {
+ path: '/search',
+ name: 'search',
+ component: () => import('../views/SearchView.vue'),
+ meta: { requiresAuth: true }
+ },
+ {
+ path: '/artwork/:id',
+ name: 'artwork',
+ component: () => import('../views/ArtworkView.vue'),
+ meta: { requiresAuth: true }
+ },
+ {
+ path: '/artist/:id',
+ name: 'artist',
+ component: () => import('../views/ArtistView.vue'),
+ meta: { requiresAuth: true }
+ },
+ {
+ path: '/downloads',
+ name: 'downloads',
+ component: () => import('../views/DownloadsView.vue'),
+ meta: { requiresAuth: true }
+ },
+ {
+ path: '/artists',
+ name: 'artists',
+ component: () => import('../views/ArtistsView.vue'),
+ meta: { requiresAuth: true }
+ }
+ ]
+})
+
+// 路由守卫
+router.beforeEach(async (to, from, next) => {
+ // 检查是否需要认证
+ if (to.meta.requiresAuth) {
+ // 这里可以添加认证检查逻辑
+ // 暂时直接放行,后续可以集成认证状态检查
+ next()
+ } else {
+ next()
+ }
+})
+
+export default router
diff --git a/ui/src/services/api.ts b/ui/src/services/api.ts
new file mode 100644
index 0000000..85a3d61
--- /dev/null
+++ b/ui/src/services/api.ts
@@ -0,0 +1,91 @@
+import axios, { type AxiosInstance, type AxiosRequestConfig, type AxiosResponse } from 'axios';
+import type { ApiResponse } from '@/types';
+
+// API配置
+const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000';
+
+class ApiService {
+ private client: AxiosInstance;
+
+ constructor() {
+ this.client = axios.create({
+ baseURL: API_BASE_URL,
+ timeout: 30000,
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ // 请求拦截器
+ this.client.interceptors.request.use(
+ (config) => {
+ // 可以在这里添加认证token等
+ return config;
+ },
+ (error) => {
+ return Promise.reject(error);
+ }
+ );
+
+ // 响应拦截器
+ this.client.interceptors.response.use(
+ (response: AxiosResponse) => {
+ return response;
+ },
+ (error) => {
+ // 统一错误处理
+ if (error.response) {
+ const { status, data } = error.response;
+ console.error(`API Error ${status}:`, data);
+ } else if (error.request) {
+ console.error('Network Error:', error.request);
+ } else {
+ console.error('Request Error:', error.message);
+ }
+ return Promise.reject(error);
+ }
+ );
+ }
+
+ /**
+ * GET请求
+ */
+ async get(url: string, config?: AxiosRequestConfig): Promise> {
+ const response = await this.client.get>(url, config);
+ return response.data;
+ }
+
+ /**
+ * POST请求
+ */
+ async post(url: string, data?: any, config?: AxiosRequestConfig): Promise> {
+ const response = await this.client.post>(url, data, config);
+ return response.data;
+ }
+
+ /**
+ * PUT请求
+ */
+ async put(url: string, data?: any, config?: AxiosRequestConfig): Promise> {
+ const response = await this.client.put>(url, data, config);
+ return response.data;
+ }
+
+ /**
+ * DELETE请求
+ */
+ async delete(url: string, config?: AxiosRequestConfig): Promise> {
+ const response = await this.client.delete>(url, config);
+ return response.data;
+ }
+
+ /**
+ * 健康检查
+ */
+ async healthCheck(): Promise {
+ return this.get('/health');
+ }
+}
+
+export const apiService = new ApiService();
+export default apiService;
\ No newline at end of file
diff --git a/ui/src/services/artist.ts b/ui/src/services/artist.ts
new file mode 100644
index 0000000..9a68435
--- /dev/null
+++ b/ui/src/services/artist.ts
@@ -0,0 +1,81 @@
+import apiService from './api';
+import type { ApiResponse, Artist, User } from '@/types';
+
+export interface ArtistArtworksOptions {
+ type?: 'art' | 'manga' | 'novel';
+ filter?: 'for_ios' | 'for_android';
+ offset?: number;
+ limit?: number;
+}
+
+export interface ArtistFollowingOptions {
+ restrict?: 'public' | 'private';
+ offset?: number;
+ limit?: number;
+}
+
+export interface ArtistFollowersOptions {
+ offset?: number;
+ limit?: number;
+}
+
+class ArtistService {
+ /**
+ * 获取作者信息
+ */
+ async getArtistInfo(id: number): Promise> {
+ return apiService.get(`/api/artist/${id}`);
+ }
+
+ /**
+ * 获取作者作品列表
+ */
+ async getArtistArtworks(id: number, options: ArtistArtworksOptions = {}): Promise> {
+ const params = new URLSearchParams();
+ if (options.type) params.append('type', options.type);
+ if (options.filter) params.append('filter', options.filter);
+ if (options.offset !== undefined) params.append('offset', options.offset.toString());
+ if (options.limit !== undefined) params.append('limit', options.limit.toString());
+
+ const query = params.toString();
+ const url = query ? `/api/artist/${id}/artworks?${query}` : `/api/artist/${id}/artworks`;
+ return apiService.get<{ artworks: any[]; next_url?: string; total: number }>(url);
+ }
+
+ /**
+ * 获取作者关注列表
+ */
+ async getArtistFollowing(id: number, options: ArtistFollowingOptions = {}): Promise> {
+ const params = new URLSearchParams();
+ if (options.restrict) params.append('restrict', options.restrict);
+ if (options.offset !== undefined) params.append('offset', options.offset.toString());
+ if (options.limit !== undefined) params.append('limit', options.limit.toString());
+
+ const query = params.toString();
+ const url = query ? `/api/artist/${id}/following?${query}` : `/api/artist/${id}/following`;
+ return apiService.get<{ users: User[]; next_url?: string; total: number }>(url);
+ }
+
+ /**
+ * 获取作者粉丝列表
+ */
+ async getArtistFollowers(id: number, options: ArtistFollowersOptions = {}): Promise> {
+ const params = new URLSearchParams();
+ if (options.offset !== undefined) params.append('offset', options.offset.toString());
+ if (options.limit !== undefined) params.append('limit', options.limit.toString());
+
+ const query = params.toString();
+ const url = query ? `/api/artist/${id}/followers?${query}` : `/api/artist/${id}/followers`;
+ return apiService.get<{ users: User[]; next_url?: string; total: number }>(url);
+ }
+
+ /**
+ * 关注/取消关注作者
+ */
+ async followArtist(id: number, action: 'follow' | 'unfollow'): Promise {
+ return apiService.post(`/api/artist/${id}/follow`, { action });
+ }
+}
+
+export const artistService = new ArtistService();
+export default artistService;
\ No newline at end of file
diff --git a/ui/src/services/artwork.ts b/ui/src/services/artwork.ts
new file mode 100644
index 0000000..d757cb8
--- /dev/null
+++ b/ui/src/services/artwork.ts
@@ -0,0 +1,71 @@
+import apiService from './api';
+import type { ApiResponse, Artwork, SearchParams, PaginatedResponse } from '@/types';
+
+export interface ArtworkDetailOptions {
+ include_user?: boolean;
+ include_series?: boolean;
+}
+
+export interface ArtworkImagesResponse {
+ artwork_id: number;
+ total_pages: number;
+ images: Array<{
+ page: number;
+ original: string;
+ large: string;
+ medium: string;
+ square_medium: string;
+ }>;
+ selected_size: string;
+}
+
+class ArtworkService {
+ /**
+ * 获取作品详情
+ */
+ async getArtworkDetail(id: number, options: ArtworkDetailOptions = {}): Promise> {
+ const params = new URLSearchParams();
+ if (options.include_user !== undefined) {
+ params.append('include_user', options.include_user.toString());
+ }
+ if (options.include_series !== undefined) {
+ params.append('include_series', options.include_series.toString());
+ }
+
+ const query = params.toString();
+ const url = query ? `/api/artwork/${id}?${query}` : `/api/artwork/${id}`;
+ return apiService.get(url);
+ }
+
+ /**
+ * 获取作品预览信息
+ */
+ async getArtworkPreview(id: number): Promise> {
+ return apiService.get(`/api/artwork/${id}/preview`);
+ }
+
+ /**
+ * 获取作品图片URL
+ */
+ async getArtworkImages(id: number, size: string = 'medium'): Promise> {
+ return apiService.get(`/api/artwork/${id}/images?size=${size}`);
+ }
+
+ /**
+ * 搜索作品
+ */
+ async searchArtworks(params: SearchParams): Promise> {
+ const queryParams = new URLSearchParams();
+ queryParams.append('keyword', params.keyword);
+ if (params.type) queryParams.append('type', params.type);
+ if (params.sort) queryParams.append('sort', params.sort);
+ if (params.duration) queryParams.append('duration', params.duration);
+ if (params.offset !== undefined) queryParams.append('offset', params.offset.toString());
+ if (params.limit !== undefined) queryParams.append('limit', params.limit.toString());
+
+ return apiService.get<{ artworks: Artwork[]; next_url?: string; total: number }>(`/api/artwork/search?${queryParams.toString()}`);
+ }
+}
+
+export const artworkService = new ArtworkService();
+export default artworkService;
\ No newline at end of file
diff --git a/ui/src/services/auth.ts b/ui/src/services/auth.ts
new file mode 100644
index 0000000..6ca16dc
--- /dev/null
+++ b/ui/src/services/auth.ts
@@ -0,0 +1,59 @@
+import apiService from './api';
+import type { ApiResponse, LoginStatus } from '@/types';
+
+export interface LoginUrlResponse {
+ login_url: string;
+ code_verifier: string;
+}
+
+export interface LoginCallbackRequest {
+ code: string;
+}
+
+export interface LoginCallbackResponse {
+ user: {
+ id: number;
+ name: string;
+ account: string;
+ };
+}
+
+class AuthService {
+ /**
+ * 获取登录状态
+ */
+ async getLoginStatus(): Promise> {
+ return apiService.get('/api/auth/status');
+ }
+
+ /**
+ * 获取登录URL
+ */
+ async getLoginUrl(): Promise> {
+ return apiService.get('/api/auth/login-url');
+ }
+
+ /**
+ * 处理登录回调
+ */
+ async handleLoginCallback(code: string): Promise> {
+ return apiService.post('/api/auth/callback', { code });
+ }
+
+ /**
+ * 重新登录
+ */
+ async relogin(): Promise {
+ return apiService.post('/api/auth/relogin');
+ }
+
+ /**
+ * 登出
+ */
+ async logout(): Promise {
+ return apiService.post('/api/auth/logout');
+ }
+}
+
+export const authService = new AuthService();
+export default authService;
\ No newline at end of file
diff --git a/ui/src/services/download.ts b/ui/src/services/download.ts
new file mode 100644
index 0000000..6b6c649
--- /dev/null
+++ b/ui/src/services/download.ts
@@ -0,0 +1,66 @@
+import apiService from './api';
+import type { ApiResponse, DownloadTask, DownloadParams } from '@/types';
+
+export interface DownloadArtworkRequest extends DownloadParams {
+ size?: 'original' | 'large' | 'medium' | 'square_medium';
+ quality?: 'high' | 'medium' | 'low';
+ format?: 'auto' | 'jpg' | 'png';
+}
+
+export interface DownloadMultipleRequest extends DownloadParams {
+ artworkIds: number[];
+ concurrent?: number;
+}
+
+export interface DownloadArtistRequest extends DownloadParams {
+ type?: 'art' | 'manga' | 'novel';
+ filter?: 'for_ios' | 'for_android';
+ limit?: number;
+}
+
+class DownloadService {
+ /**
+ * 下载单个作品
+ */
+ async downloadArtwork(id: number, params: DownloadArtworkRequest = {}): Promise> {
+ return apiService.post(`/api/download/artwork/${id}`, params);
+ }
+
+ /**
+ * 批量下载作品
+ */
+ async downloadMultipleArtworks(params: DownloadMultipleRequest): Promise> {
+ return apiService.post('/api/download/artworks', params);
+ }
+
+ /**
+ * 下载作者作品
+ */
+ async downloadArtistArtworks(id: number, params: DownloadArtistRequest = {}): Promise> {
+ return apiService.post(`/api/download/artist/${id}`, params);
+ }
+
+ /**
+ * 获取下载进度
+ */
+ async getDownloadProgress(taskId: string): Promise> {
+ return apiService.get(`/api/download/progress/${taskId}`);
+ }
+
+ /**
+ * 取消下载任务
+ */
+ async cancelDownload(taskId: string): Promise {
+ return apiService.delete(`/api/download/cancel/${taskId}`);
+ }
+
+ /**
+ * 获取下载历史
+ */
+ async getDownloadHistory(offset: number = 0, limit: number = 20): Promise> {
+ return apiService.get<{ tasks: DownloadTask[]; total: number; offset: number; limit: number }>(`/api/download/history?offset=${offset}&limit=${limit}`);
+ }
+}
+
+export const downloadService = new DownloadService();
+export default downloadService;
\ No newline at end of file
diff --git a/ui/src/stores/auth.ts b/ui/src/stores/auth.ts
new file mode 100644
index 0000000..4889783
--- /dev/null
+++ b/ui/src/stores/auth.ts
@@ -0,0 +1,139 @@
+import { defineStore } from 'pinia';
+import { ref, computed } from 'vue';
+import authService from '@/services/auth';
+import type { LoginStatus } from '@/types';
+
+export const useAuthStore = defineStore('auth', () => {
+ // 状态
+ const loginStatus = ref({
+ isLoggedIn: false
+ });
+ const loading = ref(false);
+ const error = ref(null);
+
+ // 计算属性
+ const isLoggedIn = computed(() => loginStatus.value.isLoggedIn);
+ const username = computed(() => loginStatus.value.username);
+ const userId = computed(() => loginStatus.value.user_id);
+
+ // 获取登录状态
+ const fetchLoginStatus = async () => {
+ try {
+ loading.value = true;
+ error.value = null;
+ const response = await authService.getLoginStatus();
+ if (response.success && response.data) {
+ loginStatus.value = response.data;
+ }
+ } catch (err) {
+ error.value = err instanceof Error ? err.message : '获取登录状态失败';
+ console.error('获取登录状态失败:', err);
+ } finally {
+ loading.value = false;
+ }
+ };
+
+ // 获取登录URL
+ const getLoginUrl = async () => {
+ try {
+ loading.value = true;
+ error.value = null;
+ const response = await authService.getLoginUrl();
+ if (response.success && response.data) {
+ return response.data.login_url;
+ }
+ throw new Error(response.error || '获取登录URL失败');
+ } catch (err) {
+ error.value = err instanceof Error ? err.message : '获取登录URL失败';
+ console.error('获取登录URL失败:', err);
+ throw err;
+ } finally {
+ loading.value = false;
+ }
+ };
+
+ // 处理登录回调
+ const handleLoginCallback = async (code: string) => {
+ try {
+ loading.value = true;
+ error.value = null;
+ const response = await authService.handleLoginCallback(code);
+ if (response.success) {
+ await fetchLoginStatus(); // 重新获取登录状态
+ return response.data;
+ }
+ throw new Error(response.error || '登录失败');
+ } catch (err) {
+ error.value = err instanceof Error ? err.message : '登录失败';
+ console.error('登录失败:', err);
+ throw err;
+ } finally {
+ loading.value = false;
+ }
+ };
+
+ // 重新登录
+ const relogin = async () => {
+ try {
+ loading.value = true;
+ error.value = null;
+ const response = await authService.relogin();
+ if (response.success) {
+ await fetchLoginStatus(); // 重新获取登录状态
+ return true;
+ }
+ throw new Error(response.error || '重新登录失败');
+ } catch (err) {
+ error.value = err instanceof Error ? err.message : '重新登录失败';
+ console.error('重新登录失败:', err);
+ throw err;
+ } finally {
+ loading.value = false;
+ }
+ };
+
+ // 登出
+ const logout = async () => {
+ try {
+ loading.value = true;
+ error.value = null;
+ const response = await authService.logout();
+ if (response.success) {
+ loginStatus.value = { isLoggedIn: false };
+ return true;
+ }
+ throw new Error(response.error || '登出失败');
+ } catch (err) {
+ error.value = err instanceof Error ? err.message : '登出失败';
+ console.error('登出失败:', err);
+ throw err;
+ } finally {
+ loading.value = false;
+ }
+ };
+
+ // 清除错误
+ const clearError = () => {
+ error.value = null;
+ };
+
+ return {
+ // 状态
+ loginStatus,
+ loading,
+ error,
+
+ // 计算属性
+ isLoggedIn,
+ username,
+ userId,
+
+ // 方法
+ fetchLoginStatus,
+ getLoginUrl,
+ handleLoginCallback,
+ relogin,
+ logout,
+ clearError
+ };
+});
\ No newline at end of file
diff --git a/ui/src/stores/counter.ts b/ui/src/stores/counter.ts
new file mode 100644
index 0000000..b6757ba
--- /dev/null
+++ b/ui/src/stores/counter.ts
@@ -0,0 +1,12 @@
+import { ref, computed } from 'vue'
+import { defineStore } from 'pinia'
+
+export const useCounterStore = defineStore('counter', () => {
+ const count = ref(0)
+ const doubleCount = computed(() => count.value * 2)
+ function increment() {
+ count.value++
+ }
+
+ return { count, doubleCount, increment }
+})
diff --git a/ui/src/types/index.ts b/ui/src/types/index.ts
new file mode 100644
index 0000000..d67f4a3
--- /dev/null
+++ b/ui/src/types/index.ts
@@ -0,0 +1,132 @@
+// 基础响应类型
+export interface ApiResponse {
+ success: boolean;
+ message?: string;
+ data?: T;
+ error?: string;
+ code?: string;
+ timestamp?: string;
+}
+
+// 分页响应类型
+export interface PaginatedResponse extends ApiResponse {
+ pagination?: {
+ page: number;
+ limit: number;
+ total: number;
+ pages: number;
+ };
+}
+
+// 用户信息
+export interface User {
+ id: number;
+ name: string;
+ account: string;
+ profile_image_urls: {
+ medium: string;
+ };
+ is_followed: boolean;
+}
+
+// 作品信息
+export interface Artwork {
+ id: number;
+ title: string;
+ description: string;
+ user: User;
+ image_urls: {
+ square_medium: string;
+ medium: string;
+ large: string;
+ original: string;
+ };
+ tags: Array<{
+ name: string;
+ translated_name?: string;
+ }>;
+ create_date: string;
+ update_date: string;
+ type: string;
+ width: number;
+ height: number;
+ page_count: number;
+ is_bookmarked: boolean;
+ total_bookmarks: number;
+ total_view: number;
+ is_muted: boolean;
+ meta_single_page?: {
+ original_image_url?: string;
+ large_image_url?: string;
+ };
+ meta_pages?: Array<{
+ image_urls: {
+ square_medium: string;
+ medium: string;
+ large: string;
+ original: string;
+ };
+ }>;
+}
+
+// 作者信息
+export interface Artist {
+ id: number;
+ name: string;
+ account: string;
+ profile_image_urls: {
+ medium: string;
+ };
+ comment: string;
+ is_followed: boolean;
+ total_illusts: number;
+ total_manga: number;
+ total_novels: number;
+ total_bookmarked_illust: number;
+ total_following: number;
+ total_followers: number;
+}
+
+// 登录状态
+export interface LoginStatus {
+ isLoggedIn: boolean;
+ username?: string;
+ user_id?: number;
+}
+
+// 下载任务
+export interface DownloadTask {
+ id: string;
+ type: 'artwork' | 'batch' | 'artist';
+ status: 'downloading' | 'completed' | 'failed' | 'partial' | 'cancelled';
+ progress: number;
+ total: number;
+ completed: number;
+ failed: number;
+ start_time: string;
+ end_time?: string;
+ files?: Array<{
+ path: string;
+ url: string;
+ size: string;
+ }>;
+ results?: any[];
+}
+
+// 搜索参数
+export interface SearchParams {
+ keyword: string;
+ type?: 'all' | 'art' | 'manga' | 'novel';
+ sort?: 'date_desc' | 'date_asc' | 'popular_desc';
+ duration?: 'all' | 'within_last_day' | 'within_last_week' | 'within_last_month';
+ offset?: number;
+ limit?: number;
+}
+
+// 下载参数
+export interface DownloadParams {
+ size?: 'original' | 'large' | 'medium' | 'square_medium';
+ quality?: 'high' | 'medium' | 'low';
+ format?: 'auto' | 'jpg' | 'png';
+ concurrent?: number;
+}
\ No newline at end of file
diff --git a/ui/src/views/AboutView.vue b/ui/src/views/AboutView.vue
new file mode 100644
index 0000000..756ad2a
--- /dev/null
+++ b/ui/src/views/AboutView.vue
@@ -0,0 +1,15 @@
+
+
+
This is an about page
+
+
+
+
diff --git a/ui/src/views/ArtistView.vue b/ui/src/views/ArtistView.vue
new file mode 100644
index 0000000..6aae2a6
--- /dev/null
+++ b/ui/src/views/ArtistView.vue
@@ -0,0 +1,490 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ artist.total_illusts }}
+
插画
+
+
+
{{ artist.total_manga }}
+
漫画
+
+
+
{{ artist.total_novels }}
+
小说
+
+
+
{{ artist.total_followers }}
+
粉丝
+
+
+
{{ artist.total_following }}
+
关注
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ui/src/views/ArtistsView.vue b/ui/src/views/ArtistsView.vue
new file mode 100644
index 0000000..c818f43
--- /dev/null
+++ b/ui/src/views/ArtistsView.vue
@@ -0,0 +1,588 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
关注的作者
+
+
+
+
+
+
+
+ {{ artist.total_illusts }}
+ 插画
+
+
+ {{ artist.total_manga }}
+ 漫画
+
+
+ {{ artist.total_followers }}
+ 粉丝
+
+
+
+
+
+ 查看作品
+
+
+
+
+
+
+
+
+
+
暂无关注的作者
+
关注喜欢的作者,在这里管理他们
+
+
+
+
+
+
+
搜索结果
+
+
+
+
+
+
+ {{ artist.total_illusts }}
+ 插画
+
+
+ {{ artist.total_manga }}
+ 漫画
+
+
+ {{ artist.total_followers }}
+ 粉丝
+
+
+
+
+
+ 查看作品
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ui/src/views/ArtworkView.vue b/ui/src/views/ArtworkView.vue
new file mode 100644
index 0000000..08b7c0d
--- /dev/null
+++ b/ui/src/views/ArtworkView.vue
@@ -0,0 +1,554 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
![]()
+
+
+
+
+ 图片加载失败
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
![]()
+
+
{{ artwork.user.name }}
+
@{{ artwork.user.account }}
+
+
+ 查看作者
+
+
+
+
+
+
+
+
{{ artwork.total_bookmarks }}
+
+
+
+
{{ artwork.total_view }}
+
+
+
+
{{ artwork.width }} × {{ artwork.height }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ui/src/views/DownloadsView.vue b/ui/src/views/DownloadsView.vue
new file mode 100644
index 0000000..c62d96f
--- /dev/null
+++ b/ui/src/views/DownloadsView.vue
@@ -0,0 +1,607 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
活跃任务
+
+
+
+
+
+
+
+ {{ task.completed }}/{{ task.total }} ({{ task.progress }}%)
+
+
+
+
+
+
+
+
+
+
+
+
+
下载历史
+
+
+
+
+
+
+
+ 完成时间:
+ {{ formatDate(task.end_time) }}
+
+
+ 文件数量:
+ {{ task.completed }}/{{ task.total }}
+
+
+ 失败数量:
+ {{ task.failed }}
+
+
+
+
+
下载的文件:
+
+
+ {{ getFileName(file.path) }}
+ {{ file.size }}
+
+
+ 还有 {{ task.files.length - 3 }} 个文件...
+
+
+
+
+
+
+
+
+
+
暂无下载记录
+
开始下载作品后,这里会显示下载历史
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ui/src/views/HomeView.vue b/ui/src/views/HomeView.vue
new file mode 100644
index 0000000..947450e
--- /dev/null
+++ b/ui/src/views/HomeView.vue
@@ -0,0 +1,365 @@
+
+
+
+
+
+
+
Pixiv 作品管理器
+
+ 发现、收藏、下载你喜欢的 Pixiv 作品
+
+
+
+
+ 立即登录
+
+
+ 开始搜索
+
+
+
+ 下载管理
+
+
+
+
+
+
+
+
主要功能
+
+
+
+
+
作品搜索
+
+ 通过关键词、标签、作者等多种方式搜索作品
+
+
+
+
+
+
一键下载
+
+ 支持单个作品、批量作品、作者作品下载
+
+
+
+
+
+
作者管理
+
+ 关注喜欢的作者,查看作品列表和统计信息
+
+
+
+
+
+
下载管理
+
+ 实时查看下载进度,管理下载历史和任务
+
+
+
+
+
+
+
+
+
快速操作
+
+
+
+
+ 搜索作品
+
+
+
+
+ 下载管理
+
+
+
+
+ 作者管理
+
+
+
+
+
+
+
+
diff --git a/ui/src/views/LoginView.vue b/ui/src/views/LoginView.vue
new file mode 100644
index 0000000..cfe105e
--- /dev/null
+++ b/ui/src/views/LoginView.vue
@@ -0,0 +1,337 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
已登录
+
欢迎回来,{{ username }}!
+
+
+
+
+
+ 返回首页
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ui/src/views/SearchView.vue b/ui/src/views/SearchView.vue
new file mode 100644
index 0000000..2f1aaa5
--- /dev/null
+++ b/ui/src/views/SearchView.vue
@@ -0,0 +1,408 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
未找到相关作品
+
尝试使用不同的关键词或调整搜索条件
+
+
+
+
+
+
开始搜索
+
输入关键词来搜索你喜欢的作品
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ui/tsconfig.app.json b/ui/tsconfig.app.json
new file mode 100644
index 0000000..913b8f2
--- /dev/null
+++ b/ui/tsconfig.app.json
@@ -0,0 +1,12 @@
+{
+ "extends": "@vue/tsconfig/tsconfig.dom.json",
+ "include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
+ "exclude": ["src/**/__tests__/*"],
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ }
+}
diff --git a/ui/tsconfig.json b/ui/tsconfig.json
new file mode 100644
index 0000000..66b5e57
--- /dev/null
+++ b/ui/tsconfig.json
@@ -0,0 +1,11 @@
+{
+ "files": [],
+ "references": [
+ {
+ "path": "./tsconfig.node.json"
+ },
+ {
+ "path": "./tsconfig.app.json"
+ }
+ ]
+}
diff --git a/ui/tsconfig.node.json b/ui/tsconfig.node.json
new file mode 100644
index 0000000..a83dfc9
--- /dev/null
+++ b/ui/tsconfig.node.json
@@ -0,0 +1,19 @@
+{
+ "extends": "@tsconfig/node22/tsconfig.json",
+ "include": [
+ "vite.config.*",
+ "vitest.config.*",
+ "cypress.config.*",
+ "nightwatch.conf.*",
+ "playwright.config.*",
+ "eslint.config.*"
+ ],
+ "compilerOptions": {
+ "noEmit": true,
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "types": ["node"]
+ }
+}
diff --git a/ui/vite.config.ts b/ui/vite.config.ts
new file mode 100644
index 0000000..4217010
--- /dev/null
+++ b/ui/vite.config.ts
@@ -0,0 +1,18 @@
+import { fileURLToPath, URL } from 'node:url'
+
+import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
+import vueDevTools from 'vite-plugin-vue-devtools'
+
+// https://vite.dev/config/
+export default defineConfig({
+ plugins: [
+ vue(),
+ vueDevTools(),
+ ],
+ resolve: {
+ alias: {
+ '@': fileURLToPath(new URL('./src', import.meta.url))
+ },
+ },
+})