From 6ca24b714dd14fee3f6d8b85f8db2da8a5fbab39 Mon Sep 17 00:00:00 2001 From: Miao-moe Date: Sat, 11 Jul 2026 17:30:10 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20HYW=20&=20Koneko=20=E9=9F=B3=E6=BA=90?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E5=90=8E=E5=8F=B0=20v3.0.0=20(=E5=8E=9F?= =?UTF-8?q?=E5=A7=8B=E7=89=88=E6=9C=AC)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 14 + .gitignore | 48 ++ README.md | 269 ++++++++++ next.config.ts | 17 + package.json | 49 ++ prisma/schema.prisma | 325 +++++++++++ src/app/api/auth/login/route.ts | 252 +++++++++ src/app/api/auth/verify/route.ts | 273 ++++++++++ src/app/api/music/card-key/route.ts | 262 +++++++++ src/app/api/music/config/route.ts | 63 +++ src/app/api/music/info/route.ts | 142 +++++ src/app/api/music/ip-rules/route.ts | 271 ++++++++++ src/app/api/music/source/route.ts | 231 ++++++++ src/app/api/music/stats/route.ts | 73 +++ src/app/api/music/url/route.ts | 363 +++++++++++++ src/app/api/route.ts | 48 ++ src/app/card-keys/page.tsx | 552 +++++++++++++++++++ src/app/globals.css | 560 +++++++++++++++++++ src/app/ip-rules/page.tsx | 420 +++++++++++++++ src/app/layout.tsx | 148 +++++ src/app/login/page.tsx | 239 +++++++++ src/app/page.tsx | 23 + src/app/settings/page.tsx | 372 +++++++++++++ src/app/sources/page.tsx | 110 ++++ src/app/theme/page.tsx | 51 ++ src/components/Dashboard.tsx | 614 +++++++++++++++++++++ src/components/Layout.tsx | 445 ++++++++++++++++ src/components/SourceManager.tsx | 665 +++++++++++++++++++++++ src/components/ThemeEditor.tsx | 586 ++++++++++++++++++++ src/components/ThemeProvider.tsx | 292 ++++++++++ src/hooks/useAuth.ts | 430 +++++++++++++++ src/lib/auth/auth-manager.ts | 737 +++++++++++++++++++++++++ src/lib/auth/index.ts | 81 +++ src/lib/auth/permissions.ts | 420 +++++++++++++++ src/lib/db.ts | 52 ++ src/lib/music/card-key-manager.ts | 519 ++++++++++++++++++ src/lib/music/config-manager.ts | 360 +++++++++++++ src/lib/music/executor.ts | 285 ++++++++++ src/lib/music/index.ts | 103 ++++ src/lib/music/ip-rules-manager.ts | 516 ++++++++++++++++++ src/lib/music/lanyin-executor.ts | 485 +++++++++++++++++ src/lib/music/log-manager.ts | 516 ++++++++++++++++++ src/lib/music/lx-executor.ts | 381 +++++++++++++ src/lib/music/qz-executor.ts | 517 ++++++++++++++++++ src/lib/music/rate-limiter.ts | 390 ++++++++++++++ src/lib/music/scheduler.ts | 384 +++++++++++++ src/lib/music/source-manager.ts | 536 +++++++++++++++++++ src/lib/theme/index.ts | 43 ++ src/lib/theme/preset-themes.ts | 307 +++++++++++ src/lib/theme/theme-context.tsx | 305 +++++++++++ src/lib/theme/theme-editor.tsx | 800 ++++++++++++++++++++++++++++ src/lib/theme/theme-provider.tsx | 224 ++++++++ src/types/index.ts | 579 ++++++++++++++++++++ tsconfig.json | 27 + 54 files changed, 16774 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 README.md create mode 100644 next.config.ts create mode 100644 package.json create mode 100644 prisma/schema.prisma create mode 100644 src/app/api/auth/login/route.ts create mode 100644 src/app/api/auth/verify/route.ts create mode 100644 src/app/api/music/card-key/route.ts create mode 100644 src/app/api/music/config/route.ts create mode 100644 src/app/api/music/info/route.ts create mode 100644 src/app/api/music/ip-rules/route.ts create mode 100644 src/app/api/music/source/route.ts create mode 100644 src/app/api/music/stats/route.ts create mode 100644 src/app/api/music/url/route.ts create mode 100644 src/app/api/route.ts create mode 100644 src/app/card-keys/page.tsx create mode 100644 src/app/globals.css create mode 100644 src/app/ip-rules/page.tsx create mode 100644 src/app/layout.tsx create mode 100644 src/app/login/page.tsx create mode 100644 src/app/page.tsx create mode 100644 src/app/settings/page.tsx create mode 100644 src/app/sources/page.tsx create mode 100644 src/app/theme/page.tsx create mode 100644 src/components/Dashboard.tsx create mode 100644 src/components/Layout.tsx create mode 100644 src/components/SourceManager.tsx create mode 100644 src/components/ThemeEditor.tsx create mode 100644 src/components/ThemeProvider.tsx create mode 100644 src/hooks/useAuth.ts create mode 100644 src/lib/auth/auth-manager.ts create mode 100644 src/lib/auth/index.ts create mode 100644 src/lib/auth/permissions.ts create mode 100644 src/lib/db.ts create mode 100644 src/lib/music/card-key-manager.ts create mode 100644 src/lib/music/config-manager.ts create mode 100644 src/lib/music/executor.ts create mode 100644 src/lib/music/index.ts create mode 100644 src/lib/music/ip-rules-manager.ts create mode 100644 src/lib/music/lanyin-executor.ts create mode 100644 src/lib/music/log-manager.ts create mode 100644 src/lib/music/lx-executor.ts create mode 100644 src/lib/music/qz-executor.ts create mode 100644 src/lib/music/rate-limiter.ts create mode 100644 src/lib/music/scheduler.ts create mode 100644 src/lib/music/source-manager.ts create mode 100644 src/lib/theme/index.ts create mode 100644 src/lib/theme/preset-themes.ts create mode 100644 src/lib/theme/theme-context.tsx create mode 100644 src/lib/theme/theme-editor.tsx create mode 100644 src/lib/theme/theme-provider.tsx create mode 100644 src/types/index.ts create mode 100644 tsconfig.json diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..e2fdce1 --- /dev/null +++ b/.env.example @@ -0,0 +1,14 @@ +# 数据库配置 +DATABASE_URL="file:./prisma/data.db" + +# JWT 密钥(用于生成 Token) +JWT_SECRET="your-super-secret-jwt-key-change-in-production" + +# 第三方认证配置(可选) +# AUTH_PROVIDER_URL="https://auth.shiqianjiang.cn" +# AUTH_PROVIDER_CLIENT_ID="" +# AUTH_PROVIDER_CLIENT_SECRET="" + +# 服务配置 +PORT=3000 +NODE_ENV=development diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2805399 --- /dev/null +++ b/.gitignore @@ -0,0 +1,48 @@ +# Dependencies +node_modules/ +.pnp +.pnp.js + +# Build outputs +.next/ +out/ +build/ +dist/ + +# Database +prisma/*.db +prisma/*.db-journal +*.db +*.db-journal + +# Environment files +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Logs +logs/ +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Testing +coverage/ +.nyc_output/ + +# Misc +*.tsbuildinfo +next-env.d.ts diff --git a/README.md b/README.md new file mode 100644 index 0000000..b1ecfc0 --- /dev/null +++ b/README.md @@ -0,0 +1,269 @@ +# HYW & Koneko 音源管理后台 + +
+ +**多平台音源聚合服务** + +支持 LX Music、澜音、QZ Music 格式 + +[![Version](https://img.shields.io/badge/version-3.0.0-blue.svg)](https://github.com) +[![License](https://img.shields.io/badge/license-ISC-green.svg)](LICENSE) +[![Next.js](https://img.shields.io/badge/Next.js-15-black.svg)](https://nextjs.org) +[![Ant Design](https://img.shields.io/badge/Ant%20Design-5-blue.svg)](https://ant.design) + +
+ +--- + +## 📖 项目简介 + +HYW & Koneko 音源管理后台是一个功能强大的多平台音源聚合服务,支持多种音源格式,提供完善的音源管理、卡密系统、频率限制、主题定制等功能。 + +### 🎯 核心特性 + +- **多格式支持**:支持 LX Music、澜音、QZ Music 三种主流音源格式 +- **多平台覆盖**:支持酷我、酷狗、QQ音乐、网易云、咪咕五大音乐平台 +- **多音质选择**:从 128kbps 到 Master 臻品音质,满足不同需求 +- **智能调度**:5种调度策略,自动选择最优音源 +- **主题系统**:8种预设主题 + 自定义主题编辑器 +- **卡密系统**:完善的卡密生成、验证、统计功能 +- **频率限制**:全局 QPS、单 IP 限制、自动封禁 +- **鉴权系统**:支持本地登录和第三方认证(预留) + +--- + +## 👥 开发团队 + +### 合作开发 + +**MistAperio Studio × Macrohard Studio** + +### 编写人 + +- **雾启工作室@云汀** (Cloudwhisper / Miao-moe) +- **Macrohard Studio@Ryn磷熠** (Macrohard0001) + +### 致谢 + +> 旧版后台由 Macrohard Studio 开发,编写人 @Macrohard0001 + +--- + +## 🚀 快速开始 + +### 环境要求 + +- Node.js >= 18.0.0 +- npm >= 9.0.0 或 pnpm >= 8.0.0 + +### 安装依赖 + +```bash +npm install +# 或 +pnpm install +``` + +### 配置环境变量 + +```bash +cp .env.example .env +# 编辑 .env 文件配置你的环境变量 +``` + +### 初始化数据库 + +```bash +npm run db:push +# 或 +npm run db:migrate +``` + +### 启动开发服务器 + +```bash +npm run dev +``` + +访问 http://localhost:3000 查看效果。 + +--- + +## 📁 项目结构 + +``` +hyw-koneko-source-admin/ +├── prisma/ # 数据库模型 +│ └── schema.prisma +├── public/ # 静态资源 +├── src/ +│ ├── app/ # Next.js App Router +│ │ ├── api/ # API 路由 +│ │ ├── login/ # 登录页面 +│ │ ├── sources/ # 音源管理 +│ │ ├── settings/ # 系统设置 +│ │ ├── card-keys/ # 卡密管理 +│ │ ├── ip-rules/ # IP 规则 +│ │ ├── logs/ # 日志查询 +│ │ ├── theme/ # 主题设置 +│ │ └── ... +│ ├── components/ # React 组件 +│ ├── hooks/ # 自定义 Hooks +│ ├── lib/ # 核心库 +│ │ ├── auth/ # 鉴权模块 +│ │ ├── music/ # 音乐业务模块 +│ │ └── theme/ # 主题系统 +│ └── types/ # 类型定义 +├── package.json +├── tsconfig.json +└── next.config.ts +``` + +--- + +## 🎨 主题系统 + +### 预设主题 + +| 主题 | 描述 | +|------|------| +| 默认主题 | Ant Design 经典蓝色主题 | +| 深色模式 | 护眼深色主题 | +| 二次元 | 可爱的粉色调主题 | +| 福瑞 | 温暖的橙棕色调主题 | +| 樱花 | 浪漫的樱花粉色主题 | +| 海洋 | 清新的海洋蓝色主题 | +| 森林 | 自然的森林绿色主题 | +| 午夜 | 深邃的紫色调深色主题 | + +### 自定义主题 + +支持通过主题编辑器自定义: +- 主题色、背景色、文字色、边框色 +- 圆角大小 +- 背景图片(支持模糊、透明度) +- 自定义 CSS + +--- + +## 📡 API 文档 + +### 获取音乐 URL + +```http +GET /api/music/url?platform=kw&songId=123456&quality=320k +``` + +### 批量获取 + +```http +POST /api/music/url +Content-Type: application/json + +{ + "items": [ + { "platform": "kw", "songId": "123", "quality": "320k" }, + { "platform": "tx", "songId": "456", "quality": "flac" } + ] +} +``` + +### 音源管理 + +```http +GET /api/music/source # 获取音源列表 +POST /api/music/source # 上传音源 +PUT /api/music/source # 更新音源 +DELETE /api/music/source # 删除音源 +``` + +--- + +## 🔐 鉴权系统 + +### 支持的认证方式 + +1. **本地登录**:用户名/邮箱 + 密码 +2. **第三方认证**:auth.shiqianjiang.cn(预留) + +### 权限等级 + +| 角色 | 权限 | +|------|------| +| user | 基础音质访问 | +| vip | 全部音质访问 | +| admin | 完全控制权限 | + +--- + +## 📊 支持的平台与音质 + +### 平台 + +| 代码 | 平台 | ID 字段 | +|------|------|---------| +| kw | 酷我音乐 | songId / rid | +| kg | 酷狗音乐 | hash | +| tx | QQ音乐 | songmid / strMediaMid | +| wy | 网易云音乐 | songId | +| mg | 咪咕音乐 | copyrightId | + +### 音质 + +| 代码 | 名称 | +|------|------| +| 128k | 128kbps | +| 320k | 320kbps | +| flac | FLAC 无损 | +| flac24bit | FLAC 24bit | +| hires | Hi-Res | +| dolby | Dolby Atmos | +| atmos_plus | Dolby Atmos Plus | +| master | Master 臻品 | + +--- + +## 📝 开发命令 + +```bash +# 开发 +npm run dev + +# 构建 +npm run build + +# 启动生产服务器 +npm run start + +# 数据库操作 +npm run db:generate # 生成 Prisma Client +npm run db:push # 推送数据库结构 +npm run db:migrate # 创建迁移 +npm run db:studio # 打开 Prisma Studio + +# 代码检查 +npm run lint +``` + +--- + +## 📄 许可证 + +ISC License + +--- + +## 🙏 致谢 + +- [Next.js](https://nextjs.org/) +- [Ant Design](https://ant.design/) +- [Prisma](https://www.prisma.io/) +- [LX Music](https://github.com/lyswhut/lx-music-desktop) + +--- + +
+ +**Made with ❤️ by MistAperio Studio × Macrohard Studio** + +
diff --git a/next.config.ts b/next.config.ts new file mode 100644 index 0000000..4652453 --- /dev/null +++ b/next.config.ts @@ -0,0 +1,17 @@ +import type { NextConfig } from 'next' + +const nextConfig: NextConfig = { + output: 'standalone', + reactStrictMode: true, + experimental: { + serverActions: { + bodySizeLimit: '10mb', + }, + }, + env: { + NEXT_PUBLIC_APP_NAME: 'HYW & Koneko 音源管理后台', + NEXT_PUBLIC_APP_VERSION: '3.0.0', + }, +} + +export default nextConfig diff --git a/package.json b/package.json new file mode 100644 index 0000000..a916fb4 --- /dev/null +++ b/package.json @@ -0,0 +1,49 @@ +{ + "name": "hyw-koneko-source-admin", + "version": "3.0.0", + "description": "HYW & Koneko 音源管理后台 - 多平台音源聚合服务,支持 LX Music、澜音、QZ Music 格式", + "private": true, + "author": "MistAperio Studio × Macrohard Studio", + "contributors": [ + "雾启工作室@云汀 (Cloudwhisper/Miao-moe)", + "Macrohard Studio@Ryn磷熠 (Macrohard0001)" + ], + "scripts": { + "dev": "next dev", + "build": "prisma generate && next build", + "start": "next start", + "lint": "next lint", + "db:generate": "prisma generate", + "db:push": "prisma db push", + "db:migrate": "prisma migrate dev", + "db:studio": "prisma studio" + }, + "dependencies": { + "next": "^15.3.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "antd": "^5.24.0", + "@ant-design/nextjs-registry": "^1.0.0", + "@ant-design/icons": "^5.6.0", + "@prisma/client": "^6.6.0", + "prisma": "^6.6.0", + "better-sqlite3": "^11.9.0", + "dayjs": "^1.11.13", + "uuid": "^11.1.0", + "chalk": "^5.4.1", + "lodash": "^4.17.21", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6" + }, + "devDependencies": { + "@types/node": "^22.14.0", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "@types/better-sqlite3": "^7.6.12", + "@types/uuid": "^10.0.0", + "@types/lodash": "^4.17.16", + "typescript": "^5.8.0", + "eslint": "^9.24.0", + "eslint-config-next": "^15.3.0" + } +} diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..f99c33f --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,325 @@ +// Prisma Schema for HYW & Koneko 音源管理后台 +// +// 合作开发: MistAperio Studio × Macrohard Studio +// 编写人: 雾启工作室@云汀 (Cloudwhisper/Miao-moe), Macrohard Studio@Ryn磷熠 (Macrohard0001) +// +// 旧版后台由 Macrohard Studio 开发,编写人 @Macrohard0001 + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "sqlite" + url = "file:./data.db" +} + +// ============================================ +// 用户与认证相关 +// ============================================ + +/// 用户表 - 用于自建登录系统或对接 auth.shiqianjiang.cn +model User { + id String @id @default(uuid()) + email String @unique + username String? + passwordHash String? // 仅自建登录时使用 + displayName String? + avatar String? + role String @default("user") // user, vip, admin + status String @default("active") // active, banned, inactive + + // 第三方认证信息 + authProvider String? // local, auth.shiqianjiang.cn + authId String? // 第三方平台的用户ID + + // 权限配置 + permissions String? // JSON: 允许的音质、平台等 + + // 时间戳 + lastLoginAt DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // 关联 + apiKeys ApiKey[] + sessions Session[] + logs RequestLog[] + + @@index([email]) + @@index([authProvider, authId]) +} + +/// API 密钥表 +model ApiKey { + id String @id @default(uuid()) + userId String + key String @unique + name String? + permissions String? // JSON: 允许的权限 + rateLimit Int @default(100) // 每分钟限制 + dailyLimit Int @default(10000) // 每日限制 + + // 统计 + usedCount Int @default(0) + lastUsedAt DateTime? + + // 状态 + expiresAt DateTime? + status String @default("active") // active, revoked, expired + createdAt DateTime @default(now()) + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@index([key]) + @@index([userId]) +} + +/// 会话表 +model Session { + id String @id @default(uuid()) + userId String + token String @unique + userAgent String? + ipAddress String? + expiresAt DateTime + createdAt DateTime @default(now()) + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@index([token]) + @@index([userId]) +} + +// ============================================ +// 音源管理相关 +// ============================================ + +/// 音源脚本表 - 支持 LX Music、澜音、QZ Music 格式 +model SourceScript { + id String @id @default(uuid()) + name String + description String? + version String @default("1.0.0") + author String? + + // 脚本类型 + format String @default("lx") // lx, lanyin, qzmusic + + // 脚本内容 + scriptContent String // 完整脚本代码 + scriptHash String // 内容哈希,用于检测变更 + + // 平台与音质 + platforms String // JSON: ["kw", "kg", "tx", "wy", "mg"] + qualities String // JSON: ["128k", "320k", "flac", ...] + + // 状态 + enabled Boolean @default(true) + status String @default("active") // active, error, deprecated + + // 优先级与权重 + priority Int @default(0) + weight Int @default(100) + + // 统计 + successCount Int @default(0) + failCount Int @default(0) + avgResponseTime Int @default(0) + lastError String? + lastUsedAt DateTime? + + // 元数据 + homepage String? + license String? + tags String? // JSON: ["独家", "高音质", ...] + + // 时间戳 + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([format]) + @@index([enabled]) + @@index([scriptHash]) +} + +/// 音源文件表 - 存储大型脚本文件 +model SourceFile { + id String @id @default(uuid()) + sourceId String @unique + filePath String + fileSize Int + fileHash String + createdAt DateTime @default(now()) +} + +// ============================================ +// 卡密系统 +// ============================================ + +/// 卡密表 +model CardKey { + id String @id @default(uuid()) + key String @unique + type String @default("normal") // normal, vip, admin + + // 状态 + status String @default("active") // active, used, expired, disabled + + // 使用限制 + maxUses Int @default(-1) // -1 表示无限制 + usedCount Int @default(0) + dailyLimit Int @default(100) + rateLimit Int @default(10) // 每分钟 + + // 权限配置 + allowedQualities String? // JSON: 允许的音质列表 + allowedPlatforms String? // JSON: 允许的平台列表 + + // 有效期 + expireAt DateTime? + + // 绑定信息 + bindIP String? + bindDevice String? + + // 备注 + remark String? + + // 关联脚本 + scriptId String? + scriptName String? + + // 时间戳 + lastUsedAt DateTime? + createdAt DateTime @default(now()) + + // 关联 + logs CardKeyLog[] + + @@index([key]) + @@index([type]) + @@index([status]) +} + +/// 卡密使用日志 +model CardKeyLog { + id String @id @default(uuid()) + cardKeyId String + key String + ip String? + action String // verify, use + success Boolean + error String? + userAgent String? + createdAt DateTime @default(now()) + + cardKey CardKey @relation(fields: [cardKeyId], references: [id], onDelete: Cascade) + + @@index([cardKeyId]) + @@index([key]) +} + +// ============================================ +// 系统配置与日志 +// ============================================ + +/// 系统配置表 +model SystemConfig { + id String @id @default("system") + + // 服务状态 + serviceStatus String @default("running") // running, stopped, maintenance + maintenanceMessage String? + + // 频率限制配置 + globalQPS Int @default(100) + singleIPQPS Int @default(10) + singleIPPerMinute Int @default(60) + banThreshold Int @default(100) + banDuration Int @default(3600) // 秒 + + // 执行模式 + executionMode String @default("hybrid") // simplified, full, hybrid + + // 主题配置 + defaultTheme String @default("default") + + // 其他配置 + extraConfig String? // JSON: 其他扩展配置 + + updatedAt DateTime @updatedAt +} + +/// IP 规则表 +model IPRule { + id String @id @default(uuid()) + ip String // IP 或 CIDR + type String // whitelist, blacklist + reason String? + expiresAt DateTime? + createdAt DateTime @default(now()) + + @@index([ip]) + @@index([type]) +} + +/// 请求日志表 +model RequestLog { + id String @id @default(uuid()) + userId String? + + // 请求信息 + platform String? + songId String? + quality String? + source String? // 使用的音源 + + // 响应信息 + success Boolean + responseTime Int // 毫秒 + errorMessage String? + + // 客户端信息 + ip String? + userAgent String? + cardKey String? + + // 时间戳 + createdAt DateTime @default(now()) + + user User? @relation(fields: [userId], references: [id]) + + @@index([createdAt]) + @@index([platform]) + @@index([success]) +} + +/// 主题配置表 +model Theme { + id String @id @default(uuid()) + name String @unique + displayName String + description String? + + // 主题类型 + type String @default("custom") // preset, custom + + // 主题配置 + config String // JSON: 完整主题配置 + + // 元数据 + author String? + preview String? // 预览图URL + tags String? // JSON + + // 状态 + isDefault Boolean @default(false) + enabled Boolean @default(true) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([type]) + @@index([enabled]) +} diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts new file mode 100644 index 0000000..ba34df2 --- /dev/null +++ b/src/app/api/auth/login/route.ts @@ -0,0 +1,252 @@ +/** + * HYW & Koneko 音源管理后台 + * + * 合作开发: MistAperio Studio × Macrohard Studio + * 编写人: 雾启工作室@云汀 (Cloudwhisper/Miao-moe), Macrohard Studio@Ryn磷熠 (Macrohard0001) + * + * 旧版后台由 Macrohard Studio 开发,编写人 @Macrohard0001 + */ + +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/lib/db' +import { v4 as uuidv4 } from 'uuid' +import crypto from 'crypto' + +/** + * 登录请求接口 + */ +interface LoginRequest { + username?: string + email?: string + password: string + authProvider?: 'local' | 'auth.shiqianjiang.cn' + token?: string // 用于第三方认证 +} + +/** + * 登录响应接口 + */ +interface LoginResponse { + success: boolean + user?: { + id: string + email: string + username?: string + displayName?: string + avatar?: string + role: string + } + token?: string + expiresAt?: string + error?: string +} + +/** + * 生成 JWT Token(简化版) + */ +function generateToken(userId: string, role: string): string { + const header = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).toString('base64url') + const payload = Buffer.from(JSON.stringify({ + sub: userId, + role, + iat: Math.floor(Date.now() / 1000), + exp: Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60, // 7 天过期 + })).toString('base64url') + + const secret = process.env.JWT_SECRET || 'hyw-koneko-secret-key' + const signature = crypto + .createHmac('sha256', secret) + .update(`${header}.${payload}`) + .digest('base64url') + + return `${header}.${payload}.${signature}` +} + +/** + * 验证密码(简化版) + */ +function verifyPassword(password: string, hashedPassword: string): boolean { + // 简化实现:实际项目应使用 bcrypt + const hash = crypto.createHash('sha256').update(password).digest('hex') + return hash === hashedPassword +} + +/** + * 哈希密码 + */ +function hashPassword(password: string): string { + return crypto.createHash('sha256').update(password).digest('hex') +} + +/** + * POST /api/auth/login + * 用户登录 + * 支持本地登录和 auth.shiqianjiang.cn 认证 + */ +export async function POST(request: NextRequest): Promise> { + try { + const body: LoginRequest = await request.json() + const { username, email, password, authProvider, token } = body + + // 第三方认证(auth.shiqianjiang.cn) + if (authProvider === 'auth.shiqianjiang.cn') { + if (!token) { + return NextResponse.json({ + success: false, + error: 'Missing token for third-party authentication', + }, { status: 400 }) + } + + try { + // 调用第三方认证服务验证 token + const authResponse = await fetch('https://auth.shiqianjiang.cn/api/verify', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}`, + }, + }) + + if (!authResponse.ok) { + return NextResponse.json({ + success: false, + error: 'Third-party authentication failed', + }, { status: 401 }) + } + + const authData = await authResponse.json() + + // 查找或创建用户 + let user = await prisma.user.findUnique({ + where: { email: authData.email }, + }) + + if (!user) { + // 创建新用户 + user = await prisma.user.create({ + data: { + id: uuidv4(), + email: authData.email, + username: authData.username || authData.email.split('@')[0], + displayName: authData.displayName || authData.username, + avatar: authData.avatar, + role: authData.role || 'user', + authProvider: 'auth.shiqianjiang.cn', + password: hashPassword(uuidv4()), // 随机密码 + lastLoginAt: new Date(), + }, + }) + } else { + // 更新最后登录时间 + await prisma.user.update({ + where: { id: user.id }, + data: { lastLoginAt: new Date() }, + }) + } + + // 生成 token + const jwtToken = generateToken(user.id, user.role) + + return NextResponse.json({ + success: true, + user: { + id: user.id, + email: user.email, + username: user.username ?? undefined, + displayName: user.displayName ?? undefined, + avatar: user.avatar ?? undefined, + role: user.role, + }, + token: jwtToken, + expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(), + }) + + } catch (error) { + console.error('Third-party auth error:', error) + return NextResponse.json({ + success: false, + error: 'Third-party authentication service unavailable', + }, { status: 503 }) + } + } + + // 本地登录 + if (!password) { + return NextResponse.json({ + success: false, + error: 'Missing required field: password', + }, { status: 400 }) + } + + if (!username && !email) { + return NextResponse.json({ + success: false, + error: 'Missing required field: username or email', + }, { status: 400 }) + } + + // 查找用户 + const user = await prisma.user.findFirst({ + where: { + OR: [ + { username: username || '' }, + { email: email || '' }, + ], + }, + }) + + if (!user) { + return NextResponse.json({ + success: false, + error: 'User not found', + }, { status: 401 }) + } + + // 检查用户状态 + if (user.status === 'banned') { + return NextResponse.json({ + success: false, + error: 'User is banned', + }, { status: 403 }) + } + + // 验证密码 + if (!verifyPassword(password, user.password)) { + return NextResponse.json({ + success: false, + error: 'Invalid password', + }, { status: 401 }) + } + + // 更新最后登录时间 + await prisma.user.update({ + where: { id: user.id }, + data: { lastLoginAt: new Date() }, + }) + + // 生成 token + const jwtToken = generateToken(user.id, user.role) + + return NextResponse.json({ + success: true, + user: { + id: user.id, + email: user.email, + username: user.username ?? undefined, + displayName: user.displayName ?? undefined, + avatar: user.avatar ?? undefined, + role: user.role, + }, + token: jwtToken, + expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(), + }) + + } catch (error) { + console.error('Login error:', error) + + return NextResponse.json({ + success: false, + error: error instanceof Error ? error.message : 'Internal server error', + }, { status: 500 }) + } +} diff --git a/src/app/api/auth/verify/route.ts b/src/app/api/auth/verify/route.ts new file mode 100644 index 0000000..191798c --- /dev/null +++ b/src/app/api/auth/verify/route.ts @@ -0,0 +1,273 @@ +/** + * HYW & Koneko 音源管理后台 + * + * 合作开发: MistAperio Studio × Macrohard Studio + * 编写人: 雾启工作室@云汀 (Cloudwhisper/Miao-moe), Macrohard Studio@Ryn磷熠 (Macrohard0001) + * + * 旧版后台由 Macrohard Studio 开发,编写人 @Macrohard0001 + */ + +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/lib/db' +import crypto from 'crypto' + +/** + * 验证响应接口 + */ +interface VerifyResponse { + success: boolean + valid: boolean + user?: { + id: string + email: string + username?: string + displayName?: string + avatar?: string + role: string + permissions?: string + } + error?: string +} + +/** + * 验证 JWT Token(简化版) + */ +function verifyToken(token: string): { valid: boolean; payload?: { sub: string; role: string; exp: number } } { + try { + const parts = token.split('.') + if (parts.length !== 3) { + return { valid: false } + } + + const [header, payload, signature] = parts + + // 验证签名 + const secret = process.env.JWT_SECRET || 'hyw-koneko-secret-key' + const expectedSignature = crypto + .createHmac('sha256', secret) + .update(`${header}.${payload}`) + .digest('base64url') + + if (signature !== expectedSignature) { + return { valid: false } + } + + // 解析 payload + const decodedPayload = JSON.parse(Buffer.from(payload, 'base64url').toString()) + + // 检查过期时间 + if (decodedPayload.exp && decodedPayload.exp < Math.floor(Date.now() / 1000)) { + return { valid: false } + } + + return { valid: true, payload: decodedPayload } + } catch { + return { valid: false } + } +} + +/** + * 从请求中提取 Token + */ +function extractToken(request: NextRequest): string | null { + // 从 Authorization header 获取 + const authHeader = request.headers.get('authorization') + if (authHeader && authHeader.startsWith('Bearer ')) { + return authHeader.substring(7) + } + + // 从 Cookie 获取 + const cookieToken = request.cookies.get('token')?.value + if (cookieToken) { + return cookieToken + } + + // 从 URL 参数获取 + const url = new URL(request.url) + const urlToken = url.searchParams.get('token') + if (urlToken) { + return urlToken + } + + return null +} + +/** + * GET /api/auth/verify + * 验证 Token + */ +export async function GET(request: NextRequest): Promise> { + try { + const token = extractToken(request) + + if (!token) { + return NextResponse.json({ + success: true, + valid: false, + error: 'No token provided', + }, { status: 401 }) + } + + // 验证 token + const verification = verifyToken(token) + + if (!verification.valid || !verification.payload) { + return NextResponse.json({ + success: true, + valid: false, + error: 'Invalid or expired token', + }, { status: 401 }) + } + + // 获取用户信息 + const user = await prisma.user.findUnique({ + where: { id: verification.payload.sub }, + select: { + id: true, + email: true, + username: true, + displayName: true, + avatar: true, + role: true, + status: true, + permissions: true, + }, + }) + + if (!user) { + return NextResponse.json({ + success: true, + valid: false, + error: 'User not found', + }, { status: 401 }) + } + + // 检查用户状态 + if (user.status === 'banned') { + return NextResponse.json({ + success: true, + valid: false, + error: 'User is banned', + }, { status: 403 }) + } + + return NextResponse.json({ + success: true, + valid: true, + user: { + id: user.id, + email: user.email, + username: user.username ?? undefined, + displayName: user.displayName ?? undefined, + avatar: user.avatar ?? undefined, + role: user.role, + permissions: user.permissions ?? undefined, + }, + }) + + } catch (error) { + console.error('Verify token error:', error) + + return NextResponse.json({ + success: false, + valid: false, + error: error instanceof Error ? error.message : 'Internal server error', + }, { status: 500 }) + } +} + +/** + * POST /api/auth/verify + * 刷新 Token + */ +export async function POST(request: NextRequest): Promise> { + try { + const token = extractToken(request) + + if (!token) { + return NextResponse.json({ + success: false, + valid: false, + error: 'No token provided', + }, { status: 401 }) + } + + // 验证 token(允许过期的 token 进行刷新) + const parts = token.split('.') + if (parts.length !== 3) { + return NextResponse.json({ + success: false, + valid: false, + error: 'Invalid token format', + }, { status: 401 }) + } + + const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString()) + + // 获取用户信息 + const user = await prisma.user.findUnique({ + where: { id: payload.sub }, + }) + + if (!user) { + return NextResponse.json({ + success: false, + valid: false, + error: 'User not found', + }, { status: 401 }) + } + + // 检查用户状态 + if (user.status === 'banned') { + return NextResponse.json({ + success: false, + valid: false, + error: 'User is banned', + }, { status: 403 }) + } + + // 生成新 token + const header = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).toString('base64url') + const newPayload = Buffer.from(JSON.stringify({ + sub: user.id, + role: user.role, + iat: Math.floor(Date.now() / 1000), + exp: Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60, + })).toString('base64url') + + const secret = process.env.JWT_SECRET || 'hyw-koneko-secret-key' + const signature = crypto + .createHmac('sha256', secret) + .update(`${header}.${newPayload}`) + .digest('base64url') + + const newToken = `${header}.${newPayload}.${signature}` + + return NextResponse.json({ + success: true, + valid: true, + user: { + id: user.id, + email: user.email, + username: user.username ?? undefined, + displayName: user.displayName ?? undefined, + avatar: user.avatar ?? undefined, + role: user.role, + permissions: user.permissions ?? undefined, + }, + }, { + headers: { + 'X-New-Token': newToken, + }, + }) + + } catch (error) { + console.error('Refresh token error:', error) + + return NextResponse.json({ + success: false, + valid: false, + error: error instanceof Error ? error.message : 'Internal server error', + }, { status: 500 }) + } +} diff --git a/src/app/api/music/card-key/route.ts b/src/app/api/music/card-key/route.ts new file mode 100644 index 0000000..26a1d1c --- /dev/null +++ b/src/app/api/music/card-key/route.ts @@ -0,0 +1,262 @@ +/** + * HYW & Koneko 音源管理后台 + * + * 合作开发: MistAperio Studio × Macrohard Studio + * 编写人: 雾启工作室@云汀 (Cloudwhisper/Miao-moe), Macrohard Studio@Ryn磷熠 (Macrohard0001) + * + * 旧版后台由 Macrohard Studio 开发,编写人 @Macrohard0001 + */ + +import { NextRequest, NextResponse } from 'next/server' +import { getCardKeyManager } from '@/lib/music' +import type { CreateCardKeyRequest } from '@/lib/music' +import type { CardKeyType, CardKeyStatus, MusicPlatform, MusicQuality } from '@/types' + +/** + * GET /api/music/card-key + * 获取卡密列表 + */ +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url) + + const options = { + type: searchParams.get('type') as CardKeyType | undefined, + status: searchParams.get('status') as CardKeyStatus | undefined, + page: searchParams.get('page') ? parseInt(searchParams.get('page')!) : 1, + pageSize: searchParams.get('pageSize') ? parseInt(searchParams.get('pageSize')!) : 20, + } + + const cardKeyManager = getCardKeyManager() + const result = await cardKeyManager.list(options) + + return NextResponse.json({ + success: true, + data: result.data, + pagination: { + total: result.total, + page: result.page, + pageSize: result.pageSize, + totalPages: Math.ceil(result.total / result.pageSize), + }, + }) + + } catch (error) { + console.error('Get card keys error:', error) + + return NextResponse.json({ + success: false, + error: error instanceof Error ? error.message : 'Internal server error', + }, { status: 500 }) + } +} + +/** + * POST /api/music/card-key + * 创建卡密 + */ +export async function POST(request: NextRequest) { + try { + const body = await request.json() + + // 支持单个创建和批量创建 + const isBatch = body.cardKeys && Array.isArray(body.cardKeys) + const count = body.count || 1 + + // 验证卡密类型 + const validTypes: CardKeyType[] = ['normal', 'vip', 'admin'] + const type = (isBatch ? body.cardKeys[0]?.type : body.type) as CardKeyType + + if (!type || !validTypes.includes(type)) { + return NextResponse.json({ + success: false, + error: `Invalid or missing type. Valid types: ${validTypes.join(', ')}`, + }, { status: 400 }) + } + + const cardKeyManager = getCardKeyManager() + + if (isBatch) { + // 批量创建(使用不同的配置) + const results = [] + for (const cardKeyData of body.cardKeys) { + const createRequest: CreateCardKeyRequest = { + type: cardKeyData.type, + maxUses: cardKeyData.maxUses, + dailyLimit: cardKeyData.dailyLimit, + rateLimit: cardKeyData.rateLimit, + allowedQualities: cardKeyData.allowedQualities as MusicQuality[], + allowedPlatforms: cardKeyData.allowedPlatforms as MusicPlatform[], + validDays: cardKeyData.validDays, + bindIP: cardKeyData.bindIP, + bindDevice: cardKeyData.bindDevice, + remark: cardKeyData.remark, + scriptId: cardKeyData.scriptId, + scriptName: cardKeyData.scriptName, + customKey: cardKeyData.customKey, + } + const cardKey = await cardKeyManager.create(createRequest) + results.push(cardKey) + } + + return NextResponse.json({ + success: true, + data: results, + message: `Created ${results.length} card key(s)`, + }, { status: 201 }) + } + + // 单个或批量创建(相同配置) + const createRequest: CreateCardKeyRequest = { + type: body.type, + maxUses: body.maxUses, + dailyLimit: body.dailyLimit, + rateLimit: body.rateLimit, + allowedQualities: body.allowedQualities as MusicQuality[], + allowedPlatforms: body.allowedPlatforms as MusicPlatform[], + validDays: body.validDays, + bindIP: body.bindIP, + bindDevice: body.bindDevice, + remark: body.remark, + scriptId: body.scriptId, + scriptName: body.scriptName, + customKey: body.customKey, + } + + if (count > 1) { + // 批量创建 + const results = await cardKeyManager.createBatch(count, createRequest) + + return NextResponse.json({ + success: true, + data: results, + message: `Created ${results.length} card key(s)`, + }, { status: 201 }) + } + + // 单个创建 + const cardKey = await cardKeyManager.create(createRequest) + + return NextResponse.json({ + success: true, + data: cardKey, + message: 'Card key created successfully', + }, { status: 201 }) + + } catch (error) { + console.error('Create card key error:', error) + + return NextResponse.json({ + success: false, + error: error instanceof Error ? error.message : 'Internal server error', + }, { status: 500 }) + } +} + +/** + * DELETE /api/music/card-key + * 删除卡密 + */ +export async function DELETE(request: NextRequest) { + try { + const { searchParams } = new URL(request.url) + const id = searchParams.get('id') + const ids = searchParams.get('ids') + + if (!id && !ids) { + return NextResponse.json({ + success: false, + error: 'Missing required parameter: id or ids', + }, { status: 400 }) + } + + const cardKeyManager = getCardKeyManager() + + if (ids) { + // 批量删除 + const idList = ids.split(',').filter(Boolean) + const count = await cardKeyManager.deleteMany(idList) + + return NextResponse.json({ + success: true, + data: { deletedCount: count }, + message: `Deleted ${count} card key(s)`, + }) + } else { + // 单个删除 + const success = await cardKeyManager.delete(id!) + + if (!success) { + return NextResponse.json({ + success: false, + error: 'Card key not found or delete failed', + }, { status: 404 }) + } + + return NextResponse.json({ + success: true, + message: 'Card key deleted successfully', + }) + } + + } catch (error) { + console.error('Delete card key error:', error) + + return NextResponse.json({ + success: false, + error: error instanceof Error ? error.message : 'Internal server error', + }, { status: 500 }) + } +} + +/** + * PATCH /api/music/card-key + * 更新卡密 + */ +export async function PATCH(request: NextRequest) { + try { + const body = await request.json() + + if (!body.id) { + return NextResponse.json({ + success: false, + error: 'Missing required field: id', + }, { status: 400 }) + } + + const cardKeyManager = getCardKeyManager() + const updated = await cardKeyManager.update(body.id, { + status: body.status as CardKeyStatus, + maxUses: body.maxUses, + dailyLimit: body.dailyLimit, + rateLimit: body.rateLimit, + allowedQualities: body.allowedQualities as MusicQuality[], + allowedPlatforms: body.allowedPlatforms as MusicPlatform[], + expireAt: body.expireAt ? new Date(body.expireAt) : undefined, + bindIP: body.bindIP, + bindDevice: body.bindDevice, + remark: body.remark, + }) + + if (!updated) { + return NextResponse.json({ + success: false, + error: 'Card key not found', + }, { status: 404 }) + } + + return NextResponse.json({ + success: true, + data: updated, + message: 'Card key updated successfully', + }) + + } catch (error) { + console.error('Update card key error:', error) + + return NextResponse.json({ + success: false, + error: error instanceof Error ? error.message : 'Internal server error', + }, { status: 500 }) + } +} diff --git a/src/app/api/music/config/route.ts b/src/app/api/music/config/route.ts new file mode 100644 index 0000000..af47fc2 --- /dev/null +++ b/src/app/api/music/config/route.ts @@ -0,0 +1,63 @@ +/** + * HYW & Koneko 音源管理后台 + * + * 合作开发: MistAperio Studio × Macrohard Studio + * 编写人: 雾启工作室@云汀 (Cloudwhisper/Miao-moe), Macrohard Studio@Ryn磷熠 (Macrohard0001) + * + * 旧版后台由 Macrohard Studio 开发,编写人 @Macrohard0001 + */ + +import { NextRequest, NextResponse } from 'next/server' +import { getConfigManager } from '@/lib/music' +import type { UpdateSystemConfigRequest } from '@/lib/music' + +/** + * GET /api/music/config + * 获取系统配置 + */ +export async function GET() { + try { + const configManager = getConfigManager() + const config = await configManager.getSystemConfig() + + return NextResponse.json({ + success: true, + data: config, + }) + + } catch (error) { + console.error('Get config error:', error) + + return NextResponse.json({ + success: false, + error: error instanceof Error ? error.message : 'Internal server error', + }, { status: 500 }) + } +} + +/** + * POST /api/music/config + * 更新系统配置 + */ +export async function POST(request: NextRequest) { + try { + const body = await request.json() as UpdateSystemConfigRequest + + const configManager = getConfigManager() + const updatedConfig = await configManager.updateSystemConfig(body) + + return NextResponse.json({ + success: true, + data: updatedConfig, + message: 'Configuration updated successfully', + }) + + } catch (error) { + console.error('Update config error:', error) + + return NextResponse.json({ + success: false, + error: error instanceof Error ? error.message : 'Internal server error', + }, { status: 500 }) + } +} diff --git a/src/app/api/music/info/route.ts b/src/app/api/music/info/route.ts new file mode 100644 index 0000000..942b92d --- /dev/null +++ b/src/app/api/music/info/route.ts @@ -0,0 +1,142 @@ +/** + * HYW & Koneko 音源管理后台 + * + * 合作开发: MistAperio Studio × Macrohard Studio + * 编写人: 雾启工作室@云汀 (Cloudwhisper/Miao-moe), Macrohard Studio@Ryn磷熠 (Macrohard0001) + * + * 旧版后台由 Macrohard Studio 开发,编写人 @Macrohard0001 + */ + +import { NextRequest, NextResponse } from 'next/server' +import { getScheduler, getConfigManager } from '@/lib/music' +import type { MusicPlatform } from '@/types' +import { v4 as uuidv4 } from 'uuid' + +/** + * GET /api/music/info + * 获取音乐信息(歌词、封面等) + */ +export async function GET(request: NextRequest) { + const startTime = Date.now() + const requestId = uuidv4() + + try { + const { searchParams } = new URL(request.url) + const platform = searchParams.get('platform') as MusicPlatform | null + const songId = searchParams.get('songId') + const action = searchParams.get('action') as 'lyric' | 'pic' | null + + // 验证参数 + if (!platform) { + return NextResponse.json({ + success: false, + error: 'Missing required parameter: platform', + requestId, + }, { status: 400 }) + } + + const validPlatforms = ['kw', 'kg', 'tx', 'wy', 'mg'] + if (!validPlatforms.includes(platform)) { + return NextResponse.json({ + success: false, + error: `Invalid platform: ${platform}`, + requestId, + }, { status: 400 }) + } + + if (!songId) { + return NextResponse.json({ + success: false, + error: 'Missing required parameter: songId', + requestId, + }, { status: 400 }) + } + + if (!action) { + return NextResponse.json({ + success: false, + error: 'Missing required parameter: action (lyric/pic)', + requestId, + }, { status: 400 }) + } + + if (!['lyric', 'pic'].includes(action)) { + return NextResponse.json({ + success: false, + error: `Invalid action: ${action}. Valid actions: lyric, pic`, + requestId, + }, { status: 400 }) + } + + // 检查服务状态 + const configManager = getConfigManager() + const serviceStatus = await configManager.getServiceStatus() + + if (serviceStatus.status !== 'running') { + return NextResponse.json({ + success: false, + error: serviceStatus.message || `Service is ${serviceStatus.status}`, + requestId, + }, { status: 503 }) + } + + // 获取音乐信息 + // 注意:这里使用调度器获取音乐 URL,然后从响应中提取信息 + // 实际项目中可能需要单独的信息获取接口 + const scheduler = getScheduler() + + // 使用最低音质获取信息(减少资源消耗) + const response = await scheduler.executeWithRetry({ + requestId, + platform, + songId, + quality: '128k', + extra: { action }, + }) + + const responseTime = Date.now() - startTime + + if (!response.success) { + return NextResponse.json({ + success: false, + error: response.error || 'Failed to get music info', + requestId, + responseTime, + }, { status: 500 }) + } + + // 根据操作类型返回不同数据 + const result: Record = { + songId, + platform, + } + + if (action === 'lyric') { + // 返回歌词信息 + result.lyric = response.info?.name ? `[00:00.00]${response.info.name}` : null + result.info = response.info + } else if (action === 'pic') { + // 返回封面信息 + result.pic = response.info?.pic || null + result.info = response.info + } + + return NextResponse.json({ + success: true, + data: result, + requestId, + responseTime, + }) + + } catch (error) { + const responseTime = Date.now() - startTime + console.error('Get music info error:', error) + + return NextResponse.json({ + success: false, + error: error instanceof Error ? error.message : 'Internal server error', + requestId, + responseTime, + }, { status: 500 }) + } +} diff --git a/src/app/api/music/ip-rules/route.ts b/src/app/api/music/ip-rules/route.ts new file mode 100644 index 0000000..80bfec3 --- /dev/null +++ b/src/app/api/music/ip-rules/route.ts @@ -0,0 +1,271 @@ +/** + * HYW & Koneko 音源管理后台 + * + * 合作开发: MistAperio Studio × Macrohard Studio + * 编写人: 雾启工作室@云汀 (Cloudwhisper/Miao-moe), Macrohard Studio@Ryn磷熠 (Macrohard0001) + * + * 旧版后台由 Macrohard Studio 开发,编写人 @Macrohard0001 + */ + +import { NextRequest, NextResponse } from 'next/server' +import { getIPRulesManager } from '@/lib/music' +import type { CreateIPRuleRequest, IPRuleQueryOptions } from '@/lib/music' +import type { IPRuleType } from '@/types' + +/** + * GET /api/music/ip-rules + * 获取 IP 规则列表 + */ +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url) + + const options: IPRuleQueryOptions = { + type: searchParams.get('type') as IPRuleType | undefined, + ip: searchParams.get('ip') ?? undefined, + page: searchParams.get('page') ? parseInt(searchParams.get('page')!) : 1, + pageSize: searchParams.get('pageSize') ? parseInt(searchParams.get('pageSize')!) : 20, + } + + const ipRulesManager = getIPRulesManager() + const result = await ipRulesManager.list(options) + + // 获取统计信息 + const stats = await ipRulesManager.getStats() + + return NextResponse.json({ + success: true, + data: result.data, + pagination: { + total: result.total, + page: result.page, + pageSize: result.pageSize, + totalPages: Math.ceil(result.total / result.pageSize), + }, + stats, + }) + + } catch (error) { + console.error('Get IP rules error:', error) + + return NextResponse.json({ + success: false, + error: error instanceof Error ? error.message : 'Internal server error', + }, { status: 500 }) + } +} + +/** + * POST /api/music/ip-rules + * 添加 IP 规则 + */ +export async function POST(request: NextRequest) { + try { + const body = await request.json() + + // 支持单个添加和批量添加 + const isBatch = body.rules && Array.isArray(body.rules) + + // 验证规则类型 + const validTypes: IPRuleType[] = ['whitelist', 'blacklist'] + + if (isBatch) { + // 批量添加 + const rules: CreateIPRuleRequest[] = body.rules.map((rule: { ip: string; type: string; reason?: string; expiresAt?: string }) => ({ + ip: rule.ip, + type: rule.type as IPRuleType, + reason: rule.reason, + expiresAt: rule.expiresAt ? new Date(rule.expiresAt) : undefined, + })) + + // 验证所有规则 + for (const rule of rules) { + if (!rule.ip) { + return NextResponse.json({ + success: false, + error: 'Missing required field: ip', + }, { status: 400 }) + } + if (!rule.type || !validTypes.includes(rule.type)) { + return NextResponse.json({ + success: false, + error: `Invalid type for IP ${rule.ip}. Valid types: ${validTypes.join(', ')}`, + }, { status: 400 }) + } + } + + const ipRulesManager = getIPRulesManager() + const results = await ipRulesManager.createBatch(rules) + + return NextResponse.json({ + success: true, + data: results, + message: `Added ${results.length} IP rule(s)`, + }, { status: 201 }) + } + + // 单个添加 + if (!body.ip) { + return NextResponse.json({ + success: false, + error: 'Missing required field: ip', + }, { status: 400 }) + } + + if (!body.type || !validTypes.includes(body.type)) { + return NextResponse.json({ + success: false, + error: `Invalid or missing type. Valid types: ${validTypes.join(', ')}`, + }, { status: 400 }) + } + + const createRequest: CreateIPRuleRequest = { + ip: body.ip, + type: body.type as IPRuleType, + reason: body.reason, + expiresAt: body.expiresAt ? new Date(body.expiresAt) : undefined, + } + + const ipRulesManager = getIPRulesManager() + const rule = await ipRulesManager.create(createRequest) + + return NextResponse.json({ + success: true, + data: rule, + message: 'IP rule added successfully', + }, { status: 201 }) + + } catch (error) { + console.error('Add IP rule error:', error) + + return NextResponse.json({ + success: false, + error: error instanceof Error ? error.message : 'Internal server error', + }, { status: 500 }) + } +} + +/** + * DELETE /api/music/ip-rules + * 删除 IP 规则 + */ +export async function DELETE(request: NextRequest) { + try { + const { searchParams } = new URL(request.url) + const id = searchParams.get('id') + const ids = searchParams.get('ids') + const ip = searchParams.get('ip') + const type = searchParams.get('type') as IPRuleType | null + const cleanup = searchParams.get('cleanup') + + const ipRulesManager = getIPRulesManager() + + // 清理过期规则 + if (cleanup === 'true') { + const count = await ipRulesManager.cleanupExpired() + + return NextResponse.json({ + success: true, + data: { deletedCount: count }, + message: `Cleaned up ${count} expired rule(s)`, + }) + } + + // 通过 IP 和类型删除 + if (ip && type) { + if (type === 'whitelist') { + const success = await ipRulesManager.removeFromWhitelist(ip) + return NextResponse.json({ + success, + message: success ? 'Removed from whitelist' : 'IP not found in whitelist', + }) + } else if (type === 'blacklist') { + const success = await ipRulesManager.removeFromBlacklist(ip) + return NextResponse.json({ + success, + message: success ? 'Removed from blacklist' : 'IP not found in blacklist', + }) + } + } + + // 批量删除 + if (ids) { + const idList = ids.split(',').filter(Boolean) + const count = await ipRulesManager.deleteMany(idList) + + return NextResponse.json({ + success: true, + data: { deletedCount: count }, + message: `Deleted ${count} IP rule(s)`, + }) + } + + // 单个删除 + if (id) { + const success = await ipRulesManager.delete(id) + + if (!success) { + return NextResponse.json({ + success: false, + error: 'IP rule not found or delete failed', + }, { status: 404 }) + } + + return NextResponse.json({ + success: true, + message: 'IP rule deleted successfully', + }) + } + + return NextResponse.json({ + success: false, + error: 'Missing required parameter: id, ids, ip+type, or cleanup=true', + }, { status: 400 }) + + } catch (error) { + console.error('Delete IP rule error:', error) + + return NextResponse.json({ + success: false, + error: error instanceof Error ? error.message : 'Internal server error', + }, { status: 500 }) + } +} + +/** + * POST /api/music/ip-rules/check + * 检查 IP 是否允许访问 + */ +export async function PUT(request: NextRequest) { + try { + const body = await request.json() + + if (!body.ip) { + return NextResponse.json({ + success: false, + error: 'Missing required field: ip', + }, { status: 400 }) + } + + const ipRulesManager = getIPRulesManager() + const result = await ipRulesManager.checkIP(body.ip) + + return NextResponse.json({ + success: true, + data: { + ip: body.ip, + allowed: result.allowed, + rule: result.rule, + reason: result.reason, + }, + }) + + } catch (error) { + console.error('Check IP error:', error) + + return NextResponse.json({ + success: false, + error: error instanceof Error ? error.message : 'Internal server error', + }, { status: 500 }) + } +} diff --git a/src/app/api/music/source/route.ts b/src/app/api/music/source/route.ts new file mode 100644 index 0000000..5ceb170 --- /dev/null +++ b/src/app/api/music/source/route.ts @@ -0,0 +1,231 @@ +/** + * HYW & Koneko 音源管理后台 + * + * 合作开发: MistAperio Studio × Macrohard Studio + * 编写人: 雾启工作室@云汀 (Cloudwhisper/Miao-moe), Macrohard Studio@Ryn磷熠 (Macrohard0001) + * + * 旧版后台由 Macrohard Studio 开发,编写人 @Macrohard0001 + */ + +import { NextRequest, NextResponse } from 'next/server' +import { getSourceManager } from '@/lib/music' +import type { CreateSourceRequest, UpdateSourceRequest, SourceQueryOptions } from '@/lib/music' +import type { MusicPlatform, MusicQuality, ScriptFormat } from '@/types' + +/** + * GET /api/music/source + * 获取音源列表 + */ +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url) + + const options: SourceQueryOptions = { + format: searchParams.get('format') as ScriptFormat | undefined, + platform: searchParams.get('platform') as MusicPlatform | undefined, + enabled: searchParams.get('enabled') ? searchParams.get('enabled') === 'true' : undefined, + status: searchParams.get('status') as 'active' | 'error' | 'deprecated' | undefined, + search: searchParams.get('search') ?? undefined, + page: searchParams.get('page') ? parseInt(searchParams.get('page')!) : 1, + pageSize: searchParams.get('pageSize') ? parseInt(searchParams.get('pageSize')!) : 20, + sortBy: searchParams.get('sortBy') as SourceQueryOptions['sortBy'] | undefined, + sortOrder: searchParams.get('sortOrder') as 'asc' | 'desc' | undefined, + } + + const sourceManager = getSourceManager() + const result = await sourceManager.list(options) + + return NextResponse.json({ + success: true, + data: result.data, + pagination: { + total: result.total, + page: result.page, + pageSize: result.pageSize, + totalPages: Math.ceil(result.total / result.pageSize), + }, + }) + + } catch (error) { + console.error('Get sources error:', error) + + return NextResponse.json({ + success: false, + error: error instanceof Error ? error.message : 'Internal server error', + }, { status: 500 }) + } +} + +/** + * POST /api/music/source + * 创建/上传音源 + */ +export async function POST(request: NextRequest) { + try { + const body = await request.json() as CreateSourceRequest + + // 验证必填字段 + if (!body.name) { + return NextResponse.json({ + success: false, + error: 'Missing required field: name', + }, { status: 400 }) + } + + if (!body.format) { + return NextResponse.json({ + success: false, + error: 'Missing required field: format', + }, { status: 400 }) + } + + if (!body.scriptContent) { + return NextResponse.json({ + success: false, + error: 'Missing required field: scriptContent', + }, { status: 400 }) + } + + const validFormats: ScriptFormat[] = ['lx', 'lanyin', 'qzmusic'] + if (!validFormats.includes(body.format)) { + return NextResponse.json({ + success: false, + error: `Invalid format: ${body.format}. Valid formats: ${validFormats.join(', ')}`, + }, { status: 400 }) + } + + const sourceManager = getSourceManager() + + // 检查脚本是否已存在 + const duplicate = await sourceManager.checkDuplicate(body.scriptContent) + if (duplicate.exists) { + return NextResponse.json({ + success: false, + error: 'Script already exists', + data: { existingSource: duplicate.source }, + }, { status: 409 }) + } + + // 验证脚本 + const validation = await sourceManager.validateScript(body.scriptContent, body.format) + if (!validation.valid) { + return NextResponse.json({ + success: false, + error: `Invalid script: ${validation.error}`, + }, { status: 400 }) + } + + const source = await sourceManager.create(body) + + return NextResponse.json({ + success: true, + data: source, + message: 'Source created successfully', + }, { status: 201 }) + + } catch (error) { + console.error('Create source error:', error) + + return NextResponse.json({ + success: false, + error: error instanceof Error ? error.message : 'Internal server error', + }, { status: 500 }) + } +} + +/** + * PUT /api/music/source + * 更新音源 + */ +export async function PUT(request: NextRequest) { + try { + const body = await request.json() as { id: string } & UpdateSourceRequest + + if (!body.id) { + return NextResponse.json({ + success: false, + error: 'Missing required field: id', + }, { status: 400 }) + } + + const sourceManager = getSourceManager() + const updatedSource = await sourceManager.update(body.id, body) + + if (!updatedSource) { + return NextResponse.json({ + success: false, + error: 'Source not found', + }, { status: 404 }) + } + + return NextResponse.json({ + success: true, + data: updatedSource, + message: 'Source updated successfully', + }) + + } catch (error) { + console.error('Update source error:', error) + + return NextResponse.json({ + success: false, + error: error instanceof Error ? error.message : 'Internal server error', + }, { status: 500 }) + } +} + +/** + * DELETE /api/music/source + * 删除音源 + */ +export async function DELETE(request: NextRequest) { + try { + const { searchParams } = new URL(request.url) + const id = searchParams.get('id') + const ids = searchParams.get('ids') + + if (!id && !ids) { + return NextResponse.json({ + success: false, + error: 'Missing required parameter: id or ids', + }, { status: 400 }) + } + + const sourceManager = getSourceManager() + + if (ids) { + // 批量删除 + const idList = ids.split(',').filter(Boolean) + const count = await sourceManager.deleteMany(idList) + + return NextResponse.json({ + success: true, + data: { deletedCount: count }, + message: `Deleted ${count} source(s)`, + }) + } else { + // 单个删除 + const success = await sourceManager.delete(id!) + + if (!success) { + return NextResponse.json({ + success: false, + error: 'Source not found or delete failed', + }, { status: 404 }) + } + + return NextResponse.json({ + success: true, + message: 'Source deleted successfully', + }) + } + + } catch (error) { + console.error('Delete source error:', error) + + return NextResponse.json({ + success: false, + error: error instanceof Error ? error.message : 'Internal server error', + }, { status: 500 }) + } +} diff --git a/src/app/api/music/stats/route.ts b/src/app/api/music/stats/route.ts new file mode 100644 index 0000000..f3b02a4 --- /dev/null +++ b/src/app/api/music/stats/route.ts @@ -0,0 +1,73 @@ +/** + * HYW & Koneko 音源管理后台 + * + * 合作开发: MistAperio Studio × Macrohard Studio + * 编写人: 雾启工作室@云汀 (Cloudwhisper/Miao-moe), Macrohard Studio@Ryn磷熠 (Macrohard0001) + * + * 旧版后台由 Macrohard Studio 开发,编写人 @Macrohard0001 + */ + +import { NextRequest, NextResponse } from 'next/server' +import { getLogManager } from '@/lib/music' + +/** + * GET /api/music/stats + * 获取统计数据 + */ +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url) + + // 解析时间范围 + const startDateStr = searchParams.get('startDate') + const endDateStr = searchParams.get('endDate') + + let startDate: Date | undefined + let endDate: Date | undefined + + if (startDateStr) { + startDate = new Date(startDateStr) + } + if (endDateStr) { + endDate = new Date(endDateStr) + } + + const logManager = getLogManager() + + // 并行获取所有统计数据 + const [statistics, platformStats, topSources] = await Promise.all([ + logManager.getStatistics(startDate, endDate), + logManager.getPlatformStats(startDate, endDate), + logManager.getTopSources(10, startDate, endDate), + ]) + + return NextResponse.json({ + success: true, + data: { + overview: { + totalRequests: statistics.totalRequests, + successRequests: statistics.successRequests, + failedRequests: statistics.failedRequests, + successRate: statistics.successRate, + avgResponseTime: statistics.avgResponseTime, + }, + byPlatform: platformStats, + byQuality: statistics.requestsByQuality, + byHour: statistics.requestsByHour, + topSources, + timeRange: { + start: startDate?.toISOString(), + end: endDate?.toISOString(), + }, + }, + }) + + } catch (error) { + console.error('Get stats error:', error) + + return NextResponse.json({ + success: false, + error: error instanceof Error ? error.message : 'Internal server error', + }, { status: 500 }) + } +} diff --git a/src/app/api/music/url/route.ts b/src/app/api/music/url/route.ts new file mode 100644 index 0000000..2b83ac9 --- /dev/null +++ b/src/app/api/music/url/route.ts @@ -0,0 +1,363 @@ +/** + * HYW & Koneko 音源管理后台 + * + * 合作开发: MistAperio Studio × Macrohard Studio + * 编写人: 雾启工作室@云汀 (Cloudwhisper/Miao-moe), Macrohard Studio@Ryn磷熠 (Macrohard0001) + * + * 旧版后台由 Macrohard Studio 开发,编写人 @Macrohard0001 + */ + +import { NextRequest, NextResponse } from 'next/server' +import { getScheduler, getCardKeyManager, getLogManager, getIPRulesManager, getConfigManager } from '@/lib/music' +import type { MusicPlatform, MusicQuality, MusicRequest } from '@/types' +import { v4 as uuidv4 } from 'uuid' + +/** + * 验证请求参数 + */ +function validateParams(params: { + platform?: string + songId?: string + quality?: string +}): { valid: boolean; error?: string } { + const validPlatforms = ['kw', 'kg', 'tx', 'wy', 'mg'] + const validQualities = ['128k', '320k', 'flac', 'flac24bit', 'hires', 'dolby', 'atmos_plus', 'master'] + + if (!params.platform) { + return { valid: false, error: 'Missing required parameter: platform' } + } + if (!validPlatforms.includes(params.platform)) { + return { valid: false, error: `Invalid platform: ${params.platform}. Valid platforms: ${validPlatforms.join(', ')}` } + } + + if (!params.songId) { + return { valid: false, error: 'Missing required parameter: songId' } + } + + if (!params.quality) { + return { valid: false, error: 'Missing required parameter: quality' } + } + if (!validQualities.includes(params.quality)) { + return { valid: false, error: `Invalid quality: ${params.quality}. Valid qualities: ${validQualities.join(', ')}` } + } + + return { valid: true } +} + +/** + * 获取客户端 IP + */ +function getClientIP(request: NextRequest): string { + const forwarded = request.headers.get('x-forwarded-for') + if (forwarded) { + return forwarded.split(',')[0].trim() + } + + const realIP = request.headers.get('x-real-ip') + if (realIP) { + return realIP + } + + return '127.0.0.1' +} + +/** + * GET /api/music/url + * 获取单个音乐 URL + */ +export async function GET(request: NextRequest) { + const startTime = Date.now() + const requestId = uuidv4() + + try { + const { searchParams } = new URL(request.url) + const platform = searchParams.get('platform') as MusicPlatform | undefined + const songId = searchParams.get('songId') ?? undefined + const quality = searchParams.get('quality') as MusicQuality | undefined + const cardKey = searchParams.get('cardKey') ?? undefined + + // 验证参数 + const validation = validateParams({ platform, songId, quality }) + if (!validation.valid) { + return NextResponse.json({ + success: false, + error: validation.error, + requestId, + }, { status: 400 }) + } + + const clientIP = getClientIP(request) + + // 检查服务状态 + const configManager = getConfigManager() + const serviceStatus = await configManager.getServiceStatus() + + if (serviceStatus.status === 'maintenance') { + return NextResponse.json({ + success: false, + error: serviceStatus.message || 'Service is under maintenance', + requestId, + }, { status: 503 }) + } + + if (serviceStatus.status === 'stopped') { + return NextResponse.json({ + success: false, + error: 'Service is stopped', + requestId, + }, { status: 503 }) + } + + // 检查 IP 规则 + const ipRulesManager = getIPRulesManager() + const ipCheck = await ipRulesManager.checkIP(clientIP) + + if (!ipCheck.allowed) { + return NextResponse.json({ + success: false, + error: ipCheck.reason || 'IP is not allowed', + requestId, + }, { status: 403 }) + } + + // 验证卡密(如果提供) + if (cardKey) { + const cardKeyManager = getCardKeyManager() + const verifyResult = await cardKeyManager.verify(cardKey, { + ip: clientIP, + platform, + quality, + }) + + if (!verifyResult.valid) { + return NextResponse.json({ + success: false, + error: verifyResult.error || 'Invalid card key', + requestId, + }, { status: 401 }) + } + + // 使用卡密 + const useResult = await cardKeyManager.use(cardKey, clientIP) + if (!useResult.success) { + return NextResponse.json({ + success: false, + error: useResult.error || 'Card key usage failed', + requestId, + }, { status: 429 }) + } + } + + // 执行请求 + const scheduler = getScheduler() + const response = await scheduler.executeWithRetry({ + requestId, + platform: platform!, + songId: songId!, + quality: quality!, + ip: clientIP, + userAgent: request.headers.get('user-agent') ?? undefined, + cardKey, + }) + + const responseTime = Date.now() - startTime + + // 记录日志 + const logManager = getLogManager() + await logManager.log({ + platform, + songId, + quality, + source: response.source, + success: response.success, + responseTime, + errorMessage: response.error, + ip: clientIP, + userAgent: request.headers.get('user-agent') ?? undefined, + cardKey, + }) + + return NextResponse.json({ + success: response.success, + data: response.success ? { + url: response.url, + info: response.info, + source: response.source, + } : null, + error: response.error, + requestId, + responseTime, + }, { status: response.success ? 200 : 500 }) + + } catch (error) { + const responseTime = Date.now() - startTime + console.error('Get music URL error:', error) + + return NextResponse.json({ + success: false, + error: error instanceof Error ? error.message : 'Internal server error', + requestId, + responseTime, + }, { status: 500 }) + } +} + +/** + * POST /api/music/url + * 批量获取音乐 URL + */ +export async function POST(request: NextRequest) { + const startTime = Date.now() + const requestId = uuidv4() + + try { + const body = await request.json() + const { songs, cardKey } = body as { + songs: MusicRequest[] + cardKey?: string + } + + // 验证请求体 + if (!songs || !Array.isArray(songs) || songs.length === 0) { + return NextResponse.json({ + success: false, + error: 'Invalid request body. Expected { songs: MusicRequest[] }', + requestId, + }, { status: 400 }) + } + + // 限制批量请求数量 + if (songs.length > 50) { + return NextResponse.json({ + success: false, + error: 'Too many songs. Maximum 50 songs per batch.', + requestId, + }, { status: 400 }) + } + + const clientIP = getClientIP(request) + + // 检查服务状态 + const configManager = getConfigManager() + const serviceStatus = await configManager.getServiceStatus() + + if (serviceStatus.status !== 'running') { + return NextResponse.json({ + success: false, + error: serviceStatus.message || `Service is ${serviceStatus.status}`, + requestId, + }, { status: 503 }) + } + + // 检查 IP 规则 + const ipRulesManager = getIPRulesManager() + const ipCheck = await ipRulesManager.checkIP(clientIP) + + if (!ipCheck.allowed) { + return NextResponse.json({ + success: false, + error: ipCheck.reason || 'IP is not allowed', + requestId, + }, { status: 403 }) + } + + // 验证卡密(如果提供) + if (cardKey) { + const cardKeyManager = getCardKeyManager() + const verifyResult = await cardKeyManager.verify(cardKey, { + ip: clientIP, + }) + + if (!verifyResult.valid) { + return NextResponse.json({ + success: false, + error: verifyResult.error || 'Invalid card key', + requestId, + }, { status: 401 }) + } + } + + // 批量执行请求 + const scheduler = getScheduler() + const results = await Promise.all( + songs.map(async (song, index) => { + const songRequestId = `${requestId}-${index}` + + try { + const response = await scheduler.executeWithRetry({ + requestId: songRequestId, + platform: song.platform, + songId: song.songId, + quality: song.quality, + ip: clientIP, + cardKey, + }) + + return { + songId: song.songId, + platform: song.platform, + quality: song.quality, + success: response.success, + url: response.url, + info: response.info, + source: response.source, + error: response.error, + responseTime: response.responseTime, + } + } catch (error) { + return { + songId: song.songId, + platform: song.platform, + quality: song.quality, + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + responseTime: 0, + } + } + }) + ) + + const responseTime = Date.now() - startTime + + // 记录日志 + const logManager = getLogManager() + await logManager.logBatch( + results.map((result, index) => ({ + platform: songs[index].platform, + songId: songs[index].songId, + quality: songs[index].quality, + source: result.source, + success: result.success, + responseTime: result.responseTime, + errorMessage: result.error, + ip: clientIP, + cardKey, + })) + ) + + const successCount = results.filter(r => r.success).length + + return NextResponse.json({ + success: true, + data: { + total: results.length, + success: successCount, + failed: results.length - successCount, + results, + }, + requestId, + responseTime, + }) + + } catch (error) { + const responseTime = Date.now() - startTime + console.error('Batch get music URL error:', error) + + return NextResponse.json({ + success: false, + error: error instanceof Error ? error.message : 'Internal server error', + requestId, + responseTime, + }, { status: 500 }) + } +} diff --git a/src/app/api/route.ts b/src/app/api/route.ts new file mode 100644 index 0000000..86b7ee9 --- /dev/null +++ b/src/app/api/route.ts @@ -0,0 +1,48 @@ +/** + * HYW & Koneko 音源管理后台 + * + * 合作开发: MistAperio Studio × Macrohard Studio + * 编写人: 雾启工作室@云汀 (Cloudwhisper/Miao-moe), Macrohard Studio@Ryn磷熠 (Macrohard0001) + * + * 旧版后台由 Macrohard Studio 开发,编写人 @Macrohard0001 + */ + +import { NextResponse } from 'next/server' +import { getConfigManager } from '@/lib/music' + +/** + * GET /api + * 健康检查,返回服务状态 + */ +export async function GET() { + try { + const configManager = getConfigManager() + const serviceStatus = await configManager.getServiceStatus() + + return NextResponse.json({ + success: true, + data: { + name: 'HYW & Koneko 音源管理后台', + version: '2.0.0', + status: serviceStatus.status, + message: serviceStatus.message, + timestamp: new Date().toISOString(), + uptime: process.uptime(), + environment: process.env.NODE_ENV, + }, + }) + } catch (error) { + console.error('Health check error:', error) + + return NextResponse.json({ + success: false, + error: 'Internal server error', + data: { + name: 'HYW & Koneko 音源管理后台', + version: '2.0.0', + status: 'error', + timestamp: new Date().toISOString(), + }, + }, { status: 500 }) + } +} diff --git a/src/app/card-keys/page.tsx b/src/app/card-keys/page.tsx new file mode 100644 index 0000000..e3397c5 --- /dev/null +++ b/src/app/card-keys/page.tsx @@ -0,0 +1,552 @@ +/** + * HYW & Koneko 音源管理后台 + * + * 合作开发: MistAperio Studio × Macrohard Studio + * 编写人: 雾启工作室@云汀 (Cloudwhisper/Miao-moe), Macrohard Studio@Ryn磷熠 (Macrohard0001) + * + * 旧版后台由 Macrohard Studio 开发,编写人 @Macrohard0001 + */ + +'use client' + +import React, { useState, useEffect } from 'react' +import Layout from '@/components/Layout' +import { + Card, + Table, + Button, + Space, + Tag, + Modal, + Form, + Input, + Select, + InputNumber, + DatePicker, + message, + Popconfirm, + Tooltip, + Typography, + Row, + Col, + Drawer, + Descriptions, + Empty, + Spin, + Statistic, + Progress, +} from 'antd' +import { + PlusOutlined, + DeleteOutlined, + SearchOutlined, + ReloadOutlined, + EyeOutlined, + CopyOutlined, + KeyOutlined, + CheckCircleOutlined, + CloseCircleOutlined, + ClockCircleOutlined, + StopOutlined, + BarChartOutlined, +} from '@ant-design/icons' +import { useTheme } from '@/components/ThemeProvider' +import type { CardKeyConfig, CardKeyType, CardKeyStatus, MusicQuality, MusicPlatform } from '@/types' +import { QUALITY_NAMES, PLATFORM_NAMES } from '@/types' + +const { Title, Text, Paragraph } = Typography +const { Option } = Select +const { RangePicker } = DatePicker + +// 卡密管理页面 +export default function CardKeysPage() { + const { isDark } = useTheme() + const [form] = Form.useForm() + + // 状态 + const [cardKeys, setCardKeys] = useState([]) + const [loading, setLoading] = useState(false) + const [modalVisible, setModalVisible] = useState(false) + const [drawerVisible, setDrawerVisible] = useState(false) + const [viewingCardKey, setViewingCardKey] = useState(null) + const [searchText, setSearchText] = useState('') + const [submitting, setSubmitting] = useState(false) + + // 加载卡密列表 + const loadCardKeys = async () => { + setLoading(true) + try { + const response = await fetch('/api/music/card-key') + if (response.ok) { + const data = await response.json() + setCardKeys(data.cardKeys || []) + } + } catch (error) { + console.error('Failed to load card keys:', error) + } finally { + setLoading(false) + } + } + + // 创建卡密 + const handleCreateCardKey = async (values: Partial) => { + setSubmitting(true) + try { + const response = await fetch('/api/music/card-key', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(values), + }) + if (response.ok) { + message.success('卡密创建成功') + setModalVisible(false) + form.resetFields() + loadCardKeys() + } else { + message.error('创建失败') + } + } catch (error) { + message.error('创建失败') + } finally { + setSubmitting(false) + } + } + + // 删除卡密 + const handleDeleteCardKey = async (id: string) => { + try { + const response = await fetch(`/api/music/card-key?id=${id}`, { + method: 'DELETE', + }) + if (response.ok) { + message.success('卡密删除成功') + loadCardKeys() + } else { + message.error('删除失败') + } + } catch (error) { + message.error('删除失败') + } + } + + // 复制卡密 + const handleCopyCardKey = (key: string) => { + navigator.clipboard.writeText(key) + message.success('卡密已复制到剪贴板') + } + + // 查看详情 + const handleViewDetails = (cardKey: CardKeyConfig) => { + setViewingCardKey(cardKey) + setDrawerVisible(true) + } + + // 获取卡密类型标签 + const getTypeTag = (type: CardKeyType) => { + const config = { + normal: { color: 'default', text: '普通' }, + vip: { color: 'gold', text: 'VIP' }, + admin: { color: 'purple', text: '管理员' }, + } + return {config[type].text} + } + + // 获取卡密状态标签 + const getStatusTag = (status: CardKeyStatus) => { + const config = { + active: { color: 'success', icon: , text: '可用' }, + used: { color: 'default', icon: , text: '已用完' }, + expired: { color: 'warning', icon: , text: '已过期' }, + disabled: { color: 'error', icon: , text: '已禁用' }, + } + const c = config[status] + return {c.text} + } + + // 初始化加载 + useEffect(() => { + loadCardKeys() + }, []) + + // 过滤卡密列表 + const filteredCardKeys = cardKeys.filter( + (item) => + item.key.toLowerCase().includes(searchText.toLowerCase()) || + item.remark?.toLowerCase().includes(searchText.toLowerCase()) + ) + + // 统计数据 + const statistics = { + total: cardKeys.length, + active: cardKeys.filter((k) => k.status === 'active').length, + used: cardKeys.filter((k) => k.status === 'used').length, + expired: cardKeys.filter((k) => k.status === 'expired').length, + } + + // 表格列定义 + const columns = [ + { + title: '卡密', + dataIndex: 'key', + key: 'key', + width: 280, + render: (key: string) => ( + + + {key.length > 24 ? `${key.slice(0, 12)}...${key.slice(-12)}` : key} + + + + + + + + + + {/* 卡密列表 */} + + `共 ${total} 条`, + defaultPageSize: 10, + }} + locale={{ + emptyText: , + }} + /> + + + {/* 创建卡密弹窗 */} + { + setModalVisible(false) + form.resetFields() + }} + onOk={() => form.submit()} + confirmLoading={submitting} + width={600} + destroyOnClose + > +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {/* 详情抽屉 */} + setDrawerVisible(false)} + open={drawerVisible} + > + {viewingCardKey && ( + + + + {viewingCardKey.key} + + + } + /> + + + + + } + /> + + + + + } + /> + + + + + {/* 工具栏 */} + + + + } + value={searchText} + onChange={(e) => setSearchText(e.target.value)} + style={{ maxWidth: 400 }} + allowClear + /> + + + + + + + + + + + {/* 规则列表 */} + + setActiveTab(key as typeof activeTab)} + items={tabItems} + /> + +
`共 ${total} 条`, + defaultPageSize: 10, + }} + locale={{ + emptyText: , + }} + /> + + + {/* 添加规则弹窗 */} + { + setModalVisible(false) + form.resetFields() + }} + onOk={() => form.submit()} + confirmLoading={submitting} + width={500} + destroyOnClose + > +
+ + + + + + + + + + + + + + + + +
+ + ) +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx new file mode 100644 index 0000000..d475005 --- /dev/null +++ b/src/app/layout.tsx @@ -0,0 +1,148 @@ +/** + * HYW & Koneko 音源管理后台 + * + * 合作开发: MistAperio Studio × Macrohard Studio + * 编写人: 雾启工作室@云汀 (Cloudwhisper/Miao-moe), Macrohard Studio@Ryn磷熠 (Macrohard0001) + * + * 旧版后台由 Macrohard Studio 开发,编写人 @Macrohard0001 + */ + +import type { Metadata, Viewport } from 'next' +import { AntdRegistry } from '@ant-design/nextjs-registry' +import { ConfigProvider, theme } from 'antd' +import zhCN from 'antd/locale/zh_CN' +import './globals.css' +import { ThemeProvider } from '@/components/ThemeProvider' + +// 元数据配置 +export const metadata: Metadata = { + title: 'HYW & Koneko 音源管理后台', + description: 'HYW & Koneko 音源管理后台 - 多平台音源聚合服务,支持 LX Music、澜音、QZ Music 格式', + keywords: ['音源管理', 'LX Music', '澜音', 'QZ Music', '音乐源'], + authors: [ + { name: '雾启工作室@云汀', url: 'https://github.com/Miao-moe' }, + { name: 'Macrohard Studio@Ryn磷熠', url: 'https://github.com/Macrohard0001' }, + ], + creator: 'MistAperio Studio × Macrohard Studio', + publisher: 'MistAperio Studio × Macrohard Studio', + robots: { + index: false, + follow: false, + }, +} + +// 视口配置 +export const viewport: Viewport = { + width: 'device-width', + initialScale: 1, + maximumScale: 1, + userScalable: false, + themeColor: [ + { media: '(prefers-color-scheme: light)', color: '#1677ff' }, + { media: '(prefers-color-scheme: dark)', color: '#1f1f1f' }, + ], +} + +// 默认主题配置 +const defaultThemeConfig = { + token: { + colorPrimary: '#1677ff', + borderRadius: 6, + fontSize: 14, + fontSizeHeading1: 38, + fontSizeHeading2: 30, + fontSizeHeading3: 24, + fontSizeHeading4: 20, + fontSizeHeading5: 16, + lineType: 'solid', + }, + components: { + Layout: { + headerBg: '#ffffff', + siderBg: '#001529', + bodyBg: '#f5f5f5', + }, + Menu: { + darkItemBg: '#001529', + darkItemSelectedBg: '#1677ff', + darkItemHoverBg: 'rgba(255, 255, 255, 0.08)', + }, + Card: { + paddingLG: 24, + }, + Table: { + headerBg: '#fafafa', + }, + }, +} + +// 深色主题配置 +const darkThemeConfig = { + token: { + colorPrimary: '#1677ff', + borderRadius: 6, + fontSize: 14, + }, + components: { + Layout: { + headerBg: '#141414', + siderBg: '#1f1f1f', + bodyBg: '#000000', + }, + Menu: { + darkItemBg: '#1f1f1f', + darkItemSelectedBg: '#1677ff', + darkItemHoverBg: 'rgba(255, 255, 255, 0.08)', + }, + Card: { + paddingLG: 24, + }, + Table: { + headerBg: '#1f1f1f', + }, + }, +} + +// 根布局组件 +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + + + + + + + + {children} + + + + + ) +} + +// 根布局内容组件 +function RootLayoutContent({ children }: { children: React.ReactNode }) { + // 这里使用客户端组件来获取主题状态 + // 实际的主题切换逻辑在 ThemeProvider 中处理 + const isDark = false // 默认值,实际由 ThemeProvider 控制 + + return ( + + {children} + + ) +} diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx new file mode 100644 index 0000000..48331a8 --- /dev/null +++ b/src/app/login/page.tsx @@ -0,0 +1,239 @@ +/** + * HYW & Koneko 音源管理后台 - 登录页面 + * + * 合作开发: MistAperio Studio × Macrohard Studio + * 编写人: 雾启工作室@云汀 (Cloudwhisper/Miao-moe), Macrohard Studio@Ryn磷熠 (Macrohard0001) + * + * 旧版后台由 Macrohard Studio 开发,编写人 @Macrohard0001 + */ + +'use client' + +import React, { useState, useEffect } from 'react' +import { useRouter } from 'next/navigation' +import { + Form, + Input, + Button, + Card, + message, + Divider, + Space, + Typography, + theme, +} from 'antd' +import { + UserOutlined, + LockOutlined, + GithubOutlined, + ApiOutlined, +} from '@ant-design/icons' +import { useAuth } from '@/hooks/useAuth' + +const { Title, Text, Paragraph } = Typography +const { useToken } = theme + +// ============================================ +// 登录表单字段 +// ============================================ + +interface LoginFormValues { + username: string + password: string + remember?: boolean +} + +// ============================================ +// 登录页面组件 +// ============================================ + +export default function LoginPage() { + const router = useRouter() + const { token: designToken } = useToken() + const { login, isLoading, error, clearError, isAuthenticated } = useAuth() + const [form] = Form.useForm() + const [loginType, setLoginType] = useState<'local' | 'thirdParty'>('local') + + // 如果已登录,重定向到首页 + useEffect(() => { + if (isAuthenticated) { + router.push('/') + } + }, [isAuthenticated, router]) + + // 显示错误消息 + useEffect(() => { + if (error) { + message.error(error) + clearError() + } + }, [error, clearError]) + + /** + * 处理本地登录 + */ + const handleLocalLogin = async (values: LoginFormValues) => { + const response = await login({ + username: values.username, + password: values.password, + authProvider: 'local', + }) + + if (response.success) { + message.success('登录成功') + router.push('/') + } + } + + /** + * 处理第三方登录(预留) + */ + const handleThirdPartyLogin = async (provider: string) => { + message.info(`${provider} 登录功能即将上线`) + // TODO: 实现第三方登录流程 + // 1. 跳转到第三方授权页面 + // 2. 获取授权码 + // 3. 调用后端换取 token + } + + // 页面样式 + const pageStyle: React.CSSProperties = { + minHeight: '100vh', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + background: designToken.colorBgLayout, + padding: '24px', + } + + const cardStyle: React.CSSProperties = { + width: '100%', + maxWidth: 420, + borderRadius: designToken.borderRadiusLG, + boxShadow: designToken.boxShadowSecondary, + } + + const logoStyle: React.CSSProperties = { + textAlign: 'center', + marginBottom: 32, + } + + const titleStyle: React.CSSProperties = { + marginBottom: 8, + color: designToken.colorText, + } + + const subtitleStyle: React.CSSProperties = { + color: designToken.colorTextSecondary, + marginBottom: 24, + } + + const thirdPartyButtonStyle: React.CSSProperties = { + width: '100%', + marginBottom: 12, + } + + return ( +
+ + {/* Logo 和标题 */} +
+
🎵
+ + HYW & Koneko + + + 音源管理后台 + +
+ + {/* 本地登录表单 */} + {loginType === 'local' && ( +
+ + } + placeholder="用户名 / 邮箱" + size="large" + /> + + + + } + placeholder="密码" + size="large" + /> + + + + + + + )} + + {/* 第三方登录选项 */} + + 其他登录方式 + + + + {/* 时迁认证中心 */} + + + {/* GitHub 登录(预留) */} + + + + {/* 底部信息 */} + + +
+ + 合作开发: MistAperio Studio × Macrohard Studio + + + 编写人: 雾启工作室@云汀, Macrohard Studio@Ryn磷熠 + +
+
+
+ ) +} diff --git a/src/app/page.tsx b/src/app/page.tsx new file mode 100644 index 0000000..fb46600 --- /dev/null +++ b/src/app/page.tsx @@ -0,0 +1,23 @@ +/** + * HYW & Koneko 音源管理后台 + * + * 合作开发: MistAperio Studio × Macrohard Studio + * 编写人: 雾启工作室@云汀 (Cloudwhisper/Miao-moe), Macrohard Studio@Ryn磷熠 (Macrohard0001) + * + * 旧版后台由 Macrohard Studio 开发,编写人 @Macrohard0001 + */ + +'use client' + +import React from 'react' +import Layout from '@/components/Layout' +import Dashboard from '@/components/Dashboard' + +// 首页组件 +export default function HomePage() { + return ( + + + + ) +} diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx new file mode 100644 index 0000000..f3483e4 --- /dev/null +++ b/src/app/settings/page.tsx @@ -0,0 +1,372 @@ +/** + * HYW & Koneko 音源管理后台 + * + * 合作开发: MistAperio Studio × Macrohard Studio + * 编写人: 雾启工作室@云汀 (Cloudwhisper/Miao-moe), Macrohard Studio@Ryn磷熠 (Macrohard0001) + * + * 旧版后台由 Macrohard Studio 开发,编写人 @Macrohard0001 + */ + +'use client' + +import React, { useState, useEffect } from 'react' +import Layout from '@/components/Layout' +import { + Card, + Row, + Col, + Typography, + Form, + Input, + InputNumber, + Select, + Switch, + Button, + Space, + Divider, + message, + Spin, + Tag, + Alert, +} from 'antd' +import { + SaveOutlined, + ReloadOutlined, + SettingOutlined, + SafetyOutlined, + ThunderboltOutlined, + ControlOutlined, + PlayCircleOutlined, + PauseCircleOutlined, + ToolOutlined, +} from '@ant-design/icons' +import { useTheme } from '@/components/ThemeProvider' +import type { SystemConfig, RateLimitConfig, ExecutionMode, QualityTier, MusicQuality } from '@/types' +import { DEFAULT_RATE_LIMIT, DEFAULT_QUALITY_CONFIG, QUALITY_NAMES } from '@/types' + +const { Title, Text, Paragraph } = Typography +const { Option } = Select + +// 系统设置页面 +export default function SettingsPage() { + const { isDark } = useTheme() + const [form] = Form.useForm() + const [loading, setLoading] = useState(false) + const [saving, setSaving] = useState(false) + const [config, setConfig] = useState(null) + + // 加载配置 + const loadConfig = async () => { + setLoading(true) + try { + const response = await fetch('/api/music/config') + if (response.ok) { + const data = await response.json() + setConfig(data.config) + form.setFieldsValue(data.config) + } + } catch (error) { + console.error('Failed to load config:', error) + } finally { + setLoading(false) + } + } + + // 保存配置 + const handleSave = async () => { + try { + const values = await form.validateFields() + setSaving(true) + + const response = await fetch('/api/music/config', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(values), + }) + + if (response.ok) { + message.success('配置保存成功') + loadConfig() + } else { + message.error('保存失败') + } + } catch (error) { + message.error('保存失败') + } finally { + setSaving(false) + } + } + + // 初始化加载 + useEffect(() => { + loadConfig() + }, []) + + // 服务状态配置 + const serviceStatusOptions = [ + { value: 'running', label: '运行中', color: 'success', icon: }, + { value: 'stopped', label: '已停止', color: 'error', icon: }, + { value: 'maintenance', label: '维护中', color: 'warning', icon: }, + ] + + // 执行模式选项 + const executionModeOptions = [ + { value: 'simplified', label: '简化模式', description: '快速响应,适合高并发场景' }, + { value: 'full', label: '完整模式', description: '完整功能,适合功能丰富的场景' }, + { value: 'hybrid', label: '混合模式', description: '智能切换,根据负载自动调整' }, + ] + + return ( + + {/* 版权信息头部 */} +
+ 系统设置 + + 配置系统参数,包括服务状态、频率限制、音质配置等 + +
+ + +
+ {/* 基本设置 */} + + + 基本设置 + + } + style={{ background: isDark ? '#1f1f1f' : '#fff', marginBottom: 16 }} + > + +
+ + + + + + + + + + + + +
  • 运行中:服务正常运行,接受所有请求
  • +
  • 已停止:服务已停止,拒绝所有请求
  • +
  • 维护中:服务维护中,显示维护消息
  • + + } + type="info" + showIcon + /> + + + {/* 频率限制设置 */} + + + 频率限制设置 + + } + style={{ background: isDark ? '#1f1f1f' : '#fff', marginBottom: 16 }} + > + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {/* 音质配置 */} + + + 音质配置 + + } + style={{ background: isDark ? '#1f1f1f' : '#fff', marginBottom: 16 }} + > + + + + + + + + + + + + + + + + + + + + + + {/* 执行模式设置 */} + + + 执行模式设置 + + } + style={{ background: isDark ? '#1f1f1f' : '#fff', marginBottom: 16 }} + > + + + + + + + + {executionModeOptions.map(option => ( + + + {option.label} + + {option.description} + + + + ))} + + + + {/* 操作按钮 */} + + + + + + + + + + + + ) +} diff --git a/src/app/sources/page.tsx b/src/app/sources/page.tsx new file mode 100644 index 0000000..40f6e6e --- /dev/null +++ b/src/app/sources/page.tsx @@ -0,0 +1,110 @@ +/** + * HYW & Koneko 音源管理后台 + * + * 合作开发: MistAperio Studio × Macrohard Studio + * 编写人: 雾启工作室@云汀 (Cloudwhisper/Miao-moe), Macrohard Studio@Ryn磷熠 (Macrohard0001) + * + * 旧版后台由 Macrohard Studio 开发,编写人 @Macrohard0001 + */ + +'use client' + +import React, { useState, useEffect } from 'react' +import Layout from '@/components/Layout' +import SourceManager from '@/components/SourceManager' +import { Typography } from 'antd' +import type { SourceConfig } from '@/types' + +const { Title, Text } = Typography + +// 音源管理页面 +export default function SourcesPage() { + const [sources, setSources] = useState([]) + const [loading, setLoading] = useState(false) + + // 加载音源列表 + const loadSources = async () => { + setLoading(true) + try { + const response = await fetch('/api/music/source') + if (response.ok) { + const data = await response.json() + setSources(data.sources || []) + } + } catch (error) { + console.error('Failed to load sources:', error) + } finally { + setLoading(false) + } + } + + // 添加音源 + const handleAddSource = async (source: Partial) => { + const response = await fetch('/api/music/source', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(source), + }) + if (!response.ok) throw new Error('Failed to add source') + loadSources() + } + + // 更新音源 + const handleUpdateSource = async (id: string, source: Partial) => { + const response = await fetch('/api/music/source', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ id, ...source }), + }) + if (!response.ok) throw new Error('Failed to update source') + loadSources() + } + + // 删除音源 + const handleDeleteSource = async (id: string) => { + const response = await fetch(`/api/music/source?id=${id}`, { + method: 'DELETE', + }) + if (!response.ok) throw new Error('Failed to delete source') + loadSources() + } + + // 切换启用状态 + const handleToggleSource = async (id: string, enabled: boolean) => { + const response = await fetch('/api/music/source', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ id, enabled }), + }) + if (!response.ok) throw new Error('Failed to toggle source') + loadSources() + } + + // 初始化加载 + useEffect(() => { + loadSources() + }, []) + + return ( + + {/* 版权信息头部 */} +
    + 音源管理 + + 管理音乐音源,包括添加、编辑、删除和启用/禁用音源 + +
    + + {/* 音源管理组件 */} + +
    + ) +} diff --git a/src/app/theme/page.tsx b/src/app/theme/page.tsx new file mode 100644 index 0000000..3306107 --- /dev/null +++ b/src/app/theme/page.tsx @@ -0,0 +1,51 @@ +/** + * HYW & Koneko 音源管理后台 + * + * 合作开发: MistAperio Studio × Macrohard Studio + * 编写人: 雾启工作室@云汀 (Cloudwhisper/Miao-moe), Macrohard Studio@Ryn磷熠 (Macrohard0001) + * + * 旧版后台由 Macrohard Studio 开发,编写人 @Macrohard0001 + */ + +'use client' + +import React from 'react' +import Layout from '@/components/Layout' +import ThemeEditor from '@/components/ThemeEditor' +import { Typography } from 'antd' +import type { ThemeVars } from '@/types' + +const { Title, Text } = Typography + +// 主题设置页面 +export default function ThemePage() { + // 保存主题配置 + const handleSaveTheme = async (theme: ThemeVars) => { + // 这里可以调用 API 保存主题配置到数据库 + console.log('Saving theme:', theme) + } + + // 重置主题 + const handleResetTheme = () => { + // 这里可以调用 API 重置主题配置 + console.log('Resetting theme') + } + + return ( + + {/* 版权信息头部 */} +
    + 主题设置 + + 自定义系统外观,选择预设主题或创建自定义主题 + +
    + + {/* 主题编辑器组件 */} + +
    + ) +} diff --git a/src/components/Dashboard.tsx b/src/components/Dashboard.tsx new file mode 100644 index 0000000..b936de6 --- /dev/null +++ b/src/components/Dashboard.tsx @@ -0,0 +1,614 @@ +/** + * HYW & Koneko 音源管理后台 + * + * 合作开发: MistAperio Studio × Macrohard Studio + * 编写人: 雾启工作室@云汀 (Cloudwhisper/Miao-moe), Macrohard Studio@Ryn磷熠 (Macrohard0001) + * + * 旧版后台由 Macrohard Studio 开发,编写人 @Macrohard0001 + */ + +'use client' + +import React, { useState, useEffect, useMemo } from 'react' +import { Card, Row, Col, Statistic, Progress, Tag, Typography, Space, List, Avatar, Empty, Spin, Tooltip } from 'antd' +import { + ApiOutlined, + CheckCircleOutlined, + CloseCircleOutlined, + ClockCircleOutlined, + DatabaseOutlined, + LineChartOutlined, + RiseOutlined, + FallOutlined, + SyncOutlined, + WarningOutlined, + PlayCircleOutlined, + PauseCircleOutlined, +} from '@ant-design/icons' +import { useTheme } from './ThemeProvider' +import type { Statistics, SourceStatus, MusicPlatform } from '@/types' +import { PLATFORM_NAMES, PLATFORM_ICONS } from '@/types' + +const { Title, Text } = Typography + +// 统计卡片属性 +interface StatCardProps { + title: string + value: number | string + prefix?: React.ReactNode + suffix?: React.ReactNode + valueStyle?: React.CSSProperties + trend?: { + value: number + isUp: boolean + } + loading?: boolean + color?: string +} + +// 统计卡片组件 +function StatCard({ title, value, prefix, suffix, valueStyle, trend, loading, color }: StatCardProps) { + const { isDark } = useTheme() + + return ( + + {title}} + value={value} + prefix={prefix} + suffix={suffix} + valueStyle={{ fontSize: 28, fontWeight: 600, ...valueStyle }} + /> + {trend && ( +
    + {trend.isUp ? ( + + ) : ( + + )} + + {trend.value}% 较昨日 + +
    + )} + {color && ( +
    + )} + + ) +} + +// 服务状态卡片属性 +interface ServiceStatusCardProps { + status: 'running' | 'stopped' | 'maintenance' + uptime?: string + version?: string +} + +// 服务状态卡片组件 +function ServiceStatusCard({ status, uptime = '99.9%', version = 'v3.0.0' }: ServiceStatusCardProps) { + const { isDark } = useTheme() + + const statusConfig = { + running: { + color: '#52c41a', + text: '运行中', + icon: , + }, + stopped: { + color: '#ff4d4f', + text: '已停止', + icon: , + }, + maintenance: { + color: '#faad14', + text: '维护中', + icon: , + }, + } + + const config = statusConfig[status] + + return ( + +
    +
    + 服务状态 +
    + + {config.text} + + 运行时间: {uptime} +
    +
    +
    + 版本 +
    + {version} +
    +
    +
    +
    + ) +} + +// 请求趋势图组件 +function RequestTrendChart({ data, loading }: { data: number[]; loading?: boolean }) { + const { isDark } = useTheme() + const canvasRef = React.useRef(null) + + // 绘制图表 + useEffect(() => { + if (!canvasRef.current || loading || !data.length) return + + const canvas = canvasRef.current + const ctx = canvas.getContext('2d') + if (!ctx) return + + // 设置画布尺寸 + const dpr = window.devicePixelRatio || 1 + const rect = canvas.getBoundingClientRect() + canvas.width = rect.width * dpr + canvas.height = rect.height * dpr + ctx.scale(dpr, dpr) + + const width = rect.width + const height = rect.height + const padding = { top: 20, right: 20, bottom: 30, left: 50 } + const chartWidth = width - padding.left - padding.right + const chartHeight = height - padding.top - padding.bottom + + // 清空画布 + ctx.clearRect(0, 0, width, height) + + // 计算数据范围 + const maxValue = Math.max(...data, 1) + const minValue = 0 + + // 绘制网格线 + ctx.strokeStyle = isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)' + ctx.lineWidth = 1 + for (let i = 0; i <= 4; i++) { + const y = padding.top + (chartHeight / 4) * i + ctx.beginPath() + ctx.moveTo(padding.left, y) + ctx.lineTo(width - padding.right, y) + ctx.stroke() + + // Y轴标签 + const value = Math.round(maxValue - (maxValue / 4) * i) + ctx.fillStyle = isDark ? 'rgba(255,255,255,0.45)' : 'rgba(0,0,0,0.45)' + ctx.font = '12px sans-serif' + ctx.textAlign = 'right' + ctx.fillText(value.toString(), padding.left - 10, y + 4) + } + + // 绘制折线 + const gradient = ctx.createLinearGradient(0, padding.top, 0, height - padding.bottom) + gradient.addColorStop(0, 'rgba(22, 119, 255, 0.3)') + gradient.addColorStop(1, 'rgba(22, 119, 255, 0)') + + // 绘制填充区域 + ctx.beginPath() + data.forEach((value, index) => { + const x = padding.left + (chartWidth / (data.length - 1)) * index + const y = padding.top + chartHeight - (value / maxValue) * chartHeight + if (index === 0) { + ctx.moveTo(x, y) + } else { + ctx.lineTo(x, y) + } + }) + ctx.lineTo(padding.left + chartWidth, padding.top + chartHeight) + ctx.lineTo(padding.left, padding.top + chartHeight) + ctx.closePath() + ctx.fillStyle = gradient + ctx.fill() + + // 绘制折线 + ctx.beginPath() + data.forEach((value, index) => { + const x = padding.left + (chartWidth / (data.length - 1)) * index + const y = padding.top + chartHeight - (value / maxValue) * chartHeight + if (index === 0) { + ctx.moveTo(x, y) + } else { + ctx.lineTo(x, y) + } + }) + ctx.strokeStyle = '#1677ff' + ctx.lineWidth = 2 + ctx.stroke() + + // 绘制数据点 + data.forEach((value, index) => { + const x = padding.left + (chartWidth / (data.length - 1)) * index + const y = padding.top + chartHeight - (value / maxValue) * chartHeight + ctx.beginPath() + ctx.arc(x, y, 3, 0, Math.PI * 2) + ctx.fillStyle = '#1677ff' + ctx.fill() + }) + + // X轴标签 + ctx.fillStyle = isDark ? 'rgba(255,255,255,0.45)' : 'rgba(0,0,0,0.45)' + ctx.font = '12px sans-serif' + ctx.textAlign = 'center' + for (let i = 0; i < 24; i += 4) { + const x = padding.left + (chartWidth / 23) * i + ctx.fillText(`${i}:00`, x, height - 10) + } + }, [data, loading, isDark]) + + if (loading) { + return ( +
    + +
    + ) + } + + return ( + + ) +} + +// 音源状态列表属性 +interface SourceListProps { + sources: SourceStatus[] + loading?: boolean +} + +// 音源状态列表组件 +function SourceList({ sources, loading }: SourceListProps) { + const { isDark } = useTheme() + + const getStatusTag = (status: string) => { + switch (status) { + case 'active': + return 正常 + case 'error': + return 异常 + case 'deprecated': + return 已弃用 + default: + return {status} + } + } + + return ( + 查看全部} + style={{ background: isDark ? '#1f1f1f' : '#fff' }} + loading={loading} + > + ( + + + {item.enabled ? ( + + ) : ( + + )} + + } + title={ +
    + {item.name} + {getStatusTag(item.status)} +
    + } + description={ + + + 成功率: {item.successRate.toFixed(1)}% + + + 响应: {item.avgResponseTime}ms + + + } + /> + 90 ? '#52c41a' : item.successRate > 70 ? '#faad14' : '#ff4d4f'} + showInfo={false} + /> +
    + )} + locale={{ emptyText: }} + /> +
    + ) +} + +// 平台统计属性 +interface PlatformStatsProps { + data: Partial> + loading?: boolean +} + +// 平台统计组件 +function PlatformStats({ data, loading }: PlatformStatsProps) { + const { isDark } = useTheme() + + const platforms = Object.entries(data) as [MusicPlatform, number][] + const total = platforms.reduce((sum, [, count]) => sum + count, 0) + + return ( + +
    + {platforms.map(([platform, count]) => { + const percent = total > 0 ? (count / total) * 100 : 0 + return ( +
    +
    + + {PLATFORM_ICONS[platform]} {PLATFORM_NAMES[platform]} + + {count} 次 +
    + +
    + ) + })} + {platforms.length === 0 && ( + + )} +
    +
    + ) +} + +// 快捷导航属性 +interface QuickNavProps { + items?: { key: string; title: string; icon: React.ReactNode; href?: string }[] +} + +// 快捷导航组件 +function QuickNav({ items }: QuickNavProps) { + const { isDark, currentTheme } = useTheme() + + const defaultItems = [ + { key: 'sources', title: '音源管理', icon: }, + { key: 'cardkeys', title: '卡密管理', icon: }, + { key: 'logs', title: '请求日志', icon: }, + { key: 'settings', title: '系统设置', icon: }, + ] + + const navItems = items || defaultItems + + return ( + + + {navItems.map((item) => ( +
    + +
    + {item.icon} +
    + {item.title} +
    + + ))} + + + ) +} + +// 仪表盘属性 +interface DashboardProps { + statistics?: Statistics + sources?: SourceStatus[] + serviceStatus?: 'running' | 'stopped' | 'maintenance' + loading?: boolean +} + +// 主仪表盘组件 +export default function Dashboard({ + statistics, + sources = [], + serviceStatus = 'running', + loading = false, +}: DashboardProps) { + const { isDark } = useTheme() + + // 模拟数据 + const mockStatistics: Statistics = statistics || { + totalRequests: 125847, + successRequests: 123456, + failedRequests: 2391, + successRate: 98.1, + avgResponseTime: 128, + requestsByPlatform: { + kw: 45000, + kg: 35000, + tx: 25000, + wy: 15000, + mg: 5847, + }, + requestsByQuality: { + '128k': 30000, + '320k': 45000, + 'flac': 35000, + 'hires': 10000, + 'dolby': 5000, + 'master': 847, + }, + requestsByHour: Array.from({ length: 24 }, (_, i) => + Math.floor(Math.random() * 5000) + 1000 + ), + } + + const mockSources: SourceStatus[] = sources.length > 0 ? sources : [ + { id: '1', name: '酷我音乐音源', enabled: true, status: 'active', successRate: 99.2, avgResponseTime: 98 }, + { id: '2', name: '酷狗音乐音源', enabled: true, status: 'active', successRate: 97.8, avgResponseTime: 125 }, + { id: '3', name: 'QQ音乐音源', enabled: true, status: 'active', successRate: 98.5, avgResponseTime: 110 }, + { id: '4', name: '网易云音乐音源', enabled: false, status: 'error', successRate: 45.2, avgResponseTime: 350 }, + { id: '5', name: '咪咕音乐音源', enabled: true, status: 'active', successRate: 96.1, avgResponseTime: 145 }, + ] + + return ( +
    + {/* 服务状态 */} + + + {/* 统计卡片 */} + +
    + } + trend={{ value: 12.5, isUp: true }} + loading={loading} + color="#1677ff" + /> + + + } + valueStyle={{ color: '#52c41a' }} + trend={{ value: 8.3, isUp: true }} + loading={loading} + color="#52c41a" + /> + + + } + valueStyle={{ color: '#ff4d4f' }} + trend={{ value: 5.2, isUp: false }} + loading={loading} + color="#ff4d4f" + /> + + + } + trend={{ value: 3.1, isUp: false }} + loading={loading} + color="#722ed1" + /> + + + + {/* 成功率进度 */} + +
    + 请求成功率 + 95 ? '#52c41a' : '#faad14' }}> + {mockStatistics.successRate.toFixed(1)}% + +
    + 95 ? '#52c41a' : mockStatistics.successRate > 80 ? '#faad14' : '#ff4d4f'} + trailColor={isDark ? '#303030' : '#f0f0f0'} + /> +
    + + {/* 图表区域 */} + +
    + + + + + + + + + + {/* 音源状态和快捷导航 */} + + + + + + + + + + ) +} + +// 导出子组件 +export { StatCard, ServiceStatusCard, RequestTrendChart, SourceList, PlatformStats, QuickNav } diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx new file mode 100644 index 0000000..d4b1eb7 --- /dev/null +++ b/src/components/Layout.tsx @@ -0,0 +1,445 @@ +/** + * HYW & Koneko 音源管理后台 + * + * 合作开发: MistAperio Studio × Macrohard Studio + * 编写人: 雾启工作室@云汀 (Cloudwhisper/Miao-moe), Macrohard Studio@Ryn磷熠 (Macrohard0001) + * + * 旧版后台由 Macrohard Studio 开发,编写人 @Macrohard0001 + */ + +'use client' + +import React, { useState, useEffect } from 'react' +import { Layout as AntLayout, Menu, Button, Breadcrumb, Dropdown, Avatar, Space, Badge, Tooltip, Typography, Switch } from 'antd' +import { + MenuFoldOutlined, + MenuUnfoldOutlined, + DashboardOutlined, + MusicOutlined, + SkinOutlined, + SettingOutlined, + UserOutlined, + BellOutlined, + LogoutOutlined, + QuestionCircleOutlined, + ApiOutlined, + SafetyOutlined, + FileTextOutlined, + BarChartOutlined, + HomeOutlined, +} from '@ant-design/icons' +import { useTheme } from './ThemeProvider' +import type { MenuProps } from 'antd' +import type { ReactNode } from 'react' + +const { Header, Sider, Content } = AntLayout +const { Text } = Typography + +// 导航菜单项类型 +interface NavigationItem { + key: string + label: string + icon: ReactNode + children?: NavigationItem[] +} + +// 布局属性 +interface LayoutProps { + children: ReactNode + activeKey?: string + breadcrumbs?: { title: string; href?: string }[] +} + +// 导航菜单配置 +const navigationItems: NavigationItem[] = [ + { + key: 'dashboard', + label: '仪表盘', + icon: , + }, + { + key: 'sources', + label: '音源管理', + icon: , + }, + { + key: 'cardkeys', + label: '卡密管理', + icon: , + }, + { + key: 'statistics', + label: '数据统计', + icon: , + }, + { + key: 'logs', + label: '请求日志', + icon: , + }, + { + key: 'themes', + label: '主题设置', + icon: , + }, + { + key: 'settings', + label: '系统设置', + icon: , + }, +] + +// 用户菜单项 +const userMenuItems: MenuProps['items'] = [ + { + key: 'profile', + label: '个人资料', + icon: , + }, + { + key: 'settings', + label: '账户设置', + icon: , + }, + { type: 'divider' }, + { + key: 'logout', + label: '退出登录', + icon: , + danger: true, + }, +] + +// 通知菜单项 +const notificationItems: MenuProps['items'] = [ + { + key: '1', + label: ( +
    +
    系统更新通知
    +
    + 系统已更新至 v3.0.0,新增多项功能 +
    +
    + ), + }, + { + key: '2', + label: ( +
    +
    音源状态变更
    +
    + 酷我音乐音源已恢复正常 +
    +
    + ), + }, + { type: 'divider' }, + { + key: 'viewAll', + label: 查看全部通知, + }, +] + +// 主布局组件 +export default function Layout({ children, activeKey = 'dashboard', breadcrumbs }: LayoutProps) { + // 侧边栏折叠状态 + const [collapsed, setCollapsed] = useState(false) + // 移动端菜单显示状态 + const [mobileMenuOpen, setMobileMenuOpen] = useState(false) + // 窗口宽度 + const [windowWidth, setWindowWidth] = useState(1200) + + // 主题相关 + const { isDark, toggleDarkMode, currentTheme } = useTheme() + + // 监听窗口大小变化 + useEffect(() => { + const handleResize = () => { + const width = window.innerWidth + setWindowWidth(width) + + // 移动端自动折叠侧边栏 + if (width < 768) { + setCollapsed(true) + } + } + + handleResize() + window.addEventListener('resize', handleResize) + return () => window.removeEventListener('resize', handleResize) + }, []) + + // 是否为移动端 + const isMobile = windowWidth < 768 + + // 切换侧边栏 + const toggleSider = () => { + if (isMobile) { + setMobileMenuOpen(!mobileMenuOpen) + } else { + setCollapsed(!collapsed) + } + } + + // 菜单点击处理 + const handleMenuClick: MenuProps['onClick'] = (e) => { + console.log('Menu clicked:', e.key) + // 这里可以添加路由跳转逻辑 + if (isMobile) { + setMobileMenuOpen(false) + } + } + + // 生成菜单项 + const menuItems: MenuProps['items'] = navigationItems.map(item => ({ + key: item.key, + icon: item.icon, + label: item.label, + })) + + // 默认面包屑 + const defaultBreadcrumbs = [ + { title: , href: '/' }, + { title: navigationItems.find(item => item.key === activeKey)?.label || '仪表盘' }, + ] + + // 合并面包屑 + const finalBreadcrumbs = breadcrumbs || defaultBreadcrumbs + + return ( + + {/* 侧边栏 */} + { + if (broken) { + setCollapsed(true) + } + }} + style={{ + overflow: 'auto', + height: '100vh', + position: 'fixed', + left: 0, + top: 0, + bottom: 0, + zIndex: 100, + transition: 'all 0.3s ease', + }} + width={240} + collapsedWidth={isMobile ? 0 : 80} + > + {/* Logo 区域 */} +
    + + {!collapsed && !isMobile && ( + + HYW & Koneko + + )} +
    + + {/* 导航菜单 */} + + + {/* 底部版本信息 */} + {!collapsed && !isMobile && ( +
    + + v3.0.0 + +
    + )} + + + {/* 移动端遮罩层 */} + {isMobile && mobileMenuOpen && ( +
    setMobileMenuOpen(false)} + style={{ + position: 'fixed', + top: 0, + left: 0, + right: 0, + bottom: 0, + backgroundColor: 'rgba(0, 0, 0, 0.45)', + zIndex: 99, + }} + /> + )} + + {/* 右侧内容区域 */} + + {/* 顶部栏 */} +
    + {/* 左侧 */} +
    + {/* 折叠按钮 */} +
    + + {/* 右侧工具栏 */} +
    + {/* 主题切换 */} + + + + + {/* 帮助 */} + +
    +
    + + {/* 主内容区 */} + + {children} + + + {/* 页脚 */} + + + HYW & Koneko 音源管理后台 ©{new Date().getFullYear()} +
    + 合作开发: MistAperio Studio × Macrohard Studio +
    +
    +
    + + ) +} + +// 导出布局组件 +export { Layout } diff --git a/src/components/SourceManager.tsx b/src/components/SourceManager.tsx new file mode 100644 index 0000000..c050e89 --- /dev/null +++ b/src/components/SourceManager.tsx @@ -0,0 +1,665 @@ +/** + * HYW & Koneko 音源管理后台 + * + * 合作开发: MistAperio Studio × Macrohard Studio + * 编写人: 雾启工作室@云汀 (Cloudwhisper/Miao-moe), Macrohard Studio@Ryn磷熠 (Macrohard0001) + * + * 旧版后台由 Macrohard Studio 开发,编写人 @Macrohard0001 + */ + +'use client' + +import React, { useState, useEffect } from 'react' +import { + Card, + Table, + Button, + Space, + Tag, + Switch, + Modal, + Form, + Input, + Select, + InputNumber, + Upload, + message, + Popconfirm, + Tooltip, + Typography, + Row, + Col, + Badge, + Drawer, + Descriptions, + Empty, + Spin, +} from 'antd' +import { + PlusOutlined, + EditOutlined, + DeleteOutlined, + UploadOutlined, + SearchOutlined, + ReloadOutlined, + CheckCircleOutlined, + CloseCircleOutlined, + ExclamationCircleOutlined, + FileTextOutlined, + DownloadOutlined, + EyeOutlined, + MoreOutlined, +} from '@ant-design/icons' +import { useTheme } from './ThemeProvider' +import type { SourceConfig, MusicPlatform, MusicQuality, ScriptFormat } from '@/types' +import { PLATFORM_NAMES, PLATFORM_ICONS, QUALITY_NAMES, FORMAT_NAMES } from '@/types' + +const { Title, Text, Paragraph } = Typography +const { Option } = Select +const { TextArea } = Input + +// 音源管理属性 +interface SourceManagerProps { + sources?: SourceConfig[] + loading?: boolean + onRefresh?: () => void + onAdd?: (source: Partial) => Promise + onUpdate?: (id: string, source: Partial) => Promise + onDelete?: (id: string) => Promise + onToggle?: (id: string, enabled: boolean) => Promise +} + +// 音源管理组件 +export default function SourceManager({ + sources = [], + loading = false, + onRefresh, + onAdd, + onUpdate, + onDelete, + onToggle, +}: SourceManagerProps) { + const { isDark } = useTheme() + + // 状态 + const [searchText, setSearchText] = useState('') + const [filteredSources, setFilteredSources] = useState(sources) + const [modalVisible, setModalVisible] = useState(false) + const [drawerVisible, setDrawerVisible] = useState(false) + const [editingSource, setEditingSource] = useState(null) + const [viewingSource, setViewingSource] = useState(null) + const [form] = Form.useForm() + const [submitting, setSubmitting] = useState(false) + + // 过滤音源列表 + useEffect(() => { + if (!searchText) { + setFilteredSources(sources) + return + } + + const filtered = sources.filter( + (source) => + source.name.toLowerCase().includes(searchText.toLowerCase()) || + source.author?.toLowerCase().includes(searchText.toLowerCase()) || + source.description?.toLowerCase().includes(searchText.toLowerCase()) + ) + setFilteredSources(filtered) + }, [sources, searchText]) + + // 打开新增/编辑弹窗 + const handleOpenModal = (source?: SourceConfig) => { + if (source) { + setEditingSource(source) + form.setFieldsValue({ + name: source.name, + description: source.description, + version: source.version, + author: source.author, + format: source.format, + platforms: source.platforms, + qualities: source.qualities, + priority: source.priority, + weight: source.weight, + homepage: source.homepage, + license: source.license, + tags: source.tags?.join(', '), + }) + } else { + setEditingSource(null) + form.resetFields() + form.setFieldsValue({ + format: 'lx', + priority: 0, + weight: 100, + }) + } + setModalVisible(true) + } + + // 关闭弹窗 + const handleCloseModal = () => { + setModalVisible(false) + setEditingSource(null) + form.resetFields() + } + + // 提交表单 + const handleSubmit = async () => { + try { + const values = await form.validateFields() + setSubmitting(true) + + const sourceData: Partial = { + ...values, + tags: values.tags?.split(',').map((t: string) => t.trim()).filter(Boolean), + } + + if (editingSource) { + await onUpdate?.(editingSource.id, sourceData) + message.success('音源更新成功') + } else { + await onAdd?.(sourceData) + message.success('音源添加成功') + } + + handleCloseModal() + } catch (error) { + console.error('Form validation failed:', error) + } finally { + setSubmitting(false) + } + } + + // 删除音源 + const handleDelete = async (id: string) => { + try { + await onDelete?.(id) + message.success('音源删除成功') + } catch (error) { + message.error('删除失败') + } + } + + // 切换启用状态 + const handleToggle = async (id: string, enabled: boolean) => { + try { + await onToggle?.(id, enabled) + message.success(enabled ? '音源已启用' : '音源已禁用') + } catch (error) { + message.error('操作失败') + } + } + + // 查看详情 + const handleViewDetails = (source: SourceConfig) => { + setViewingSource(source) + setDrawerVisible(true) + } + + // 获取状态标签 + const getStatusTag = (status: string) => { + switch (status) { + case 'active': + return }>正常 + case 'error': + return }>异常 + case 'deprecated': + return }>已弃用 + default: + return {status} + } + } + + // 表格列定义 + const columns = [ + { + title: '音源名称', + dataIndex: 'name', + key: 'name', + width: 200, + render: (name: string, record: SourceConfig) => ( + + {name} + {record.version && v{record.version}} + + ), + }, + { + title: '格式', + dataIndex: 'format', + key: 'format', + width: 100, + render: (format: ScriptFormat) => FORMAT_NAMES[format] || format, + }, + { + title: '支持平台', + dataIndex: 'platforms', + key: 'platforms', + width: 200, + render: (platforms: MusicPlatform[]) => ( + + {platforms?.map((p) => ( + + {PLATFORM_ICONS[p]} {PLATFORM_NAMES[p]} + + ))} + + ), + }, + { + title: '支持音质', + dataIndex: 'qualities', + key: 'qualities', + width: 200, + render: (qualities: MusicQuality[]) => ( + QUALITY_NAMES[q]).join(', ')}> + + {qualities?.slice(0, 3).map((q) => QUALITY_NAMES[q]).join(', ')} + {qualities?.length > 3 && ` +${qualities.length - 3}`} + + + ), + }, + { + title: '状态', + dataIndex: 'status', + key: 'status', + width: 100, + render: (status: string) => getStatusTag(status), + }, + { + title: '成功率', + dataIndex: 'successRate', + key: 'successRate', + width: 100, + render: (_: unknown, record: SourceConfig) => { + const rate = record.successCount / (record.successCount + record.failCount) * 100 || 0 + return ( + 90 ? '#52c41a' : rate > 70 ? '#faad14' : '#ff4d4f' }}> + {rate.toFixed(1)}% + + ) + }, + }, + { + title: '响应时间', + dataIndex: 'avgResponseTime', + key: 'avgResponseTime', + width: 100, + render: (time: number) => `${time || 0}ms`, + }, + { + title: '启用', + dataIndex: 'enabled', + key: 'enabled', + width: 80, + render: (enabled: boolean, record: SourceConfig) => ( + handleToggle(record.id, checked)} + size="small" + /> + ), + }, + { + title: '操作', + key: 'action', + width: 150, + fixed: 'right' as const, + render: (_: unknown, record: SourceConfig) => ( + + +
    + } + value={searchText} + onChange={(e) => setSearchText(e.target.value)} + style={{ maxWidth: 400 }} + allowClear + /> + + + + + + + + + + + {/* 音源列表 */} + +
    0 ? filteredSources : mockSources} + rowKey="id" + loading={loading} + scroll={{ x: 1200 }} + pagination={{ + showSizeChanger: true, + showQuickJumper: true, + showTotal: (total) => `共 ${total} 条`, + defaultPageSize: 10, + }} + locale={{ + emptyText: , + }} + /> + + + {/* 新增/编辑弹窗 */} + +
    + +
    + + + + + + + + + + + + +