diff --git a/.gitignore b/.gitignore index 114d8b1..88cf83a 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,8 @@ logs/ # Test coverage coverage.out coverage.html + + +# 开发文档 +document/ +database_schema.md \ No newline at end of file diff --git a/.env.example b/backend/.env.example similarity index 100% rename from .env.example rename to backend/.env.example diff --git a/cmd/main.go b/backend/cmd/main.go similarity index 100% rename from cmd/main.go rename to backend/cmd/main.go diff --git a/cmd/seed/main.go b/backend/cmd/seed/main.go similarity index 100% rename from cmd/seed/main.go rename to backend/cmd/seed/main.go diff --git a/go.mod b/backend/go.mod similarity index 100% rename from go.mod rename to backend/go.mod diff --git a/go.sum b/backend/go.sum similarity index 100% rename from go.sum rename to backend/go.sum diff --git a/internal/auth/jwt.go b/backend/internal/auth/jwt.go similarity index 100% rename from internal/auth/jwt.go rename to backend/internal/auth/jwt.go diff --git a/internal/auth/password.go b/backend/internal/auth/password.go similarity index 100% rename from internal/auth/password.go rename to backend/internal/auth/password.go diff --git a/internal/config/env.go b/backend/internal/config/env.go similarity index 100% rename from internal/config/env.go rename to backend/internal/config/env.go diff --git a/internal/db/db.go b/backend/internal/db/db.go similarity index 100% rename from internal/db/db.go rename to backend/internal/db/db.go diff --git a/internal/db/migrations.go b/backend/internal/db/migrations.go similarity index 83% rename from internal/db/migrations.go rename to backend/internal/db/migrations.go index ae2c7ec..287a8a0 100644 --- a/internal/db/migrations.go +++ b/backend/internal/db/migrations.go @@ -21,8 +21,6 @@ func Migrate(db *sql.DB) error { createCardKeysTable, createInvitationsTable, createAPIKeysTable, - createSystemSettingsTable, - insertDefaultSettings, } for i, migration := range migrations { @@ -177,28 +175,3 @@ CREATE TABLE IF NOT EXISTS api_keys ( CREATE INDEX IF NOT EXISTS idx_api_keys_key ON api_keys(key); CREATE INDEX IF NOT EXISTS idx_api_keys_is_active ON api_keys(is_active); ` - -const createSystemSettingsTable = ` -CREATE TABLE IF NOT EXISTS system_settings ( - id SERIAL PRIMARY KEY, - key VARCHAR(100) NOT NULL UNIQUE, - value TEXT NOT NULL, - value_type VARCHAR(20) NOT NULL DEFAULT 'string', - description VARCHAR(255), - updated_at TIMESTAMP -); - -CREATE INDEX IF NOT EXISTS idx_system_settings_key ON system_settings(key); -` - -const insertDefaultSettings = ` -INSERT INTO system_settings (key, value, value_type, description) VALUES - ('turnstile_enabled', 'false', 'bool', 'Cloudflare Turnstile 开关'), - ('turnstile_site_key', '', 'string', 'Turnstile Site Key'), - ('turnstile_secret_key', '', 'string', 'Turnstile Secret Key'), - ('token_check_interval', '6', 'int', 'Token 检测间隔(小时)'), - ('token_failure_threshold', '2', 'int', '连续失败禁用阈值'), - ('invitation_validity_days', '30', 'int', '邀请有效期(天)'), - ('site_title', 'ChatGPT Team 邀请', 'string', '站点标题') -ON CONFLICT (key) DO NOTHING; -` diff --git a/internal/handler/auth_handler.go b/backend/internal/handler/auth_handler.go similarity index 96% rename from internal/handler/auth_handler.go rename to backend/internal/handler/auth_handler.go index 17efe04..b661d18 100644 --- a/internal/handler/auth_handler.go +++ b/backend/internal/handler/auth_handler.go @@ -81,7 +81,7 @@ func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) { if admin == nil { respondJSON(w, http.StatusUnauthorized, LoginResponse{ Success: false, - Message: "Invalid username or password", + Message: "用户名或密码错误", }) return } @@ -90,7 +90,7 @@ func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) { if !admin.IsActive { respondJSON(w, http.StatusForbidden, LoginResponse{ Success: false, - Message: "Account is disabled", + Message: "账号已被禁用", }) return } @@ -99,7 +99,7 @@ func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) { if !auth.CheckPassword(req.Password, admin.PasswordHash) { respondJSON(w, http.StatusUnauthorized, LoginResponse{ Success: false, - Message: "Invalid username or password", + Message: "用户名或密码错误", }) return } diff --git a/internal/handler/card_key_handler.go b/backend/internal/handler/card_key_handler.go similarity index 55% rename from internal/handler/card_key_handler.go rename to backend/internal/handler/card_key_handler.go index aa2115c..8ebe227 100644 --- a/internal/handler/card_key_handler.go +++ b/backend/internal/handler/card_key_handler.go @@ -5,6 +5,7 @@ import ( "encoding/hex" "encoding/json" "net/http" + "strconv" "strings" "time" @@ -38,10 +39,13 @@ type BatchCreateCardKeyRequest struct { // CardKeyResponse 卡密响应 type CardKeyResponse struct { - Success bool `json:"success"` - Message string `json:"message"` - Data *models.CardKey `json:"data,omitempty"` - Keys []*models.CardKey `json:"keys,omitempty"` + Success bool `json:"success"` + Message string `json:"message"` + Data *models.CardKey `json:"data,omitempty"` + Keys []*models.CardKey `json:"keys,omitempty"` + Total int `json:"total,omitempty"` + Page int `json:"page,omitempty"` + PageSize int `json:"page_size,omitempty"` } // Handle 处理卡密接口 (GET: 列表, POST: 创建) @@ -187,8 +191,36 @@ func (h *CardKeyHandler) BatchCreate(w http.ResponseWriter, r *http.Request) { // list 获取卡密列表 func (h *CardKeyHandler) list(w http.ResponseWriter, r *http.Request) { + // 获取分页参数 + pageStr := r.URL.Query().Get("page") + pageSizeStr := r.URL.Query().Get("page_size") - cardKeys, err := h.repo.FindAll() + page, _ := strconv.Atoi(pageStr) + pageSize, _ := strconv.Atoi(pageSizeStr) + + // 默认值 + if page <= 0 { + page = 1 + } + if pageSize <= 0 { + pageSize = 10 + } + if pageSize > 100 { + pageSize = 100 + } + + // 获取总数 + total, err := h.repo.Count() + if err != nil { + respondJSON(w, http.StatusInternalServerError, CardKeyResponse{ + Success: false, + Message: "Failed to count card keys", + }) + return + } + + // 获取分页数据 + cardKeys, err := h.repo.FindAllPaginated(page, pageSize) if err != nil { respondJSON(w, http.StatusInternalServerError, CardKeyResponse{ Success: false, @@ -198,9 +230,12 @@ func (h *CardKeyHandler) list(w http.ResponseWriter, r *http.Request) { } respondJSON(w, http.StatusOK, CardKeyResponse{ - Success: true, - Message: "OK", - Keys: cardKeys, + Success: true, + Message: "OK", + Keys: cardKeys, + Total: total, + Page: page, + PageSize: pageSize, }) } @@ -211,3 +246,122 @@ func generateCardKey() string { hex := strings.ToUpper(hex.EncodeToString(bytes)) return hex[0:4] + "-" + hex[4:8] + "-" + hex[8:12] + "-" + hex[12:16] } + +// Delete 删除单个卡密 (DELETE /api/cardkeys/delete?id=xxx) +func (h *CardKeyHandler) Delete(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodDelete { + respondJSON(w, http.StatusMethodNotAllowed, CardKeyResponse{ + Success: false, + Message: "Method not allowed", + }) + return + } + + idStr := r.URL.Query().Get("id") + id, err := strconv.Atoi(idStr) + if err != nil { + respondJSON(w, http.StatusBadRequest, CardKeyResponse{ + Success: false, + Message: "Invalid card key ID", + }) + return + } + + if err := h.repo.Delete(id); err != nil { + respondJSON(w, http.StatusInternalServerError, CardKeyResponse{ + Success: false, + Message: "Failed to delete card key", + }) + return + } + + respondJSON(w, http.StatusOK, CardKeyResponse{ + Success: true, + Message: "Card key deleted successfully", + }) +} + +// BatchDeleteRequest 批量删除请求 +type BatchDeleteRequest struct { + IDs []int `json:"ids"` +} + +// BatchDelete 批量删除卡密 (DELETE /api/cardkeys/batch) +func (h *CardKeyHandler) BatchDelete(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodDelete { + respondJSON(w, http.StatusMethodNotAllowed, CardKeyResponse{ + Success: false, + Message: "Method not allowed", + }) + return + } + + var req BatchDeleteRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + respondJSON(w, http.StatusBadRequest, CardKeyResponse{ + Success: false, + Message: "Invalid request body", + }) + return + } + + if len(req.IDs) == 0 { + respondJSON(w, http.StatusBadRequest, CardKeyResponse{ + Success: false, + Message: "No IDs provided", + }) + return + } + + if err := h.repo.BatchDelete(req.IDs); err != nil { + respondJSON(w, http.StatusInternalServerError, CardKeyResponse{ + Success: false, + Message: "Failed to delete card keys", + }) + return + } + + respondJSON(w, http.StatusOK, CardKeyResponse{ + Success: true, + Message: "Card keys deleted successfully", + }) +} + +// ToggleActiveRequest 切换激活状态请求 +type ToggleActiveRequest struct { + ID int `json:"id"` + IsActive bool `json:"is_active"` +} + +// ToggleActive 切换卡密激活状态 (POST /api/cardkeys/toggle) +func (h *CardKeyHandler) ToggleActive(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + respondJSON(w, http.StatusMethodNotAllowed, CardKeyResponse{ + Success: false, + Message: "Method not allowed", + }) + return + } + + var req ToggleActiveRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + respondJSON(w, http.StatusBadRequest, CardKeyResponse{ + Success: false, + Message: "Invalid request body", + }) + return + } + + if err := h.repo.ToggleActive(req.ID, req.IsActive); err != nil { + respondJSON(w, http.StatusInternalServerError, CardKeyResponse{ + Success: false, + Message: "Failed to toggle card key status", + }) + return + } + + respondJSON(w, http.StatusOK, CardKeyResponse{ + Success: true, + Message: "Card key status updated successfully", + }) +} diff --git a/internal/handler/chatgpt_account_handler.go b/backend/internal/handler/chatgpt_account_handler.go similarity index 87% rename from internal/handler/chatgpt_account_handler.go rename to backend/internal/handler/chatgpt_account_handler.go index aad656b..a67ed56 100644 --- a/internal/handler/chatgpt_account_handler.go +++ b/backend/internal/handler/chatgpt_account_handler.go @@ -42,9 +42,12 @@ type AccountResponse struct { // ListResponse 列表响应 type ListResponse struct { - Success bool `json:"success"` - Message string `json:"message"` - Data []*models.ChatGPTAccount `json:"data,omitempty"` + Success bool `json:"success"` + Message string `json:"message"` + Data []*models.ChatGPTAccount `json:"data,omitempty"` + Total int `json:"total,omitempty"` + Page int `json:"page,omitempty"` + PageSize int `json:"page_size,omitempty"` } // Create 创建账号 @@ -153,7 +156,36 @@ func (h *ChatGPTAccountHandler) List(w http.ResponseWriter, r *http.Request) { return } - accounts, err := h.repo.FindAll() + // 获取分页参数 + pageStr := r.URL.Query().Get("page") + pageSizeStr := r.URL.Query().Get("page_size") + + page, _ := strconv.Atoi(pageStr) + pageSize, _ := strconv.Atoi(pageSizeStr) + + // 默认值 + if page <= 0 { + page = 1 + } + if pageSize <= 0 { + pageSize = 10 + } + if pageSize > 100 { + pageSize = 100 + } + + // 获取总数 + total, err := h.repo.Count() + if err != nil { + respondJSON(w, http.StatusInternalServerError, ListResponse{ + Success: false, + Message: "Failed to count accounts", + }) + return + } + + // 获取分页数据 + accounts, err := h.repo.FindAllPaginated(page, pageSize) if err != nil { respondJSON(w, http.StatusInternalServerError, ListResponse{ Success: false, @@ -168,9 +200,12 @@ func (h *ChatGPTAccountHandler) List(w http.ResponseWriter, r *http.Request) { } respondJSON(w, http.StatusOK, ListResponse{ - Success: true, - Message: "OK", - Data: accounts, + Success: true, + Message: "OK", + Data: accounts, + Total: total, + Page: page, + PageSize: pageSize, }) } diff --git a/internal/handler/invite_handler.go b/backend/internal/handler/invite_handler.go similarity index 88% rename from internal/handler/invite_handler.go rename to backend/internal/handler/invite_handler.go index 10dd6a2..e36343e 100644 --- a/internal/handler/invite_handler.go +++ b/backend/internal/handler/invite_handler.go @@ -3,6 +3,7 @@ package handler import ( "database/sql" "encoding/json" + "fmt" "net/http" "time" @@ -54,6 +55,57 @@ type InviteResponse struct { AccountName string `json:"account_name,omitempty"` } +// InviteListResponse 邀请列表响应 +type InviteListResponse struct { + Success bool `json:"success"` + Message string `json:"message"` + Data []*models.Invitation `json:"data,omitempty"` +} + +// ListByAccount 获取账号的邀请列表 (GET /api/invite?account_id=xxx) +func (h *InviteHandler) ListByAccount(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + respondJSON(w, http.StatusMethodNotAllowed, InviteListResponse{ + Success: false, + Message: "Method not allowed", + }) + return + } + + accountIDStr := r.URL.Query().Get("account_id") + if accountIDStr == "" { + respondJSON(w, http.StatusBadRequest, InviteListResponse{ + Success: false, + Message: "account_id is required", + }) + return + } + + var accountID int + if _, err := fmt.Sscanf(accountIDStr, "%d", &accountID); err != nil { + respondJSON(w, http.StatusBadRequest, InviteListResponse{ + Success: false, + Message: "Invalid account_id", + }) + return + } + + invitations, err := h.invitationRepo.FindByAccountID(accountID) + if err != nil { + respondJSON(w, http.StatusInternalServerError, InviteListResponse{ + Success: false, + Message: "Failed to fetch invitations", + }) + return + } + + respondJSON(w, http.StatusOK, InviteListResponse{ + Success: true, + Message: "OK", + Data: invitations, + }) +} + // InviteByAdmin 管理员邀请/移除接口 (POST/DELETE /api/invite) func (h *InviteHandler) InviteByAdmin(w http.ResponseWriter, r *http.Request) { switch r.Method { diff --git a/internal/middleware/auth_middleware.go b/backend/internal/middleware/auth_middleware.go similarity index 100% rename from internal/middleware/auth_middleware.go rename to backend/internal/middleware/auth_middleware.go diff --git a/internal/models/admin.go b/backend/internal/models/admin.go similarity index 100% rename from internal/models/admin.go rename to backend/internal/models/admin.go diff --git a/internal/models/api_key.go b/backend/internal/models/api_key.go similarity index 100% rename from internal/models/api_key.go rename to backend/internal/models/api_key.go diff --git a/internal/models/card_key.go b/backend/internal/models/card_key.go similarity index 100% rename from internal/models/card_key.go rename to backend/internal/models/card_key.go diff --git a/internal/models/chatgpt_account.go b/backend/internal/models/chatgpt_account.go similarity index 100% rename from internal/models/chatgpt_account.go rename to backend/internal/models/chatgpt_account.go diff --git a/internal/models/invitation.go b/backend/internal/models/invitation.go similarity index 100% rename from internal/models/invitation.go rename to backend/internal/models/invitation.go diff --git a/internal/repository/admin_repo.go b/backend/internal/repository/admin_repo.go similarity index 100% rename from internal/repository/admin_repo.go rename to backend/internal/repository/admin_repo.go diff --git a/internal/repository/card_key_repo.go b/backend/internal/repository/card_key_repo.go similarity index 59% rename from internal/repository/card_key_repo.go rename to backend/internal/repository/card_key_repo.go index 332207d..13b083c 100644 --- a/internal/repository/card_key_repo.go +++ b/backend/internal/repository/card_key_repo.go @@ -4,6 +4,8 @@ import ( "database/sql" "gpt-manager-go/internal/models" + + "github.com/lib/pq" ) // CardKeyRepository 卡密仓储 @@ -87,3 +89,61 @@ func (r *CardKeyRepository) FindAll() ([]*models.CardKey, error) { } return cardKeys, nil } + +// FindAllPaginated 分页获取卡密 +func (r *CardKeyRepository) FindAllPaginated(page, pageSize int) ([]*models.CardKey, error) { + offset := (page - 1) * pageSize + rows, err := r.db.Query(` + SELECT id, key, max_uses, used_count, validity_type, expires_at, is_active, created_by_id, created_at + FROM card_keys ORDER BY created_at DESC + LIMIT $1 OFFSET $2 + `, pageSize, offset) + if err != nil { + return nil, err + } + defer rows.Close() + + var cardKeys []*models.CardKey + for rows.Next() { + ck := &models.CardKey{} + if err := rows.Scan( + &ck.ID, &ck.Key, &ck.MaxUses, &ck.UsedCount, + &ck.ValidityType, &ck.ExpiresAt, &ck.IsActive, + &ck.CreatedByID, &ck.CreatedAt, + ); err != nil { + return nil, err + } + cardKeys = append(cardKeys, ck) + } + return cardKeys, nil +} + +// Count 获取卡密总数 +func (r *CardKeyRepository) Count() (int, error) { + var count int + err := r.db.QueryRow(`SELECT COUNT(*) FROM card_keys`).Scan(&count) + return count, err +} + +// Delete 删除单个卡密 +func (r *CardKeyRepository) Delete(id int) error { + _, err := r.db.Exec(`DELETE FROM card_keys WHERE id = $1`, id) + return err +} + +// BatchDelete 批量删除卡密 +func (r *CardKeyRepository) BatchDelete(ids []int) error { + if len(ids) == 0 { + return nil + } + // 构建 IN 子句 + query := `DELETE FROM card_keys WHERE id = ANY($1)` + _, err := r.db.Exec(query, pq.Array(ids)) + return err +} + +// ToggleActive 切换卡密激活状态 +func (r *CardKeyRepository) ToggleActive(id int, isActive bool) error { + _, err := r.db.Exec(`UPDATE card_keys SET is_active = $1 WHERE id = $2`, isActive, id) + return err +} diff --git a/internal/repository/chatgpt_account_repo.go b/backend/internal/repository/chatgpt_account_repo.go similarity index 84% rename from internal/repository/chatgpt_account_repo.go rename to backend/internal/repository/chatgpt_account_repo.go index 80a1e79..7aafb44 100644 --- a/internal/repository/chatgpt_account_repo.go +++ b/backend/internal/repository/chatgpt_account_repo.go @@ -205,3 +205,41 @@ func (r *ChatGPTAccountRepository) UpdateLastUsed(id int) error { `, time.Now(), id) return err } + +// FindAllPaginated 分页获取账号 +func (r *ChatGPTAccountRepository) FindAllPaginated(page, pageSize int) ([]*models.ChatGPTAccount, error) { + offset := (page - 1) * pageSize + rows, err := r.db.Query(` + SELECT id, name, auth_token, team_account_id, seats_in_use, seats_entitled, + active_start, active_until, is_active, consecutive_failures, + last_check, last_used, created_at, updated_at + FROM chatgpt_accounts ORDER BY created_at DESC + LIMIT $1 OFFSET $2 + `, pageSize, offset) + if err != nil { + return nil, err + } + defer rows.Close() + + var accounts []*models.ChatGPTAccount + for rows.Next() { + account := &models.ChatGPTAccount{} + if err := rows.Scan( + &account.ID, &account.Name, &account.AuthToken, &account.TeamAccountID, + &account.SeatsInUse, &account.SeatsEntitled, &account.ActiveStart, &account.ActiveUntil, + &account.IsActive, &account.ConsecutiveFailures, &account.LastCheck, + &account.LastUsed, &account.CreatedAt, &account.UpdatedAt, + ); err != nil { + return nil, err + } + accounts = append(accounts, account) + } + return accounts, nil +} + +// Count 获取账号总数 +func (r *ChatGPTAccountRepository) Count() (int, error) { + var count int + err := r.db.QueryRow(`SELECT COUNT(*) FROM chatgpt_accounts`).Scan(&count) + return count, err +} diff --git a/internal/repository/invitation_repo.go b/backend/internal/repository/invitation_repo.go similarity index 74% rename from internal/repository/invitation_repo.go rename to backend/internal/repository/invitation_repo.go index 6d863a0..7a101dd 100644 --- a/internal/repository/invitation_repo.go +++ b/backend/internal/repository/invitation_repo.go @@ -81,3 +81,29 @@ func (r *InvitationRepository) DeleteByEmailAndAccountID(email string, accountID `, email, accountID) return err } + +// FindByAccountID 根据账号ID查找邀请记录 +func (r *InvitationRepository) FindByAccountID(accountID int) ([]*models.Invitation, error) { + rows, err := r.db.Query(` + SELECT id, card_key_id, account_id, invited_email, status, error_message, expires_at, created_at, updated_at + FROM invitations WHERE account_id = $1 ORDER BY created_at DESC + `, accountID) + if err != nil { + return nil, err + } + defer rows.Close() + + var invitations []*models.Invitation + for rows.Next() { + inv := &models.Invitation{} + if err := rows.Scan( + &inv.ID, &inv.CardKeyID, &inv.AccountID, &inv.InvitedEmail, + &inv.Status, &inv.ErrorMessage, &inv.ExpiresAt, + &inv.CreatedAt, &inv.UpdatedAt, + ); err != nil { + return nil, err + } + invitations = append(invitations, inv) + } + return invitations, nil +} diff --git a/internal/router/router.go b/backend/internal/router/router.go similarity index 72% rename from internal/router/router.go rename to backend/internal/router/router.go index f7719dc..79765f9 100644 --- a/internal/router/router.go +++ b/backend/internal/router/router.go @@ -47,12 +47,36 @@ func SetupRoutes(db *sql.DB) http.Handler { protectedMux.HandleFunc("/api/accounts/refresh", accountHandler.Refresh) protectedMux.HandleFunc("/api/accounts/delete", accountHandler.Delete) - // 邀请接口 (管理员) - POST: 邀请, DELETE: 移除 - protectedMux.HandleFunc("/api/invite", inviteHandler.InviteByAdmin) + // 邀请接口 (管理员) - GET: 列表, POST: 邀请, DELETE: 移除 + protectedMux.HandleFunc("/api/invite", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + inviteHandler.ListByAccount(w, r) + case http.MethodPost, http.MethodDelete: + inviteHandler.InviteByAdmin(w, r) + default: + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusMethodNotAllowed) + w.Write([]byte(`{"success":false,"message":"Method not allowed"}`)) + } + }) // 卡密管理接口 - protectedMux.HandleFunc("/api/cardkeys", cardKeyHandler.Handle) // GET: 列表, POST: 创建 - protectedMux.HandleFunc("/api/cardkeys/batch", cardKeyHandler.BatchCreate) // POST: 批量创建 + protectedMux.HandleFunc("/api/cardkeys", cardKeyHandler.Handle) // GET: 列表, POST: 创建 + protectedMux.HandleFunc("/api/cardkeys/batch", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodPost: + cardKeyHandler.BatchCreate(w, r) + case http.MethodDelete: + cardKeyHandler.BatchDelete(w, r) + default: + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusMethodNotAllowed) + w.Write([]byte(`{"success":false,"message":"Method not allowed"}`)) + } + }) + protectedMux.HandleFunc("/api/cardkeys/delete", cardKeyHandler.Delete) // DELETE: 单个删除 + protectedMux.HandleFunc("/api/cardkeys/toggle", cardKeyHandler.ToggleActive) // POST: 切换激活状态 // 挂载受保护的路由 mux.Handle("/api/profile", middleware.AuthMiddleware(protectedMux)) diff --git a/internal/service/chatgpt_service.go b/backend/internal/service/chatgpt_service.go similarity index 100% rename from internal/service/chatgpt_service.go rename to backend/internal/service/chatgpt_service.go diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 0000000..f7566fd --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1 @@ +VITE_BASE_URL=http://localhost:8080 diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..33895ab --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,5 @@ +# Vue 3 + TypeScript + Vite + +This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 ` + + diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..3153fe7 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,38 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vue-tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@tailwindcss/vite": "^4.1.18", + "@tanstack/vue-table": "^8.21.3", + "@vueuse/core": "^14.1.0", + "axios": "^1.13.2", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "file-saver": "^2.0.5", + "lucide-vue-next": "^0.562.0", + "pinia": "^3.0.4", + "reka-ui": "^2.7.0", + "tailwind-merge": "^3.4.0", + "tailwindcss": "^4.1.18", + "vue": "^3.5.24", + "vue-router": "4", + "vue-sonner": "^2.0.9" + }, + "devDependencies": { + "@types/file-saver": "^2.0.7", + "@types/node": "^24.10.1", + "@vitejs/plugin-vue": "^6.0.1", + "@vue/tsconfig": "^0.8.1", + "tw-animate-css": "^1.4.0", + "typescript": "~5.9.3", + "vite": "^7.2.4", + "vue-tsc": "^3.1.4" + } +} diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml new file mode 100644 index 0000000..08345f1 --- /dev/null +++ b/frontend/pnpm-lock.yaml @@ -0,0 +1,1891 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@tailwindcss/vite': + specifier: ^4.1.18 + version: 4.1.18(vite@7.3.1(@types/node@24.10.7)(jiti@2.6.1)(lightningcss@1.30.2)) + '@tanstack/vue-table': + specifier: ^8.21.3 + version: 8.21.3(vue@3.5.26(typescript@5.9.3)) + '@vueuse/core': + specifier: ^14.1.0 + version: 14.1.0(vue@3.5.26(typescript@5.9.3)) + axios: + specifier: ^1.13.2 + version: 1.13.2 + class-variance-authority: + specifier: ^0.7.1 + version: 0.7.1 + clsx: + specifier: ^2.1.1 + version: 2.1.1 + file-saver: + specifier: ^2.0.5 + version: 2.0.5 + lucide-vue-next: + specifier: ^0.562.0 + version: 0.562.0(vue@3.5.26(typescript@5.9.3)) + pinia: + specifier: ^3.0.4 + version: 3.0.4(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3)) + reka-ui: + specifier: ^2.7.0 + version: 2.7.0(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3)) + tailwind-merge: + specifier: ^3.4.0 + version: 3.4.0 + tailwindcss: + specifier: ^4.1.18 + version: 4.1.18 + vue: + specifier: ^3.5.24 + version: 3.5.26(typescript@5.9.3) + vue-router: + specifier: '4' + version: 4.6.4(vue@3.5.26(typescript@5.9.3)) + vue-sonner: + specifier: ^2.0.9 + version: 2.0.9 + devDependencies: + '@types/file-saver': + specifier: ^2.0.7 + version: 2.0.7 + '@types/node': + specifier: ^24.10.1 + version: 24.10.7 + '@vitejs/plugin-vue': + specifier: ^6.0.1 + version: 6.0.3(vite@7.3.1(@types/node@24.10.7)(jiti@2.6.1)(lightningcss@1.30.2))(vue@3.5.26(typescript@5.9.3)) + '@vue/tsconfig': + specifier: ^0.8.1 + version: 0.8.1(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3)) + tw-animate-css: + specifier: ^1.4.0 + version: 1.4.0 + typescript: + specifier: ~5.9.3 + version: 5.9.3 + vite: + specifier: ^7.2.4 + version: 7.3.1(@types/node@24.10.7)(jiti@2.6.1)(lightningcss@1.30.2) + vue-tsc: + specifier: ^3.1.4 + version: 3.2.2(typescript@5.9.3) + +packages: + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.28.6': + resolution: {integrity: sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.28.6': + resolution: {integrity: sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==} + engines: {node: '>=6.9.0'} + + '@esbuild/aix-ppc64@0.27.2': + resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.2': + resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.2': + resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.2': + resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.2': + resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.2': + resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.2': + resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.2': + resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.2': + resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.2': + resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.2': + resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.2': + resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.2': + resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.2': + resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.2': + resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.2': + resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.2': + resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.2': + resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.2': + resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.2': + resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.2': + resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.2': + resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.2': + resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.2': + resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.2': + resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.2': + resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@floating-ui/core@1.7.3': + resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==} + + '@floating-ui/dom@1.7.4': + resolution: {integrity: sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==} + + '@floating-ui/utils@0.2.10': + resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + + '@floating-ui/vue@1.1.9': + resolution: {integrity: sha512-BfNqNW6KA83Nexspgb9DZuz578R7HT8MZw1CfK9I6Ah4QReNWEJsXWHN+SdmOVLNGmTPDi+fDT535Df5PzMLbQ==} + + '@internationalized/date@3.10.1': + resolution: {integrity: sha512-oJrXtQiAXLvT9clCf1K4kxp3eKsQhIaZqxEyowkBcsvZDdZkbWrVmnGknxs5flTD0VGsxrxKgBCZty1EzoiMzA==} + + '@internationalized/number@3.6.5': + resolution: {integrity: sha512-6hY4Kl4HPBvtfS62asS/R22JzNNy8vi/Ssev7x6EobfCp+9QIB2hKvI2EtbdJ0VSQacxVNtqhE/NmF/NZ0gm6g==} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@rolldown/pluginutils@1.0.0-beta.53': + resolution: {integrity: sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==} + + '@rollup/rollup-android-arm-eabi@4.55.1': + resolution: {integrity: sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.55.1': + resolution: {integrity: sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.55.1': + resolution: {integrity: sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.55.1': + resolution: {integrity: sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.55.1': + resolution: {integrity: sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.55.1': + resolution: {integrity: sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.55.1': + resolution: {integrity: sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.55.1': + resolution: {integrity: sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.55.1': + resolution: {integrity: sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.55.1': + resolution: {integrity: sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.55.1': + resolution: {integrity: sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.55.1': + resolution: {integrity: sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.55.1': + resolution: {integrity: sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.55.1': + resolution: {integrity: sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.55.1': + resolution: {integrity: sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.55.1': + resolution: {integrity: sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.55.1': + resolution: {integrity: sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.55.1': + resolution: {integrity: sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.55.1': + resolution: {integrity: sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.55.1': + resolution: {integrity: sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.55.1': + resolution: {integrity: sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.55.1': + resolution: {integrity: sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.55.1': + resolution: {integrity: sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.55.1': + resolution: {integrity: sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.55.1': + resolution: {integrity: sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==} + cpu: [x64] + os: [win32] + + '@swc/helpers@0.5.18': + resolution: {integrity: sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==} + + '@tailwindcss/node@4.1.18': + resolution: {integrity: sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==} + + '@tailwindcss/oxide-android-arm64@4.1.18': + resolution: {integrity: sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.1.18': + resolution: {integrity: sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.1.18': + resolution: {integrity: sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.1.18': + resolution: {integrity: sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18': + resolution: {integrity: sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.18': + resolution: {integrity: sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-musl@4.1.18': + resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-gnu@4.1.18': + resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-musl@4.1.18': + resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-wasm32-wasi@4.1.18': + resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.18': + resolution: {integrity: sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.1.18': + resolution: {integrity: sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.1.18': + resolution: {integrity: sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==} + engines: {node: '>= 10'} + + '@tailwindcss/vite@4.1.18': + resolution: {integrity: sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 + + '@tanstack/table-core@8.21.3': + resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==} + engines: {node: '>=12'} + + '@tanstack/virtual-core@3.13.18': + resolution: {integrity: sha512-Mx86Hqu1k39icq2Zusq+Ey2J6dDWTjDvEv43PJtRCoEYTLyfaPnxIQ6iy7YAOK0NV/qOEmZQ/uCufrppZxTgcg==} + + '@tanstack/vue-table@8.21.3': + resolution: {integrity: sha512-rusRyd77c5tDPloPskctMyPLFEQUeBzxdQ+2Eow4F7gDPlPOB1UnnhzfpdvqZ8ZyX2rRNGmqNnQWm87OI2OQPw==} + engines: {node: '>=12'} + peerDependencies: + vue: '>=3.2' + + '@tanstack/vue-virtual@3.13.18': + resolution: {integrity: sha512-6pT8HdHtTU5Z+t906cGdCroUNA5wHjFXsNss9gwk7QAr1VNZtz9IQCs2Nhx0gABK48c+OocHl2As+TMg8+Hy4A==} + peerDependencies: + vue: ^2.7.0 || ^3.0.0 + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/file-saver@2.0.7': + resolution: {integrity: sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==} + + '@types/node@24.10.7': + resolution: {integrity: sha512-+054pVMzVTmRQV8BhpGv3UyfZ2Llgl8rdpDTon+cUH9+na0ncBVXj3wTUKh14+Kiz18ziM3b4ikpP5/Pc0rQEQ==} + + '@types/web-bluetooth@0.0.21': + resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} + + '@vitejs/plugin-vue@6.0.3': + resolution: {integrity: sha512-TlGPkLFLVOY3T7fZrwdvKpjprR3s4fxRln0ORDo1VQ7HHyxJwTlrjKU3kpVWTlaAjIEuCTokmjkZnr8Tpc925w==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 + vue: ^3.2.25 + + '@volar/language-core@2.4.27': + resolution: {integrity: sha512-DjmjBWZ4tJKxfNC1F6HyYERNHPYS7L7OPFyCrestykNdUZMFYzI9WTyvwPcaNaHlrEUwESHYsfEw3isInncZxQ==} + + '@volar/source-map@2.4.27': + resolution: {integrity: sha512-ynlcBReMgOZj2i6po+qVswtDUeeBRCTgDurjMGShbm8WYZgJ0PA4RmtebBJ0BCYol1qPv3GQF6jK7C9qoVc7lg==} + + '@volar/typescript@2.4.27': + resolution: {integrity: sha512-eWaYCcl/uAPInSK2Lze6IqVWaBu/itVqR5InXcHXFyles4zO++Mglt3oxdgj75BDcv1Knr9Y93nowS8U3wqhxg==} + + '@vue/compiler-core@3.5.26': + resolution: {integrity: sha512-vXyI5GMfuoBCnv5ucIT7jhHKl55Y477yxP6fc4eUswjP8FG3FFVFd41eNDArR+Uk3QKn2Z85NavjaxLxOC19/w==} + + '@vue/compiler-dom@3.5.26': + resolution: {integrity: sha512-y1Tcd3eXs834QjswshSilCBnKGeQjQXB6PqFn/1nxcQw4pmG42G8lwz+FZPAZAby6gZeHSt/8LMPfZ4Rb+Bd/A==} + + '@vue/compiler-sfc@3.5.26': + resolution: {integrity: sha512-egp69qDTSEZcf4bGOSsprUr4xI73wfrY5oRs6GSgXFTiHrWj4Y3X5Ydtip9QMqiCMCPVwLglB9GBxXtTadJ3mA==} + + '@vue/compiler-ssr@3.5.26': + resolution: {integrity: sha512-lZT9/Y0nSIRUPVvapFJEVDbEXruZh2IYHMk2zTtEgJSlP5gVOqeWXH54xDKAaFS4rTnDeDBQUYDtxKyoW9FwDw==} + + '@vue/devtools-api@6.6.4': + resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==} + + '@vue/devtools-api@7.7.9': + resolution: {integrity: sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==} + + '@vue/devtools-kit@7.7.9': + resolution: {integrity: sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==} + + '@vue/devtools-shared@7.7.9': + resolution: {integrity: sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==} + + '@vue/language-core@3.2.2': + resolution: {integrity: sha512-5DAuhxsxBN9kbriklh3Q5AMaJhyOCNiQJvCskN9/30XOpdLiqZU9Q+WvjArP17ubdGEyZtBzlIeG5nIjEbNOrQ==} + + '@vue/reactivity@3.5.26': + resolution: {integrity: sha512-9EnYB1/DIiUYYnzlnUBgwU32NNvLp/nhxLXeWRhHUEeWNTn1ECxX8aGO7RTXeX6PPcxe3LLuNBFoJbV4QZ+CFQ==} + + '@vue/runtime-core@3.5.26': + resolution: {integrity: sha512-xJWM9KH1kd201w5DvMDOwDHYhrdPTrAatn56oB/LRG4plEQeZRQLw0Bpwih9KYoqmzaxF0OKSn6swzYi84e1/Q==} + + '@vue/runtime-dom@3.5.26': + resolution: {integrity: sha512-XLLd/+4sPC2ZkN/6+V4O4gjJu6kSDbHAChvsyWgm1oGbdSO3efvGYnm25yCjtFm/K7rrSDvSfPDgN1pHgS4VNQ==} + + '@vue/server-renderer@3.5.26': + resolution: {integrity: sha512-TYKLXmrwWKSodyVuO1WAubucd+1XlLg4set0YoV+Hu8Lo79mp/YMwWV5mC5FgtsDxX3qo1ONrxFaTP1OQgy1uA==} + peerDependencies: + vue: 3.5.26 + + '@vue/shared@3.5.26': + resolution: {integrity: sha512-7Z6/y3uFI5PRoKeorTOSXKcDj0MSasfNNltcslbFrPpcw6aXRUALq4IfJlaTRspiWIUOEZbrpM+iQGmCOiWe4A==} + + '@vue/tsconfig@0.8.1': + resolution: {integrity: sha512-aK7feIWPXFSUhsCP9PFqPyFOcz4ENkb8hZ2pneL6m2UjCkccvaOhC/5KCKluuBufvp2KzkbdA2W2pk20vLzu3g==} + peerDependencies: + typescript: 5.x + vue: ^3.4.0 + peerDependenciesMeta: + typescript: + optional: true + vue: + optional: true + + '@vueuse/core@12.8.2': + resolution: {integrity: sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==} + + '@vueuse/core@14.1.0': + resolution: {integrity: sha512-rgBinKs07hAYyPF834mDTigH7BtPqvZ3Pryuzt1SD/lg5wEcWqvwzXXYGEDb2/cP0Sj5zSvHl3WkmMELr5kfWw==} + peerDependencies: + vue: ^3.5.0 + + '@vueuse/metadata@12.8.2': + resolution: {integrity: sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==} + + '@vueuse/metadata@14.1.0': + resolution: {integrity: sha512-7hK4g015rWn2PhKcZ99NyT+ZD9sbwm7SGvp7k+k+rKGWnLjS/oQozoIZzWfCewSUeBmnJkIb+CNr7Zc/EyRnnA==} + + '@vueuse/shared@12.8.2': + resolution: {integrity: sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==} + + '@vueuse/shared@14.1.0': + resolution: {integrity: sha512-EcKxtYvn6gx1F8z9J5/rsg3+lTQnvOruQd8fUecW99DCK04BkWD7z5KQ/wTAx+DazyoEE9dJt/zV8OIEQbM6kw==} + peerDependencies: + vue: ^3.5.0 + + alien-signals@3.1.2: + resolution: {integrity: sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw==} + + aria-hidden@1.2.6: + resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} + engines: {node: '>=10'} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + axios@1.13.2: + resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==} + + birpc@2.9.0: + resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + class-variance-authority@0.7.1: + resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + copy-anything@4.0.5: + resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==} + engines: {node: '>=18'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + defu@6.1.4: + resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + enhanced-resolve@5.18.4: + resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==} + engines: {node: '>=10.13.0'} + + entities@7.0.0: + resolution: {integrity: sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ==} + engines: {node: '>=0.12'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + esbuild@0.27.2: + resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==} + engines: {node: '>=18'} + hasBin: true + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-saver@2.0.5: + resolution: {integrity: sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==} + + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + 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'} + + hookable@5.5.3: + resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + + is-what@5.5.0: + resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==} + engines: {node: '>=18'} + + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + + lightningcss-android-arm64@1.30.2: + resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.30.2: + resolution: {integrity: sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.30.2: + resolution: {integrity: sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.30.2: + resolution: {integrity: sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.30.2: + resolution: {integrity: sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.30.2: + resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.30.2: + resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.30.2: + resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.30.2: + resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.30.2: + resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.30.2: + resolution: {integrity: sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.30.2: + resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} + engines: {node: '>= 12.0.0'} + + lucide-vue-next@0.562.0: + resolution: {integrity: sha512-LN0BLGKMFulv0lnfK29r14DcngRUhIqdcaL0zXTt2o0oS9odlrjCGaU3/X9hIihOjjN8l8e+Y9G/famcNYaI7Q==} + peerDependencies: + vue: '>=3.0.1' + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + 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'} + + mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + + 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 + + ohash@2.0.11: + resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + + perfect-debounce@1.0.0: + resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pinia@3.0.4: + resolution: {integrity: sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==} + peerDependencies: + typescript: '>=4.5.0' + vue: ^3.5.11 + peerDependenciesMeta: + typescript: + optional: true + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + reka-ui@2.7.0: + resolution: {integrity: sha512-m+XmxQN2xtFzBP3OAdIafKq7C8OETo2fqfxcIIxYmNN2Ch3r5oAf6yEYCIJg5tL/yJU2mHqF70dCCekUkrAnXA==} + peerDependencies: + vue: '>= 3.2.0' + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + rollup@4.55.1: + resolution: {integrity: sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + 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'} + + superjson@2.2.6: + resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==} + engines: {node: '>=16'} + + tailwind-merge@3.4.0: + resolution: {integrity: sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==} + + tailwindcss@4.1.18: + resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==} + + tapable@2.3.0: + resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} + engines: {node: '>=6'} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tw-animate-css@1.4.0: + resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + + vite@7.3.1: + resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} + 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-demi@0.14.10: + resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==} + engines: {node: '>=12'} + hasBin: true + peerDependencies: + '@vue/composition-api': ^1.0.0-rc.1 + vue: ^3.0.0-0 || ^2.6.0 + peerDependenciesMeta: + '@vue/composition-api': + optional: true + + vue-router@4.6.4: + resolution: {integrity: sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==} + peerDependencies: + vue: ^3.5.0 + + vue-sonner@2.0.9: + resolution: {integrity: sha512-i6BokNlNDL93fpzNxN/LZSn6D6MzlO+i3qXt6iVZne3x1k7R46d5HlFB4P8tYydhgqOrRbIZEsnRd3kG7qGXyw==} + peerDependencies: + '@nuxt/kit': ^4.0.3 + '@nuxt/schema': ^4.0.3 + nuxt: ^4.0.3 + peerDependenciesMeta: + '@nuxt/kit': + optional: true + '@nuxt/schema': + optional: true + nuxt: + optional: true + + vue-tsc@3.2.2: + resolution: {integrity: sha512-r9YSia/VgGwmbbfC06hDdAatH634XJ9nVl6Zrnz1iK4ucp8Wu78kawplXnIDa3MSu1XdQQePTHLXYwPDWn+nyQ==} + hasBin: true + peerDependencies: + typescript: '>=5.0.0' + + vue@3.5.26: + resolution: {integrity: sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + +snapshots: + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/parser@7.28.6': + dependencies: + '@babel/types': 7.28.6 + + '@babel/types@7.28.6': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@esbuild/aix-ppc64@0.27.2': + optional: true + + '@esbuild/android-arm64@0.27.2': + optional: true + + '@esbuild/android-arm@0.27.2': + optional: true + + '@esbuild/android-x64@0.27.2': + optional: true + + '@esbuild/darwin-arm64@0.27.2': + optional: true + + '@esbuild/darwin-x64@0.27.2': + optional: true + + '@esbuild/freebsd-arm64@0.27.2': + optional: true + + '@esbuild/freebsd-x64@0.27.2': + optional: true + + '@esbuild/linux-arm64@0.27.2': + optional: true + + '@esbuild/linux-arm@0.27.2': + optional: true + + '@esbuild/linux-ia32@0.27.2': + optional: true + + '@esbuild/linux-loong64@0.27.2': + optional: true + + '@esbuild/linux-mips64el@0.27.2': + optional: true + + '@esbuild/linux-ppc64@0.27.2': + optional: true + + '@esbuild/linux-riscv64@0.27.2': + optional: true + + '@esbuild/linux-s390x@0.27.2': + optional: true + + '@esbuild/linux-x64@0.27.2': + optional: true + + '@esbuild/netbsd-arm64@0.27.2': + optional: true + + '@esbuild/netbsd-x64@0.27.2': + optional: true + + '@esbuild/openbsd-arm64@0.27.2': + optional: true + + '@esbuild/openbsd-x64@0.27.2': + optional: true + + '@esbuild/openharmony-arm64@0.27.2': + optional: true + + '@esbuild/sunos-x64@0.27.2': + optional: true + + '@esbuild/win32-arm64@0.27.2': + optional: true + + '@esbuild/win32-ia32@0.27.2': + optional: true + + '@esbuild/win32-x64@0.27.2': + optional: true + + '@floating-ui/core@1.7.3': + dependencies: + '@floating-ui/utils': 0.2.10 + + '@floating-ui/dom@1.7.4': + dependencies: + '@floating-ui/core': 1.7.3 + '@floating-ui/utils': 0.2.10 + + '@floating-ui/utils@0.2.10': {} + + '@floating-ui/vue@1.1.9(vue@3.5.26(typescript@5.9.3))': + dependencies: + '@floating-ui/dom': 1.7.4 + '@floating-ui/utils': 0.2.10 + vue-demi: 0.14.10(vue@3.5.26(typescript@5.9.3)) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + + '@internationalized/date@3.10.1': + dependencies: + '@swc/helpers': 0.5.18 + + '@internationalized/number@3.6.5': + dependencies: + '@swc/helpers': 0.5.18 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@rolldown/pluginutils@1.0.0-beta.53': {} + + '@rollup/rollup-android-arm-eabi@4.55.1': + optional: true + + '@rollup/rollup-android-arm64@4.55.1': + optional: true + + '@rollup/rollup-darwin-arm64@4.55.1': + optional: true + + '@rollup/rollup-darwin-x64@4.55.1': + optional: true + + '@rollup/rollup-freebsd-arm64@4.55.1': + optional: true + + '@rollup/rollup-freebsd-x64@4.55.1': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.55.1': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.55.1': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.55.1': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.55.1': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.55.1': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.55.1': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.55.1': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.55.1': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.55.1': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.55.1': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.55.1': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.55.1': + optional: true + + '@rollup/rollup-linux-x64-musl@4.55.1': + optional: true + + '@rollup/rollup-openbsd-x64@4.55.1': + optional: true + + '@rollup/rollup-openharmony-arm64@4.55.1': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.55.1': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.55.1': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.55.1': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.55.1': + optional: true + + '@swc/helpers@0.5.18': + dependencies: + tslib: 2.8.1 + + '@tailwindcss/node@4.1.18': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.18.4 + jiti: 2.6.1 + lightningcss: 1.30.2 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.1.18 + + '@tailwindcss/oxide-android-arm64@4.1.18': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.1.18': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.1.18': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.1.18': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.1.18': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.18': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.1.18': + optional: true + + '@tailwindcss/oxide@4.1.18': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.1.18 + '@tailwindcss/oxide-darwin-arm64': 4.1.18 + '@tailwindcss/oxide-darwin-x64': 4.1.18 + '@tailwindcss/oxide-freebsd-x64': 4.1.18 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.18 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.18 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.18 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.18 + '@tailwindcss/oxide-linux-x64-musl': 4.1.18 + '@tailwindcss/oxide-wasm32-wasi': 4.1.18 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.18 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.18 + + '@tailwindcss/vite@4.1.18(vite@7.3.1(@types/node@24.10.7)(jiti@2.6.1)(lightningcss@1.30.2))': + dependencies: + '@tailwindcss/node': 4.1.18 + '@tailwindcss/oxide': 4.1.18 + tailwindcss: 4.1.18 + vite: 7.3.1(@types/node@24.10.7)(jiti@2.6.1)(lightningcss@1.30.2) + + '@tanstack/table-core@8.21.3': {} + + '@tanstack/virtual-core@3.13.18': {} + + '@tanstack/vue-table@8.21.3(vue@3.5.26(typescript@5.9.3))': + dependencies: + '@tanstack/table-core': 8.21.3 + vue: 3.5.26(typescript@5.9.3) + + '@tanstack/vue-virtual@3.13.18(vue@3.5.26(typescript@5.9.3))': + dependencies: + '@tanstack/virtual-core': 3.13.18 + vue: 3.5.26(typescript@5.9.3) + + '@types/estree@1.0.8': {} + + '@types/file-saver@2.0.7': {} + + '@types/node@24.10.7': + dependencies: + undici-types: 7.16.0 + + '@types/web-bluetooth@0.0.21': {} + + '@vitejs/plugin-vue@6.0.3(vite@7.3.1(@types/node@24.10.7)(jiti@2.6.1)(lightningcss@1.30.2))(vue@3.5.26(typescript@5.9.3))': + dependencies: + '@rolldown/pluginutils': 1.0.0-beta.53 + vite: 7.3.1(@types/node@24.10.7)(jiti@2.6.1)(lightningcss@1.30.2) + vue: 3.5.26(typescript@5.9.3) + + '@volar/language-core@2.4.27': + dependencies: + '@volar/source-map': 2.4.27 + + '@volar/source-map@2.4.27': {} + + '@volar/typescript@2.4.27': + dependencies: + '@volar/language-core': 2.4.27 + path-browserify: 1.0.1 + vscode-uri: 3.1.0 + + '@vue/compiler-core@3.5.26': + dependencies: + '@babel/parser': 7.28.6 + '@vue/shared': 3.5.26 + entities: 7.0.0 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.26': + dependencies: + '@vue/compiler-core': 3.5.26 + '@vue/shared': 3.5.26 + + '@vue/compiler-sfc@3.5.26': + dependencies: + '@babel/parser': 7.28.6 + '@vue/compiler-core': 3.5.26 + '@vue/compiler-dom': 3.5.26 + '@vue/compiler-ssr': 3.5.26 + '@vue/shared': 3.5.26 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.6 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.26': + dependencies: + '@vue/compiler-dom': 3.5.26 + '@vue/shared': 3.5.26 + + '@vue/devtools-api@6.6.4': {} + + '@vue/devtools-api@7.7.9': + dependencies: + '@vue/devtools-kit': 7.7.9 + + '@vue/devtools-kit@7.7.9': + dependencies: + '@vue/devtools-shared': 7.7.9 + birpc: 2.9.0 + hookable: 5.5.3 + mitt: 3.0.1 + perfect-debounce: 1.0.0 + speakingurl: 14.0.1 + superjson: 2.2.6 + + '@vue/devtools-shared@7.7.9': + dependencies: + rfdc: 1.4.1 + + '@vue/language-core@3.2.2': + dependencies: + '@volar/language-core': 2.4.27 + '@vue/compiler-dom': 3.5.26 + '@vue/shared': 3.5.26 + alien-signals: 3.1.2 + muggle-string: 0.4.1 + path-browserify: 1.0.1 + picomatch: 4.0.3 + + '@vue/reactivity@3.5.26': + dependencies: + '@vue/shared': 3.5.26 + + '@vue/runtime-core@3.5.26': + dependencies: + '@vue/reactivity': 3.5.26 + '@vue/shared': 3.5.26 + + '@vue/runtime-dom@3.5.26': + dependencies: + '@vue/reactivity': 3.5.26 + '@vue/runtime-core': 3.5.26 + '@vue/shared': 3.5.26 + csstype: 3.2.3 + + '@vue/server-renderer@3.5.26(vue@3.5.26(typescript@5.9.3))': + dependencies: + '@vue/compiler-ssr': 3.5.26 + '@vue/shared': 3.5.26 + vue: 3.5.26(typescript@5.9.3) + + '@vue/shared@3.5.26': {} + + '@vue/tsconfig@0.8.1(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3))': + optionalDependencies: + typescript: 5.9.3 + vue: 3.5.26(typescript@5.9.3) + + '@vueuse/core@12.8.2(typescript@5.9.3)': + dependencies: + '@types/web-bluetooth': 0.0.21 + '@vueuse/metadata': 12.8.2 + '@vueuse/shared': 12.8.2(typescript@5.9.3) + vue: 3.5.26(typescript@5.9.3) + transitivePeerDependencies: + - typescript + + '@vueuse/core@14.1.0(vue@3.5.26(typescript@5.9.3))': + dependencies: + '@types/web-bluetooth': 0.0.21 + '@vueuse/metadata': 14.1.0 + '@vueuse/shared': 14.1.0(vue@3.5.26(typescript@5.9.3)) + vue: 3.5.26(typescript@5.9.3) + + '@vueuse/metadata@12.8.2': {} + + '@vueuse/metadata@14.1.0': {} + + '@vueuse/shared@12.8.2(typescript@5.9.3)': + dependencies: + vue: 3.5.26(typescript@5.9.3) + transitivePeerDependencies: + - typescript + + '@vueuse/shared@14.1.0(vue@3.5.26(typescript@5.9.3))': + dependencies: + vue: 3.5.26(typescript@5.9.3) + + alien-signals@3.1.2: {} + + aria-hidden@1.2.6: + dependencies: + tslib: 2.8.1 + + asynckit@0.4.0: {} + + axios@1.13.2: + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.5 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + + birpc@2.9.0: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + class-variance-authority@0.7.1: + dependencies: + clsx: 2.1.1 + + clsx@2.1.1: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + copy-anything@4.0.5: + dependencies: + is-what: 5.5.0 + + csstype@3.2.3: {} + + defu@6.1.4: {} + + delayed-stream@1.0.0: {} + + detect-libc@2.1.2: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + enhanced-resolve@5.18.4: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.0 + + entities@7.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 + + esbuild@0.27.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.2 + '@esbuild/android-arm': 0.27.2 + '@esbuild/android-arm64': 0.27.2 + '@esbuild/android-x64': 0.27.2 + '@esbuild/darwin-arm64': 0.27.2 + '@esbuild/darwin-x64': 0.27.2 + '@esbuild/freebsd-arm64': 0.27.2 + '@esbuild/freebsd-x64': 0.27.2 + '@esbuild/linux-arm': 0.27.2 + '@esbuild/linux-arm64': 0.27.2 + '@esbuild/linux-ia32': 0.27.2 + '@esbuild/linux-loong64': 0.27.2 + '@esbuild/linux-mips64el': 0.27.2 + '@esbuild/linux-ppc64': 0.27.2 + '@esbuild/linux-riscv64': 0.27.2 + '@esbuild/linux-s390x': 0.27.2 + '@esbuild/linux-x64': 0.27.2 + '@esbuild/netbsd-arm64': 0.27.2 + '@esbuild/netbsd-x64': 0.27.2 + '@esbuild/openbsd-arm64': 0.27.2 + '@esbuild/openbsd-x64': 0.27.2 + '@esbuild/openharmony-arm64': 0.27.2 + '@esbuild/sunos-x64': 0.27.2 + '@esbuild/win32-arm64': 0.27.2 + '@esbuild/win32-ia32': 0.27.2 + '@esbuild/win32-x64': 0.27.2 + + estree-walker@2.0.2: {} + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + file-saver@2.0.5: {} + + follow-redirects@1.15.11: {} + + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + 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 + + hookable@5.5.3: {} + + is-what@5.5.0: {} + + jiti@2.6.1: {} + + lightningcss-android-arm64@1.30.2: + optional: true + + lightningcss-darwin-arm64@1.30.2: + optional: true + + lightningcss-darwin-x64@1.30.2: + optional: true + + lightningcss-freebsd-x64@1.30.2: + optional: true + + lightningcss-linux-arm-gnueabihf@1.30.2: + optional: true + + lightningcss-linux-arm64-gnu@1.30.2: + optional: true + + lightningcss-linux-arm64-musl@1.30.2: + optional: true + + lightningcss-linux-x64-gnu@1.30.2: + optional: true + + lightningcss-linux-x64-musl@1.30.2: + optional: true + + lightningcss-win32-arm64-msvc@1.30.2: + optional: true + + lightningcss-win32-x64-msvc@1.30.2: + optional: true + + lightningcss@1.30.2: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.30.2 + lightningcss-darwin-arm64: 1.30.2 + lightningcss-darwin-x64: 1.30.2 + lightningcss-freebsd-x64: 1.30.2 + lightningcss-linux-arm-gnueabihf: 1.30.2 + lightningcss-linux-arm64-gnu: 1.30.2 + lightningcss-linux-arm64-musl: 1.30.2 + lightningcss-linux-x64-gnu: 1.30.2 + lightningcss-linux-x64-musl: 1.30.2 + lightningcss-win32-arm64-msvc: 1.30.2 + lightningcss-win32-x64-msvc: 1.30.2 + + lucide-vue-next@0.562.0(vue@3.5.26(typescript@5.9.3)): + dependencies: + vue: 3.5.26(typescript@5.9.3) + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + math-intrinsics@1.1.0: {} + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mitt@3.0.1: {} + + muggle-string@0.4.1: {} + + nanoid@3.3.11: {} + + ohash@2.0.11: {} + + path-browserify@1.0.1: {} + + perfect-debounce@1.0.0: {} + + picocolors@1.1.1: {} + + picomatch@4.0.3: {} + + pinia@3.0.4(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3)): + dependencies: + '@vue/devtools-api': 7.7.9 + vue: 3.5.26(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + proxy-from-env@1.1.0: {} + + reka-ui@2.7.0(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3)): + dependencies: + '@floating-ui/dom': 1.7.4 + '@floating-ui/vue': 1.1.9(vue@3.5.26(typescript@5.9.3)) + '@internationalized/date': 3.10.1 + '@internationalized/number': 3.6.5 + '@tanstack/vue-virtual': 3.13.18(vue@3.5.26(typescript@5.9.3)) + '@vueuse/core': 12.8.2(typescript@5.9.3) + '@vueuse/shared': 12.8.2(typescript@5.9.3) + aria-hidden: 1.2.6 + defu: 6.1.4 + ohash: 2.0.11 + vue: 3.5.26(typescript@5.9.3) + transitivePeerDependencies: + - '@vue/composition-api' + - typescript + + rfdc@1.4.1: {} + + rollup@4.55.1: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.55.1 + '@rollup/rollup-android-arm64': 4.55.1 + '@rollup/rollup-darwin-arm64': 4.55.1 + '@rollup/rollup-darwin-x64': 4.55.1 + '@rollup/rollup-freebsd-arm64': 4.55.1 + '@rollup/rollup-freebsd-x64': 4.55.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.55.1 + '@rollup/rollup-linux-arm-musleabihf': 4.55.1 + '@rollup/rollup-linux-arm64-gnu': 4.55.1 + '@rollup/rollup-linux-arm64-musl': 4.55.1 + '@rollup/rollup-linux-loong64-gnu': 4.55.1 + '@rollup/rollup-linux-loong64-musl': 4.55.1 + '@rollup/rollup-linux-ppc64-gnu': 4.55.1 + '@rollup/rollup-linux-ppc64-musl': 4.55.1 + '@rollup/rollup-linux-riscv64-gnu': 4.55.1 + '@rollup/rollup-linux-riscv64-musl': 4.55.1 + '@rollup/rollup-linux-s390x-gnu': 4.55.1 + '@rollup/rollup-linux-x64-gnu': 4.55.1 + '@rollup/rollup-linux-x64-musl': 4.55.1 + '@rollup/rollup-openbsd-x64': 4.55.1 + '@rollup/rollup-openharmony-arm64': 4.55.1 + '@rollup/rollup-win32-arm64-msvc': 4.55.1 + '@rollup/rollup-win32-ia32-msvc': 4.55.1 + '@rollup/rollup-win32-x64-gnu': 4.55.1 + '@rollup/rollup-win32-x64-msvc': 4.55.1 + fsevents: 2.3.3 + + source-map-js@1.2.1: {} + + speakingurl@14.0.1: {} + + superjson@2.2.6: + dependencies: + copy-anything: 4.0.5 + + tailwind-merge@3.4.0: {} + + tailwindcss@4.1.18: {} + + tapable@2.3.0: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + tslib@2.8.1: {} + + tw-animate-css@1.4.0: {} + + typescript@5.9.3: {} + + undici-types@7.16.0: {} + + vite@7.3.1(@types/node@24.10.7)(jiti@2.6.1)(lightningcss@1.30.2): + dependencies: + esbuild: 0.27.2 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.55.1 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 24.10.7 + fsevents: 2.3.3 + jiti: 2.6.1 + lightningcss: 1.30.2 + + vscode-uri@3.1.0: {} + + vue-demi@0.14.10(vue@3.5.26(typescript@5.9.3)): + dependencies: + vue: 3.5.26(typescript@5.9.3) + + vue-router@4.6.4(vue@3.5.26(typescript@5.9.3)): + dependencies: + '@vue/devtools-api': 6.6.4 + vue: 3.5.26(typescript@5.9.3) + + vue-sonner@2.0.9: {} + + vue-tsc@3.2.2(typescript@5.9.3): + dependencies: + '@volar/typescript': 2.4.27 + '@vue/language-core': 3.2.2 + typescript: 5.9.3 + + vue@3.5.26(typescript@5.9.3): + dependencies: + '@vue/compiler-dom': 3.5.26 + '@vue/compiler-sfc': 3.5.26 + '@vue/runtime-dom': 3.5.26 + '@vue/server-renderer': 3.5.26(vue@3.5.26(typescript@5.9.3)) + '@vue/shared': 3.5.26 + optionalDependencies: + typescript: 5.9.3 diff --git a/frontend/public/vite.svg b/frontend/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/frontend/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/App.vue b/frontend/src/App.vue new file mode 100644 index 0000000..e76493b --- /dev/null +++ b/frontend/src/App.vue @@ -0,0 +1,23 @@ + + + diff --git a/frontend/src/api/accounts.ts b/frontend/src/api/accounts.ts new file mode 100644 index 0000000..9bb5d9f --- /dev/null +++ b/frontend/src/api/accounts.ts @@ -0,0 +1,58 @@ +import request from './request' + +export interface Account { + id: number + team_account_id: string + name: string + is_active: boolean + seats_in_use: number + seats_entitled: number + created_at: string + updated_at: string +} + +export interface AccountsResponse { + success: boolean + data?: Account[] + total?: number + page?: number + page_size?: number + message?: string +} + +export interface AccountResponse { + success: boolean + data?: Account + message?: string +} + +export interface CreateAccountRequest { + team_account_id: string + auth_token: string + name?: string +} + +export interface PaginationParams { + page?: number + page_size?: number +} + +export function getAccounts(params?: PaginationParams) { + const searchParams = new URLSearchParams() + if (params?.page) searchParams.set('page', String(params.page)) + if (params?.page_size) searchParams.set('page_size', String(params.page_size)) + const query = searchParams.toString() + return request.get(`/api/accounts${query ? `?${query}` : ''}`) +} + +export function createAccount(data: CreateAccountRequest) { + return request.post('/api/accounts/create', data) +} + +export function refreshAccount(id: number) { + return request.post(`/api/accounts/refresh?id=${id}`) +} + +export function deleteAccount(id: number) { + return request.delete(`/api/accounts/delete?id=${id}`) +} diff --git a/frontend/src/api/auth.ts b/frontend/src/api/auth.ts new file mode 100644 index 0000000..373c27c --- /dev/null +++ b/frontend/src/api/auth.ts @@ -0,0 +1,29 @@ +import request from './request' + +export interface LoginRequest { + username: string + password: string +} + +export interface LoginResponse { + success: boolean + token?: string + message?: string +} + +export interface ProfileResponse { + success: boolean + user?: { + id: number + username: string + } + message?: string +} + +export function login(data: LoginRequest) { + return request.post('/api/login', data) +} + +export function getProfile() { + return request.get('/api/profile') +} diff --git a/frontend/src/api/cardkeys.ts b/frontend/src/api/cardkeys.ts new file mode 100644 index 0000000..2463856 --- /dev/null +++ b/frontend/src/api/cardkeys.ts @@ -0,0 +1,73 @@ +import request from './request' + +export interface CardKey { + id: number + key: string + max_uses: number + used_count: number + validity_type: string + expires_at: string + is_active: boolean + created_by_id: number + created_at: string +} + +export interface CardKeysResponse { + success: boolean + keys?: CardKey[] + total?: number + page?: number + page_size?: number + message?: string +} + +export interface CardKeyResponse { + success: boolean + data?: CardKey + keys?: CardKey[] + message?: string +} + +export interface CreateCardKeyRequest { + validity_days?: number + max_uses?: number +} + +export interface BatchCreateCardKeyRequest { + count: number + validity_days?: number + max_uses?: number +} + +export interface PaginationParams { + page?: number + page_size?: number +} + +export function getCardKeys(params?: PaginationParams) { + const searchParams = new URLSearchParams() + if (params?.page) searchParams.set('page', String(params.page)) + if (params?.page_size) searchParams.set('page_size', String(params.page_size)) + const query = searchParams.toString() + return request.get(`/api/cardkeys${query ? `?${query}` : ''}`) +} + +export function createCardKey(data: CreateCardKeyRequest) { + return request.post('/api/cardkeys', data) +} + +export function batchCreateCardKeys(data: BatchCreateCardKeyRequest) { + return request.post('/api/cardkeys/batch', data) +} + +export function deleteCardKey(id: number) { + return request.delete(`/api/cardkeys/delete?id=${id}`) +} + +export function batchDeleteCardKeys(ids: number[]) { + return request.delete('/api/cardkeys/batch', { data: { ids } }) +} + +export function toggleCardKeyActive(id: number, is_active: boolean) { + return request.post('/api/cardkeys/toggle', { id, is_active }) +} diff --git a/frontend/src/api/invite.ts b/frontend/src/api/invite.ts new file mode 100644 index 0000000..dec3083 --- /dev/null +++ b/frontend/src/api/invite.ts @@ -0,0 +1,39 @@ +import request from './request' + +export interface InviteByCardRequest { + email: string + card_key: string +} + +export interface Invitation { + id: number + email: string + account_id: number + status: string + created_at: string +} + +export interface InvitationsResponse { + success: boolean + invitations?: Invitation[] + message?: string +} + +export interface DeleteInviteRequest { + email: string + account_id: number +} + +// Public endpoint - no auth required +export function inviteByCard(data: InviteByCardRequest) { + return request.post('/api/invite/card', data) +} + +// Admin endpoints - auth required +export function listInvitations(accountId: number) { + return request.get(`/api/invite?account_id=${accountId}`) +} + +export function deleteInvite(data: DeleteInviteRequest) { + return request.delete('/api/invite', { data }) +} diff --git a/frontend/src/api/request.ts b/frontend/src/api/request.ts new file mode 100644 index 0000000..8b2c306 --- /dev/null +++ b/frontend/src/api/request.ts @@ -0,0 +1,42 @@ +import axios, { type InternalAxiosRequestConfig, type AxiosResponse, type AxiosError } from 'axios' +import { useAuthStore } from '@/stores/auth' +import router from '@/router' + +const request = axios.create({ + baseURL: import.meta.env.VITE_BASE_URL || '', + timeout: 30000, + headers: { + 'Content-Type': 'application/json', + }, +}) + +// Request interceptor - add Authorization header +request.interceptors.request.use( + (config: InternalAxiosRequestConfig) => { + const authStore = useAuthStore() + if (authStore.token) { + config.headers.Authorization = `Bearer ${authStore.token}` + } + return config + }, + (error: AxiosError) => { + return Promise.reject(error) + } +) + +// Response interceptor - handle 401/403 +request.interceptors.response.use( + (response: AxiosResponse) => { + return response + }, + (error: AxiosError) => { + if (error.response?.status === 401 || error.response?.status === 403) { + const authStore = useAuthStore() + authStore.logout() + router.push('/admin/login') + } + return Promise.reject(error) + } +) + +export default request diff --git a/frontend/src/assets/vue.svg b/frontend/src/assets/vue.svg new file mode 100644 index 0000000..770e9d3 --- /dev/null +++ b/frontend/src/assets/vue.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/components/HelloWorld.vue b/frontend/src/components/HelloWorld.vue new file mode 100644 index 0000000..b58e52b --- /dev/null +++ b/frontend/src/components/HelloWorld.vue @@ -0,0 +1,41 @@ + + + + + diff --git a/frontend/src/components/ui/alert-dialog/AlertDialog.vue b/frontend/src/components/ui/alert-dialog/AlertDialog.vue new file mode 100644 index 0000000..b6e6b4b --- /dev/null +++ b/frontend/src/components/ui/alert-dialog/AlertDialog.vue @@ -0,0 +1,15 @@ + + + diff --git a/frontend/src/components/ui/alert-dialog/AlertDialogAction.vue b/frontend/src/components/ui/alert-dialog/AlertDialogAction.vue new file mode 100644 index 0000000..09cf6fc --- /dev/null +++ b/frontend/src/components/ui/alert-dialog/AlertDialogAction.vue @@ -0,0 +1,18 @@ + + + diff --git a/frontend/src/components/ui/alert-dialog/AlertDialogCancel.vue b/frontend/src/components/ui/alert-dialog/AlertDialogCancel.vue new file mode 100644 index 0000000..e261894 --- /dev/null +++ b/frontend/src/components/ui/alert-dialog/AlertDialogCancel.vue @@ -0,0 +1,25 @@ + + + diff --git a/frontend/src/components/ui/alert-dialog/AlertDialogContent.vue b/frontend/src/components/ui/alert-dialog/AlertDialogContent.vue new file mode 100644 index 0000000..4597f0d --- /dev/null +++ b/frontend/src/components/ui/alert-dialog/AlertDialogContent.vue @@ -0,0 +1,44 @@ + + + diff --git a/frontend/src/components/ui/alert-dialog/AlertDialogDescription.vue b/frontend/src/components/ui/alert-dialog/AlertDialogDescription.vue new file mode 100644 index 0000000..69642c9 --- /dev/null +++ b/frontend/src/components/ui/alert-dialog/AlertDialogDescription.vue @@ -0,0 +1,23 @@ + + + diff --git a/frontend/src/components/ui/alert-dialog/AlertDialogFooter.vue b/frontend/src/components/ui/alert-dialog/AlertDialogFooter.vue new file mode 100644 index 0000000..50d4098 --- /dev/null +++ b/frontend/src/components/ui/alert-dialog/AlertDialogFooter.vue @@ -0,0 +1,22 @@ + + + diff --git a/frontend/src/components/ui/alert-dialog/AlertDialogHeader.vue b/frontend/src/components/ui/alert-dialog/AlertDialogHeader.vue new file mode 100644 index 0000000..dbe72a7 --- /dev/null +++ b/frontend/src/components/ui/alert-dialog/AlertDialogHeader.vue @@ -0,0 +1,17 @@ + + + diff --git a/frontend/src/components/ui/alert-dialog/AlertDialogTitle.vue b/frontend/src/components/ui/alert-dialog/AlertDialogTitle.vue new file mode 100644 index 0000000..bb97e4d --- /dev/null +++ b/frontend/src/components/ui/alert-dialog/AlertDialogTitle.vue @@ -0,0 +1,21 @@ + + + diff --git a/frontend/src/components/ui/alert-dialog/AlertDialogTrigger.vue b/frontend/src/components/ui/alert-dialog/AlertDialogTrigger.vue new file mode 100644 index 0000000..98d40ee --- /dev/null +++ b/frontend/src/components/ui/alert-dialog/AlertDialogTrigger.vue @@ -0,0 +1,12 @@ + + + diff --git a/frontend/src/components/ui/alert-dialog/index.ts b/frontend/src/components/ui/alert-dialog/index.ts new file mode 100644 index 0000000..cf1b45d --- /dev/null +++ b/frontend/src/components/ui/alert-dialog/index.ts @@ -0,0 +1,9 @@ +export { default as AlertDialog } from "./AlertDialog.vue" +export { default as AlertDialogAction } from "./AlertDialogAction.vue" +export { default as AlertDialogCancel } from "./AlertDialogCancel.vue" +export { default as AlertDialogContent } from "./AlertDialogContent.vue" +export { default as AlertDialogDescription } from "./AlertDialogDescription.vue" +export { default as AlertDialogFooter } from "./AlertDialogFooter.vue" +export { default as AlertDialogHeader } from "./AlertDialogHeader.vue" +export { default as AlertDialogTitle } from "./AlertDialogTitle.vue" +export { default as AlertDialogTrigger } from "./AlertDialogTrigger.vue" diff --git a/frontend/src/components/ui/alert/Alert.vue b/frontend/src/components/ui/alert/Alert.vue new file mode 100644 index 0000000..a9d336f --- /dev/null +++ b/frontend/src/components/ui/alert/Alert.vue @@ -0,0 +1,21 @@ + + + diff --git a/frontend/src/components/ui/alert/AlertDescription.vue b/frontend/src/components/ui/alert/AlertDescription.vue new file mode 100644 index 0000000..9f7d24d --- /dev/null +++ b/frontend/src/components/ui/alert/AlertDescription.vue @@ -0,0 +1,17 @@ + + + diff --git a/frontend/src/components/ui/alert/AlertTitle.vue b/frontend/src/components/ui/alert/AlertTitle.vue new file mode 100644 index 0000000..b218384 --- /dev/null +++ b/frontend/src/components/ui/alert/AlertTitle.vue @@ -0,0 +1,17 @@ + + + diff --git a/frontend/src/components/ui/alert/index.ts b/frontend/src/components/ui/alert/index.ts new file mode 100644 index 0000000..42d07b6 --- /dev/null +++ b/frontend/src/components/ui/alert/index.ts @@ -0,0 +1,24 @@ +import type { VariantProps } from "class-variance-authority" +import { cva } from "class-variance-authority" + +export { default as Alert } from "./Alert.vue" +export { default as AlertDescription } from "./AlertDescription.vue" +export { default as AlertTitle } from "./AlertTitle.vue" + +export const alertVariants = cva( + "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", + { + variants: { + variant: { + default: "bg-card text-card-foreground", + destructive: + "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90", + }, + }, + defaultVariants: { + variant: "default", + }, + }, +) + +export type AlertVariants = VariantProps diff --git a/frontend/src/components/ui/badge/Badge.vue b/frontend/src/components/ui/badge/Badge.vue new file mode 100644 index 0000000..d894dfe --- /dev/null +++ b/frontend/src/components/ui/badge/Badge.vue @@ -0,0 +1,26 @@ + + + diff --git a/frontend/src/components/ui/badge/index.ts b/frontend/src/components/ui/badge/index.ts new file mode 100644 index 0000000..bbc0dfa --- /dev/null +++ b/frontend/src/components/ui/badge/index.ts @@ -0,0 +1,26 @@ +import type { VariantProps } from "class-variance-authority" +import { cva } from "class-variance-authority" + +export { default as Badge } from "./Badge.vue" + +export const badgeVariants = cva( + "inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", + secondary: + "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", + destructive: + "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + }, +) +export type BadgeVariants = VariantProps diff --git a/frontend/src/components/ui/button/Button.vue b/frontend/src/components/ui/button/Button.vue new file mode 100644 index 0000000..374320b --- /dev/null +++ b/frontend/src/components/ui/button/Button.vue @@ -0,0 +1,29 @@ + + + diff --git a/frontend/src/components/ui/button/index.ts b/frontend/src/components/ui/button/index.ts new file mode 100644 index 0000000..26e2c55 --- /dev/null +++ b/frontend/src/components/ui/button/index.ts @@ -0,0 +1,38 @@ +import type { VariantProps } from "class-variance-authority" +import { cva } from "class-variance-authority" + +export { default as Button } from "./Button.vue" + +export const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: + "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + "default": "h-9 px-4 py-2 has-[>svg]:px-3", + "sm": "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", + "lg": "h-10 rounded-md px-6 has-[>svg]:px-4", + "icon": "size-9", + "icon-sm": "size-8", + "icon-lg": "size-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + }, +) +export type ButtonVariants = VariantProps diff --git a/frontend/src/components/ui/card/Card.vue b/frontend/src/components/ui/card/Card.vue new file mode 100644 index 0000000..f5a0707 --- /dev/null +++ b/frontend/src/components/ui/card/Card.vue @@ -0,0 +1,22 @@ + + + diff --git a/frontend/src/components/ui/card/CardAction.vue b/frontend/src/components/ui/card/CardAction.vue new file mode 100644 index 0000000..c91638b --- /dev/null +++ b/frontend/src/components/ui/card/CardAction.vue @@ -0,0 +1,17 @@ + + + diff --git a/frontend/src/components/ui/card/CardContent.vue b/frontend/src/components/ui/card/CardContent.vue new file mode 100644 index 0000000..dfbc552 --- /dev/null +++ b/frontend/src/components/ui/card/CardContent.vue @@ -0,0 +1,17 @@ + + + diff --git a/frontend/src/components/ui/card/CardDescription.vue b/frontend/src/components/ui/card/CardDescription.vue new file mode 100644 index 0000000..71c1b8d --- /dev/null +++ b/frontend/src/components/ui/card/CardDescription.vue @@ -0,0 +1,17 @@ + + + diff --git a/frontend/src/components/ui/card/CardFooter.vue b/frontend/src/components/ui/card/CardFooter.vue new file mode 100644 index 0000000..9e3739e --- /dev/null +++ b/frontend/src/components/ui/card/CardFooter.vue @@ -0,0 +1,17 @@ + + + diff --git a/frontend/src/components/ui/card/CardHeader.vue b/frontend/src/components/ui/card/CardHeader.vue new file mode 100644 index 0000000..4fe4da4 --- /dev/null +++ b/frontend/src/components/ui/card/CardHeader.vue @@ -0,0 +1,17 @@ + + + diff --git a/frontend/src/components/ui/card/CardTitle.vue b/frontend/src/components/ui/card/CardTitle.vue new file mode 100644 index 0000000..5f479e7 --- /dev/null +++ b/frontend/src/components/ui/card/CardTitle.vue @@ -0,0 +1,17 @@ + + + diff --git a/frontend/src/components/ui/card/index.ts b/frontend/src/components/ui/card/index.ts new file mode 100644 index 0000000..1627758 --- /dev/null +++ b/frontend/src/components/ui/card/index.ts @@ -0,0 +1,7 @@ +export { default as Card } from "./Card.vue" +export { default as CardAction } from "./CardAction.vue" +export { default as CardContent } from "./CardContent.vue" +export { default as CardDescription } from "./CardDescription.vue" +export { default as CardFooter } from "./CardFooter.vue" +export { default as CardHeader } from "./CardHeader.vue" +export { default as CardTitle } from "./CardTitle.vue" diff --git a/frontend/src/components/ui/checkbox/Checkbox.vue b/frontend/src/components/ui/checkbox/Checkbox.vue new file mode 100644 index 0000000..6604cbd --- /dev/null +++ b/frontend/src/components/ui/checkbox/Checkbox.vue @@ -0,0 +1,35 @@ + + + diff --git a/frontend/src/components/ui/checkbox/index.ts b/frontend/src/components/ui/checkbox/index.ts new file mode 100644 index 0000000..3391a85 --- /dev/null +++ b/frontend/src/components/ui/checkbox/index.ts @@ -0,0 +1 @@ +export { default as Checkbox } from "./Checkbox.vue" diff --git a/frontend/src/components/ui/dialog/Dialog.vue b/frontend/src/components/ui/dialog/Dialog.vue new file mode 100644 index 0000000..ade5260 --- /dev/null +++ b/frontend/src/components/ui/dialog/Dialog.vue @@ -0,0 +1,19 @@ + + + diff --git a/frontend/src/components/ui/dialog/DialogClose.vue b/frontend/src/components/ui/dialog/DialogClose.vue new file mode 100644 index 0000000..c5fae04 --- /dev/null +++ b/frontend/src/components/ui/dialog/DialogClose.vue @@ -0,0 +1,15 @@ + + + diff --git a/frontend/src/components/ui/dialog/DialogContent.vue b/frontend/src/components/ui/dialog/DialogContent.vue new file mode 100644 index 0000000..7f86b47 --- /dev/null +++ b/frontend/src/components/ui/dialog/DialogContent.vue @@ -0,0 +1,53 @@ + + + diff --git a/frontend/src/components/ui/dialog/DialogDescription.vue b/frontend/src/components/ui/dialog/DialogDescription.vue new file mode 100644 index 0000000..f52e655 --- /dev/null +++ b/frontend/src/components/ui/dialog/DialogDescription.vue @@ -0,0 +1,23 @@ + + + diff --git a/frontend/src/components/ui/dialog/DialogFooter.vue b/frontend/src/components/ui/dialog/DialogFooter.vue new file mode 100644 index 0000000..0a936e6 --- /dev/null +++ b/frontend/src/components/ui/dialog/DialogFooter.vue @@ -0,0 +1,15 @@ + + + diff --git a/frontend/src/components/ui/dialog/DialogHeader.vue b/frontend/src/components/ui/dialog/DialogHeader.vue new file mode 100644 index 0000000..bfc3c64 --- /dev/null +++ b/frontend/src/components/ui/dialog/DialogHeader.vue @@ -0,0 +1,17 @@ + + + diff --git a/frontend/src/components/ui/dialog/DialogOverlay.vue b/frontend/src/components/ui/dialog/DialogOverlay.vue new file mode 100644 index 0000000..7790077 --- /dev/null +++ b/frontend/src/components/ui/dialog/DialogOverlay.vue @@ -0,0 +1,21 @@ + + + diff --git a/frontend/src/components/ui/dialog/DialogScrollContent.vue b/frontend/src/components/ui/dialog/DialogScrollContent.vue new file mode 100644 index 0000000..f2475db --- /dev/null +++ b/frontend/src/components/ui/dialog/DialogScrollContent.vue @@ -0,0 +1,59 @@ + + + diff --git a/frontend/src/components/ui/dialog/DialogTitle.vue b/frontend/src/components/ui/dialog/DialogTitle.vue new file mode 100644 index 0000000..860f01a --- /dev/null +++ b/frontend/src/components/ui/dialog/DialogTitle.vue @@ -0,0 +1,23 @@ + + + diff --git a/frontend/src/components/ui/dialog/DialogTrigger.vue b/frontend/src/components/ui/dialog/DialogTrigger.vue new file mode 100644 index 0000000..49667e9 --- /dev/null +++ b/frontend/src/components/ui/dialog/DialogTrigger.vue @@ -0,0 +1,15 @@ + + + diff --git a/frontend/src/components/ui/dialog/index.ts b/frontend/src/components/ui/dialog/index.ts new file mode 100644 index 0000000..6768b09 --- /dev/null +++ b/frontend/src/components/ui/dialog/index.ts @@ -0,0 +1,10 @@ +export { default as Dialog } from "./Dialog.vue" +export { default as DialogClose } from "./DialogClose.vue" +export { default as DialogContent } from "./DialogContent.vue" +export { default as DialogDescription } from "./DialogDescription.vue" +export { default as DialogFooter } from "./DialogFooter.vue" +export { default as DialogHeader } from "./DialogHeader.vue" +export { default as DialogOverlay } from "./DialogOverlay.vue" +export { default as DialogScrollContent } from "./DialogScrollContent.vue" +export { default as DialogTitle } from "./DialogTitle.vue" +export { default as DialogTrigger } from "./DialogTrigger.vue" diff --git a/frontend/src/components/ui/input/Input.vue b/frontend/src/components/ui/input/Input.vue new file mode 100644 index 0000000..e5135c1 --- /dev/null +++ b/frontend/src/components/ui/input/Input.vue @@ -0,0 +1,33 @@ + + + diff --git a/frontend/src/components/ui/input/index.ts b/frontend/src/components/ui/input/index.ts new file mode 100644 index 0000000..9976b86 --- /dev/null +++ b/frontend/src/components/ui/input/index.ts @@ -0,0 +1 @@ +export { default as Input } from "./Input.vue" diff --git a/frontend/src/components/ui/label/Label.vue b/frontend/src/components/ui/label/Label.vue new file mode 100644 index 0000000..ee63970 --- /dev/null +++ b/frontend/src/components/ui/label/Label.vue @@ -0,0 +1,26 @@ + + + diff --git a/frontend/src/components/ui/label/index.ts b/frontend/src/components/ui/label/index.ts new file mode 100644 index 0000000..036e35c --- /dev/null +++ b/frontend/src/components/ui/label/index.ts @@ -0,0 +1 @@ +export { default as Label } from "./Label.vue" diff --git a/frontend/src/components/ui/pagination/Pagination.vue b/frontend/src/components/ui/pagination/Pagination.vue new file mode 100644 index 0000000..d410b24 --- /dev/null +++ b/frontend/src/components/ui/pagination/Pagination.vue @@ -0,0 +1,26 @@ + + + diff --git a/frontend/src/components/ui/pagination/PaginationContent.vue b/frontend/src/components/ui/pagination/PaginationContent.vue new file mode 100644 index 0000000..5150b49 --- /dev/null +++ b/frontend/src/components/ui/pagination/PaginationContent.vue @@ -0,0 +1,22 @@ + + + diff --git a/frontend/src/components/ui/pagination/PaginationEllipsis.vue b/frontend/src/components/ui/pagination/PaginationEllipsis.vue new file mode 100644 index 0000000..cc9e002 --- /dev/null +++ b/frontend/src/components/ui/pagination/PaginationEllipsis.vue @@ -0,0 +1,25 @@ + + + diff --git a/frontend/src/components/ui/pagination/PaginationFirst.vue b/frontend/src/components/ui/pagination/PaginationFirst.vue new file mode 100644 index 0000000..e437091 --- /dev/null +++ b/frontend/src/components/ui/pagination/PaginationFirst.vue @@ -0,0 +1,33 @@ + + + diff --git a/frontend/src/components/ui/pagination/PaginationItem.vue b/frontend/src/components/ui/pagination/PaginationItem.vue new file mode 100644 index 0000000..bc03720 --- /dev/null +++ b/frontend/src/components/ui/pagination/PaginationItem.vue @@ -0,0 +1,34 @@ + + + diff --git a/frontend/src/components/ui/pagination/PaginationLast.vue b/frontend/src/components/ui/pagination/PaginationLast.vue new file mode 100644 index 0000000..9e8087d --- /dev/null +++ b/frontend/src/components/ui/pagination/PaginationLast.vue @@ -0,0 +1,33 @@ + + + diff --git a/frontend/src/components/ui/pagination/PaginationNext.vue b/frontend/src/components/ui/pagination/PaginationNext.vue new file mode 100644 index 0000000..da5ccfb --- /dev/null +++ b/frontend/src/components/ui/pagination/PaginationNext.vue @@ -0,0 +1,33 @@ + + + diff --git a/frontend/src/components/ui/pagination/PaginationPrevious.vue b/frontend/src/components/ui/pagination/PaginationPrevious.vue new file mode 100644 index 0000000..d705c53 --- /dev/null +++ b/frontend/src/components/ui/pagination/PaginationPrevious.vue @@ -0,0 +1,33 @@ + + + diff --git a/frontend/src/components/ui/pagination/index.ts b/frontend/src/components/ui/pagination/index.ts new file mode 100644 index 0000000..51ae7fd --- /dev/null +++ b/frontend/src/components/ui/pagination/index.ts @@ -0,0 +1,8 @@ +export { default as Pagination } from "./Pagination.vue" +export { default as PaginationContent } from "./PaginationContent.vue" +export { default as PaginationEllipsis } from "./PaginationEllipsis.vue" +export { default as PaginationFirst } from "./PaginationFirst.vue" +export { default as PaginationItem } from "./PaginationItem.vue" +export { default as PaginationLast } from "./PaginationLast.vue" +export { default as PaginationNext } from "./PaginationNext.vue" +export { default as PaginationPrevious } from "./PaginationPrevious.vue" diff --git a/frontend/src/components/ui/select/Select.vue b/frontend/src/components/ui/select/Select.vue new file mode 100644 index 0000000..c94bbe8 --- /dev/null +++ b/frontend/src/components/ui/select/Select.vue @@ -0,0 +1,19 @@ + + + diff --git a/frontend/src/components/ui/select/SelectContent.vue b/frontend/src/components/ui/select/SelectContent.vue new file mode 100644 index 0000000..adf04ec --- /dev/null +++ b/frontend/src/components/ui/select/SelectContent.vue @@ -0,0 +1,51 @@ + + + diff --git a/frontend/src/components/ui/select/SelectGroup.vue b/frontend/src/components/ui/select/SelectGroup.vue new file mode 100644 index 0000000..e981c6c --- /dev/null +++ b/frontend/src/components/ui/select/SelectGroup.vue @@ -0,0 +1,15 @@ + + + diff --git a/frontend/src/components/ui/select/SelectItem.vue b/frontend/src/components/ui/select/SelectItem.vue new file mode 100644 index 0000000..9371764 --- /dev/null +++ b/frontend/src/components/ui/select/SelectItem.vue @@ -0,0 +1,44 @@ + + + diff --git a/frontend/src/components/ui/select/SelectItemText.vue b/frontend/src/components/ui/select/SelectItemText.vue new file mode 100644 index 0000000..b6700b1 --- /dev/null +++ b/frontend/src/components/ui/select/SelectItemText.vue @@ -0,0 +1,15 @@ + + + diff --git a/frontend/src/components/ui/select/SelectLabel.vue b/frontend/src/components/ui/select/SelectLabel.vue new file mode 100644 index 0000000..5b6650c --- /dev/null +++ b/frontend/src/components/ui/select/SelectLabel.vue @@ -0,0 +1,17 @@ + + + diff --git a/frontend/src/components/ui/select/SelectScrollDownButton.vue b/frontend/src/components/ui/select/SelectScrollDownButton.vue new file mode 100644 index 0000000..7dc7670 --- /dev/null +++ b/frontend/src/components/ui/select/SelectScrollDownButton.vue @@ -0,0 +1,26 @@ + + + diff --git a/frontend/src/components/ui/select/SelectScrollUpButton.vue b/frontend/src/components/ui/select/SelectScrollUpButton.vue new file mode 100644 index 0000000..07fe87e --- /dev/null +++ b/frontend/src/components/ui/select/SelectScrollUpButton.vue @@ -0,0 +1,26 @@ + + + diff --git a/frontend/src/components/ui/select/SelectSeparator.vue b/frontend/src/components/ui/select/SelectSeparator.vue new file mode 100644 index 0000000..4b5c885 --- /dev/null +++ b/frontend/src/components/ui/select/SelectSeparator.vue @@ -0,0 +1,19 @@ + + + diff --git a/frontend/src/components/ui/select/SelectTrigger.vue b/frontend/src/components/ui/select/SelectTrigger.vue new file mode 100644 index 0000000..667908b --- /dev/null +++ b/frontend/src/components/ui/select/SelectTrigger.vue @@ -0,0 +1,33 @@ + + + diff --git a/frontend/src/components/ui/select/SelectValue.vue b/frontend/src/components/ui/select/SelectValue.vue new file mode 100644 index 0000000..d5ce58b --- /dev/null +++ b/frontend/src/components/ui/select/SelectValue.vue @@ -0,0 +1,15 @@ + + + diff --git a/frontend/src/components/ui/select/index.ts b/frontend/src/components/ui/select/index.ts new file mode 100644 index 0000000..96eae60 --- /dev/null +++ b/frontend/src/components/ui/select/index.ts @@ -0,0 +1,11 @@ +export { default as Select } from "./Select.vue" +export { default as SelectContent } from "./SelectContent.vue" +export { default as SelectGroup } from "./SelectGroup.vue" +export { default as SelectItem } from "./SelectItem.vue" +export { default as SelectItemText } from "./SelectItemText.vue" +export { default as SelectLabel } from "./SelectLabel.vue" +export { default as SelectScrollDownButton } from "./SelectScrollDownButton.vue" +export { default as SelectScrollUpButton } from "./SelectScrollUpButton.vue" +export { default as SelectSeparator } from "./SelectSeparator.vue" +export { default as SelectTrigger } from "./SelectTrigger.vue" +export { default as SelectValue } from "./SelectValue.vue" diff --git a/frontend/src/components/ui/skeleton/Skeleton.vue b/frontend/src/components/ui/skeleton/Skeleton.vue new file mode 100644 index 0000000..0dadcef --- /dev/null +++ b/frontend/src/components/ui/skeleton/Skeleton.vue @@ -0,0 +1,17 @@ + + + diff --git a/frontend/src/components/ui/skeleton/index.ts b/frontend/src/components/ui/skeleton/index.ts new file mode 100644 index 0000000..e5ce72c --- /dev/null +++ b/frontend/src/components/ui/skeleton/index.ts @@ -0,0 +1 @@ +export { default as Skeleton } from "./Skeleton.vue" diff --git a/frontend/src/components/ui/sonner/Sonner.vue b/frontend/src/components/ui/sonner/Sonner.vue new file mode 100644 index 0000000..6830896 --- /dev/null +++ b/frontend/src/components/ui/sonner/Sonner.vue @@ -0,0 +1,42 @@ + + + diff --git a/frontend/src/components/ui/sonner/index.ts b/frontend/src/components/ui/sonner/index.ts new file mode 100644 index 0000000..6673112 --- /dev/null +++ b/frontend/src/components/ui/sonner/index.ts @@ -0,0 +1 @@ +export { default as Toaster } from "./Sonner.vue" diff --git a/frontend/src/components/ui/switch/Switch.vue b/frontend/src/components/ui/switch/Switch.vue new file mode 100644 index 0000000..2e725ed --- /dev/null +++ b/frontend/src/components/ui/switch/Switch.vue @@ -0,0 +1,38 @@ + + + diff --git a/frontend/src/components/ui/switch/index.ts b/frontend/src/components/ui/switch/index.ts new file mode 100644 index 0000000..cc081f3 --- /dev/null +++ b/frontend/src/components/ui/switch/index.ts @@ -0,0 +1 @@ +export { default as Switch } from "./Switch.vue" diff --git a/frontend/src/components/ui/table/Table.vue b/frontend/src/components/ui/table/Table.vue new file mode 100644 index 0000000..0d0cd9b --- /dev/null +++ b/frontend/src/components/ui/table/Table.vue @@ -0,0 +1,16 @@ + + + diff --git a/frontend/src/components/ui/table/TableBody.vue b/frontend/src/components/ui/table/TableBody.vue new file mode 100644 index 0000000..d14a2d3 --- /dev/null +++ b/frontend/src/components/ui/table/TableBody.vue @@ -0,0 +1,17 @@ + + + diff --git a/frontend/src/components/ui/table/TableCaption.vue b/frontend/src/components/ui/table/TableCaption.vue new file mode 100644 index 0000000..3630084 --- /dev/null +++ b/frontend/src/components/ui/table/TableCaption.vue @@ -0,0 +1,17 @@ + + + diff --git a/frontend/src/components/ui/table/TableCell.vue b/frontend/src/components/ui/table/TableCell.vue new file mode 100644 index 0000000..d6e9ed2 --- /dev/null +++ b/frontend/src/components/ui/table/TableCell.vue @@ -0,0 +1,22 @@ + + + diff --git a/frontend/src/components/ui/table/TableEmpty.vue b/frontend/src/components/ui/table/TableEmpty.vue new file mode 100644 index 0000000..9519328 --- /dev/null +++ b/frontend/src/components/ui/table/TableEmpty.vue @@ -0,0 +1,34 @@ + + + diff --git a/frontend/src/components/ui/table/TableFooter.vue b/frontend/src/components/ui/table/TableFooter.vue new file mode 100644 index 0000000..29e0ce9 --- /dev/null +++ b/frontend/src/components/ui/table/TableFooter.vue @@ -0,0 +1,17 @@ + + + diff --git a/frontend/src/components/ui/table/TableHead.vue b/frontend/src/components/ui/table/TableHead.vue new file mode 100644 index 0000000..f83efe5 --- /dev/null +++ b/frontend/src/components/ui/table/TableHead.vue @@ -0,0 +1,17 @@ + + + diff --git a/frontend/src/components/ui/table/TableHeader.vue b/frontend/src/components/ui/table/TableHeader.vue new file mode 100644 index 0000000..b4ab5cf --- /dev/null +++ b/frontend/src/components/ui/table/TableHeader.vue @@ -0,0 +1,17 @@ + + + diff --git a/frontend/src/components/ui/table/TableRow.vue b/frontend/src/components/ui/table/TableRow.vue new file mode 100644 index 0000000..8f1d172 --- /dev/null +++ b/frontend/src/components/ui/table/TableRow.vue @@ -0,0 +1,17 @@ + + + diff --git a/frontend/src/components/ui/table/index.ts b/frontend/src/components/ui/table/index.ts new file mode 100644 index 0000000..3be308b --- /dev/null +++ b/frontend/src/components/ui/table/index.ts @@ -0,0 +1,9 @@ +export { default as Table } from "./Table.vue" +export { default as TableBody } from "./TableBody.vue" +export { default as TableCaption } from "./TableCaption.vue" +export { default as TableCell } from "./TableCell.vue" +export { default as TableEmpty } from "./TableEmpty.vue" +export { default as TableFooter } from "./TableFooter.vue" +export { default as TableHead } from "./TableHead.vue" +export { default as TableHeader } from "./TableHeader.vue" +export { default as TableRow } from "./TableRow.vue" diff --git a/frontend/src/components/ui/table/utils.ts b/frontend/src/components/ui/table/utils.ts new file mode 100644 index 0000000..3d4fd12 --- /dev/null +++ b/frontend/src/components/ui/table/utils.ts @@ -0,0 +1,10 @@ +import type { Updater } from "@tanstack/vue-table" + +import type { Ref } from "vue" +import { isFunction } from "@tanstack/vue-table" + +export function valueUpdater(updaterOrValue: Updater, ref: Ref) { + ref.value = isFunction(updaterOrValue) + ? updaterOrValue(ref.value) + : updaterOrValue +} diff --git a/frontend/src/layouts/AdminLayout.vue b/frontend/src/layouts/AdminLayout.vue new file mode 100644 index 0000000..316e447 --- /dev/null +++ b/frontend/src/layouts/AdminLayout.vue @@ -0,0 +1,138 @@ + + + diff --git a/frontend/src/layouts/PublicLayout.vue b/frontend/src/layouts/PublicLayout.vue new file mode 100644 index 0000000..90d6be7 --- /dev/null +++ b/frontend/src/layouts/PublicLayout.vue @@ -0,0 +1,17 @@ + + + diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts new file mode 100644 index 0000000..c66a9d9 --- /dev/null +++ b/frontend/src/lib/utils.ts @@ -0,0 +1,7 @@ +import type { ClassValue } from "clsx" +import { clsx } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/frontend/src/main.ts b/frontend/src/main.ts new file mode 100644 index 0000000..c574ca6 --- /dev/null +++ b/frontend/src/main.ts @@ -0,0 +1,13 @@ +import { createApp } from 'vue' +import { createPinia } from 'pinia' +import router from './router' +import App from './App.vue' +import './style.css' +import 'vue-sonner/style.css'; // 导入样式 + +const app = createApp(App) + +app.use(createPinia()) +app.use(router) + +app.mount('#app') diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts new file mode 100644 index 0000000..5e849e1 --- /dev/null +++ b/frontend/src/router/index.ts @@ -0,0 +1,80 @@ +import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router' +import { useAuthStore } from '@/stores/auth' + +const routes: RouteRecordRaw[] = [ + // Public routes + { + path: '/', + name: 'join', + component: () => import('@/views/public/JoinPage.vue'), + meta: { layout: 'public' }, + }, + // Admin routes + { + path: '/admin', + redirect: '/admin/dashboard', + }, + { + path: '/admin/login', + name: 'admin-login', + component: () => import('@/views/admin/LoginPage.vue'), + meta: { layout: 'public' }, + }, + { + path: '/admin/dashboard', + name: 'admin-dashboard', + component: () => import('@/views/admin/DashboardPage.vue'), + meta: { layout: 'admin', requiresAuth: true }, + }, + { + path: '/admin/teams', + name: 'admin-teams', + component: () => import('@/views/admin/TeamsPage.vue'), + meta: { layout: 'admin', requiresAuth: true }, + }, + { + path: '/admin/teams/:id/invites', + name: 'admin-team-invites', + component: () => import('@/views/admin/TeamInvitesPage.vue'), + meta: { layout: 'admin', requiresAuth: true }, + }, + { + path: '/admin/cardkeys', + name: 'admin-cardkeys', + component: () => import('@/views/admin/CardKeysPage.vue'), + meta: { layout: 'admin', requiresAuth: true }, + }, +] + +const router = createRouter({ + history: createWebHistory(), + routes, +}) + +// Navigation guard +router.beforeEach(async (to, _from, next) => { + const authStore = useAuthStore() + + if (to.meta.requiresAuth) { + if (!authStore.isAuthenticated) { + next({ name: 'admin-login', query: { redirect: to.fullPath } }) + return + } + // Verify token is still valid + const isValid = await authStore.checkAuth() + if (!isValid) { + next({ name: 'admin-login', query: { redirect: to.fullPath } }) + return + } + } + + // Redirect logged-in users away from login page + if (to.name === 'admin-login' && authStore.isAuthenticated) { + next({ name: 'admin-dashboard' }) + return + } + + next() +}) + +export default router diff --git a/frontend/src/stores/accounts.ts b/frontend/src/stores/accounts.ts new file mode 100644 index 0000000..5633a80 --- /dev/null +++ b/frontend/src/stores/accounts.ts @@ -0,0 +1,53 @@ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import { getAccounts, type Account } from '@/api/accounts' + +export const useAccountsStore = defineStore('accounts', () => { + const accounts = ref([]) + const loading = ref(false) + const error = ref(null) + + const totalTeams = computed(() => accounts.value.length) + const validTeams = computed(() => accounts.value.filter(a => a.is_active).length) + const invalidTeams = computed(() => accounts.value.filter(a => !a.is_active).length) + const totalAvailableSeats = computed(() => + accounts.value.reduce((sum, a) => sum + Math.max(0, (a.seats_entitled || 0) - (a.seats_in_use || 0)), 0) + ) + + async function fetchAccounts() { + loading.value = true + error.value = null + try { + const response = await getAccounts() + if (response.data.success && response.data.data) { + accounts.value = response.data.data + } else { + throw new Error(response.data.message || '获取账号列表失败') + } + } catch (e: any) { + error.value = e.message || '获取账号列表失败' + throw e + } finally { + loading.value = false + } + } + + function updateAccount(updatedAccount: Account) { + const index = accounts.value.findIndex(a => a.id === updatedAccount.id) + if (index !== -1) { + accounts.value[index] = updatedAccount + } + } + + return { + accounts, + loading, + error, + totalTeams, + validTeams, + invalidTeams, + totalAvailableSeats, + fetchAccounts, + updateAccount, + } +}) diff --git a/frontend/src/stores/auth.ts b/frontend/src/stores/auth.ts new file mode 100644 index 0000000..68edd7b --- /dev/null +++ b/frontend/src/stores/auth.ts @@ -0,0 +1,69 @@ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import { login as apiLogin, getProfile, type LoginRequest } from '@/api/auth' + +export const useAuthStore = defineStore('auth', () => { + const token = ref(localStorage.getItem('token')) + const user = ref<{ id: number; username: string } | null>(null) + + const isAuthenticated = computed(() => !!token.value) + + async function login(credentials: LoginRequest): Promise<{ success: boolean; message?: string }> { + try { + const response = await apiLogin(credentials) + if (response.data.success && response.data.token) { + token.value = response.data.token + localStorage.setItem('token', response.data.token) + return { success: true } + } + // 后端返回 success: false + return { success: false, message: response.data.message || '登录失败' } + } catch (e: any) { + // 处理网络错误 + if (e.code === 'ERR_NETWORK' || e.message === 'Network Error') { + return { success: false, message: '无法连接到服务器,请检查后端服务是否已启动' } + } + // 处理超时 + if (e.code === 'ECONNABORTED') { + return { success: false, message: '连接超时,请稍后重试' } + } + // 处理后端返回的错误 (401, 403 等) + if (e.response?.data?.message) { + return { success: false, message: e.response.data.message } + } + // 其他错误 + return { success: false, message: '登录失败,请稍后重试' } + } + } + + async function checkAuth() { + if (!token.value) return false + try { + const response = await getProfile() + if (response.data.success && response.data.user) { + user.value = response.data.user + return true + } + logout() + return false + } catch { + logout() + return false + } + } + + function logout() { + token.value = null + user.value = null + localStorage.removeItem('token') + } + + return { + token, + user, + isAuthenticated, + login, + checkAuth, + logout, + } +}) diff --git a/frontend/src/style.css b/frontend/src/style.css new file mode 100644 index 0000000..f4c1e9b --- /dev/null +++ b/frontend/src/style.css @@ -0,0 +1,120 @@ +@import "tailwindcss"; +@import "tw-animate-css"; + +@custom-variant dark (&:is(.dark *)); + +@theme inline { + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); +} + +:root { + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/frontend/src/views/admin/CardKeysPage.vue b/frontend/src/views/admin/CardKeysPage.vue new file mode 100644 index 0000000..32fc063 --- /dev/null +++ b/frontend/src/views/admin/CardKeysPage.vue @@ -0,0 +1,608 @@ + + + diff --git a/frontend/src/views/admin/DashboardPage.vue b/frontend/src/views/admin/DashboardPage.vue new file mode 100644 index 0000000..2630f37 --- /dev/null +++ b/frontend/src/views/admin/DashboardPage.vue @@ -0,0 +1,98 @@ + + + diff --git a/frontend/src/views/admin/LoginPage.vue b/frontend/src/views/admin/LoginPage.vue new file mode 100644 index 0000000..53f1bcb --- /dev/null +++ b/frontend/src/views/admin/LoginPage.vue @@ -0,0 +1,85 @@ + + + diff --git a/frontend/src/views/admin/TeamInvitesPage.vue b/frontend/src/views/admin/TeamInvitesPage.vue new file mode 100644 index 0000000..4c828f9 --- /dev/null +++ b/frontend/src/views/admin/TeamInvitesPage.vue @@ -0,0 +1,290 @@ + + + diff --git a/frontend/src/views/admin/TeamsPage.vue b/frontend/src/views/admin/TeamsPage.vue new file mode 100644 index 0000000..aecf52c --- /dev/null +++ b/frontend/src/views/admin/TeamsPage.vue @@ -0,0 +1,412 @@ + + + diff --git a/frontend/src/views/public/JoinPage.vue b/frontend/src/views/public/JoinPage.vue new file mode 100644 index 0000000..15827b6 --- /dev/null +++ b/frontend/src/views/public/JoinPage.vue @@ -0,0 +1,114 @@ + + + diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json new file mode 100644 index 0000000..4852494 --- /dev/null +++ b/frontend/tsconfig.app.json @@ -0,0 +1,27 @@ +{ + "extends": "@vue/tsconfig/tsconfig.dom.json", + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "types": [ + "vite/client" + ], + "baseUrl": ".", + "paths": { + "@/*": [ + "./src/*" + ] + }, + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx", + "src/**/*.vue" + ] +} \ No newline at end of file diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..20ff94b --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,19 @@ +{ + "files": [], + "references": [ + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.node.json" + } + ], + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": [ + "./src/*" + ] + } + } +} \ No newline at end of file diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json new file mode 100644 index 0000000..8a67f62 --- /dev/null +++ b/frontend/tsconfig.node.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..7318f65 --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,21 @@ +import path from 'node:path' +import { defineConfig } from 'vite' +import tailwindcss from '@tailwindcss/vite' +import vue from '@vitejs/plugin-vue' + +export default defineConfig({ + plugins: [vue(), tailwindcss()], + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + }, + }, + server: { + proxy: { + '/api': { + target: 'http://localhost:8080', + changeOrigin: true, + }, + }, + }, +}) diff --git a/internal/models/system_setting.go b/internal/models/system_setting.go deleted file mode 100644 index 43363ff..0000000 --- a/internal/models/system_setting.go +++ /dev/null @@ -1,42 +0,0 @@ -package models - -import ( - "database/sql" -) - -// ValueType 配置值类型枚举 -type ValueType string - -const ( - ValueTypeString ValueType = "string" - ValueTypeInt ValueType = "int" - ValueTypeFloat ValueType = "float" - ValueTypeBool ValueType = "bool" - ValueTypeJSON ValueType = "json" -) - -// SystemSetting 系统配置表 -type SystemSetting struct { - ID int `json:"id"` - Key string `json:"key"` - Value string `json:"value"` - ValueType ValueType `json:"value_type"` - Description sql.NullString `json:"description"` - UpdatedAt sql.NullTime `json:"updated_at"` -} - -// TableName 返回表名 -func (SystemSetting) TableName() string { - return "system_settings" -} - -// 默认配置键名常量 -const ( - SettingTurnstileEnabled = "turnstile_enabled" - SettingTurnstileSiteKey = "turnstile_site_key" - SettingTurnstileSecretKey = "turnstile_secret_key" - SettingTokenCheckInterval = "token_check_interval" - SettingTokenFailureThreshold = "token_failure_threshold" - SettingInvitationValidityDays = "invitation_validity_days" - SettingSiteTitle = "site_title" -)