chore: initial commit - maqt-desktop v0.2
- Phase 1-5: UI framework, auth, weapon schemes, color filters, system optimization - Industrial/tech design style with Chinese localization - Points to gch3n.online/delta backend API - Hardware monitor, filter editor, and all module pages
This commit is contained in:
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.env
|
||||
*.log
|
||||
.DS_Store
|
||||
__pycache__/
|
||||
*.pyc
|
||||
663
DEVELOPMENT_PLAN.md
Normal file
663
DEVELOPMENT_PLAN.md
Normal file
@@ -0,0 +1,663 @@
|
||||
# 码枪堂桌面端复刻 — 全面开发规划
|
||||
|
||||
> 基于 MaQiangTang v7.0.4 完整逆向分析
|
||||
> 后端: https://gch3n.online/delta | 源码: https://git.gch3n.online/gch3n/mqsrv
|
||||
|
||||
---
|
||||
|
||||
## 一、原版功能全景图
|
||||
|
||||
### 1.1 页面结构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 桌面首页 (Desktop/Home) │
|
||||
│ ┌────────────────┐ ┌────────────────┐ │
|
||||
│ │ 快速优化 │ │ 画面滤镜 │ │
|
||||
│ │ (一键优化) │ │ (Exposure) │ │
|
||||
│ └────────────────┘ └────────────────┘ │
|
||||
│ ┌────────────────┐ ┌────────────────┐ │
|
||||
│ │ 滤镜社区 │ │ 改枪方案 │ │
|
||||
│ │ (Filter-Share) │ │ (Weapon) │ │
|
||||
│ └────────────────┘ └────────────────┘ │
|
||||
│ ┌────────────────┐ ┌────────────────┐ │
|
||||
│ │ 游戏准星 │ │ 神秘力量 │ │
|
||||
│ │ (Crosshair) │ │ (Xixi-Haha) │ │
|
||||
│ └────────────────┘ └────────────────┘ │
|
||||
│ ┌────────────────┐ │
|
||||
│ │ 嘉豪之力 │ │
|
||||
│ │ (Forbidden) │ │
|
||||
│ └────────────────┘ │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────────────────┐ │
|
||||
│ │ 底部 Dock: 社区教程 | 微信 | 官网 │ │
|
||||
│ └──────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌── 底部启动按钮 ──────────────────────────────────┐ │
|
||||
│ │ [ 启动硬件监控 ] [ 启动码枪堂2.0 (XiXiOverlay) ] │ │
|
||||
│ └──────────────────────────────────────────────────┘ │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ 底部 Dock 导航栏 (离开首页时显示) │
|
||||
│ [快速优化] [画面滤镜] [滤镜社区] [改枪方案] [设置] │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 1.2 完整页面/功能清单
|
||||
|
||||
| # | 页面 ID | 名称 | 描述 |
|
||||
|---|---------|------|------|
|
||||
| 1 | `home` | 桌面首页 | 桌面图标、启动按钮、社区入口 |
|
||||
| 2 | `one-click-optimization` | 快速优化 | 一键系统优化(VIP) |
|
||||
| 3 | `exposure` | 画面滤镜 | 显示/色彩滤镜管理(VIP) |
|
||||
| 4 | `filter-community` | 滤镜社区 | 分享和下载滤镜(VIP) |
|
||||
| 5 | `weapon` | 改枪方案 | 武器配置方案浏览/分享 |
|
||||
| 6 | `game-crosshair` | 游戏准星 | 准星定制(需登录) |
|
||||
| 7 | `xixi-haha` | 神秘力量 | 趣味功能(需登录) |
|
||||
| 8 | `forbidden-force` | 嘉豪之力 | 高级功能(VIP) |
|
||||
| 9 | `settings` | 设置页 | 应用偏好、开机自启等 |
|
||||
|
||||
### 1.3 原生服务清单(Services)
|
||||
|
||||
| 服务 | 功能 | 复用方式 |
|
||||
|------|------|----------|
|
||||
| **SystemOptimizationService** | 系统优化项执行/恢复 | IPC spawn MaqiangTangh1.exe |
|
||||
| **XiXiOverlayNativeService** | 游戏内 Overlay 浮窗 | IPC spawn MaqiangTangXiXiOverlay.exe |
|
||||
| **HardwareMonitorService** | 硬件监控(CPU/GPU 温度) | IPC spawn MaqiangTangHardwareMonitor.exe |
|
||||
| **ArgyllService** | 色彩校准(ICC 配置文件) | 调用 dispwin.exe / iccvcgt.exe |
|
||||
| **NvidiaProfileService** | NVIDIA 驱动配置 | 调用 nvidiaProfileInspector.exe |
|
||||
| **NipConfigService** | NIP 配置文件管理 | 读写 .nip 文件 |
|
||||
| **CustomFilterIccService** | 自定义滤镜 ICC | 色彩配置文件管理 |
|
||||
| **PowerManagementService** | 电源管理 | 调用 PowerShell 系统命令 |
|
||||
| **ResolutionService** | 分辨率切换 | 调用 DVChange.exe |
|
||||
| **GpuDeviceService** | GPU 设备信息 | 系统 API 查询 |
|
||||
| **SystemInfoService** | 系统信息收集 | 系统 API 查询 |
|
||||
| **CompareFitService** | 方案比对 | 算法比对 |
|
||||
| **DlssNativeService** | DLSS 配置管理 | 驱动层 DLSS 设置 |
|
||||
| **ShaderCacheService** | 着色器缓存管理 | 缓存文件管理 |
|
||||
| **NvidiaAppService** | NVIDIA App 集成 | 系统 API |
|
||||
| **PermissionChecker** | 权限检查 | 管理员权限检测 |
|
||||
| **MemoryService** | 内存优化 | 系统 API |
|
||||
| **ProcessService** | 进程管理 | 系统 API |
|
||||
| **RegistryService** | 注册表操作 | 系统 API |
|
||||
| **WindowsFeaturesService** | Windows 功能开关 | PowerShell |
|
||||
| **WindowsServiceManager** | 系统服务管理 | PowerShell |
|
||||
| **SecurityMonitorService** | 安全监控 | 系统 API |
|
||||
|
||||
---
|
||||
|
||||
## 二、项目文件结构
|
||||
|
||||
```
|
||||
maqt-desktop/
|
||||
├── docs/ # 开发文档
|
||||
│ ├── ARCHITECTURE.md # 架构说明
|
||||
│ ├── API_REFERENCE.md # API 接口文档
|
||||
│ ├── UI_WIREFRAMES.md # 界面线框图
|
||||
│ └── DEVELOPMENT_LOG.md # 开发日志
|
||||
│
|
||||
├── electron/ # Electron 主进程
|
||||
│ ├── main.ts # 窗口管理、生命周期
|
||||
│ ├── preload.ts # 安全 IPC 桥
|
||||
│ ├── menu.ts # 菜单配置
|
||||
│ ├── updater.ts # 自动更新
|
||||
│ │
|
||||
│ ├── ipc/ # IPC 处理器
|
||||
│ │ ├── auth.ipc.ts # 认证相关
|
||||
│ │ ├── system.ipc.ts # 系统级操作
|
||||
│ │ ├── overlay.ipc.ts # XiXiOverlay 控制
|
||||
│ │ ├── optimization.ipc.ts # 系统优化
|
||||
│ │ ├── profile.ipc.ts # NVIDIA 配置
|
||||
│ │ ├── display.ipc.ts # 显示/色彩
|
||||
│ │ └── file.ipc.ts # 文件操作
|
||||
│ │
|
||||
│ ├── services/ # 原生服务包装
|
||||
│ │ ├── SystemOptimizer.ts # 调用 MaqiangTangh1.exe
|
||||
│ │ ├── XiXiOverlayLauncher.ts # 调用 XiXiOverlay.exe
|
||||
│ │ ├── HardwareMonitor.ts # 调用 HardwareMonitor.exe
|
||||
│ │ ├── NvidiaProfile.ts # 调用 nvidiaProfileInspector.exe
|
||||
│ │ ├── ColorCalibration.ts # 调用 Argyll CMS
|
||||
│ │ ├── PowerManager.ts # 电源方案管理
|
||||
│ │ └── ResolutionChanger.ts # 分辨率切换调用 DVChange.exe
|
||||
│ │
|
||||
│ └── utils/ # 主进程工具
|
||||
│ ├── logger.ts # 日志
|
||||
│ ├── permissions.ts # 提权
|
||||
│ └── paths.ts # 路径管理
|
||||
│
|
||||
├── src/ # React 前端
|
||||
│ ├── main.tsx # 入口
|
||||
│ ├── App.tsx # 根组件 + 路由
|
||||
│ ├── router.tsx # 页面路由
|
||||
│ │
|
||||
│ ├── pages/ # 页面组件
|
||||
│ │ ├── Home.tsx # 桌面首页
|
||||
│ │ ├── Login.tsx # 登录页
|
||||
│ │ ├── Register.tsx # 注册页
|
||||
│ │ ├── Optimization.tsx # 快速优化
|
||||
│ │ ├── Exposure.tsx # 画面滤镜
|
||||
│ │ ├── FilterCommunity.tsx # 滤镜社区
|
||||
│ │ ├── WeaponSchemes.tsx # 改枪方案
|
||||
│ │ ├── Crosshair.tsx # 游戏准星
|
||||
│ │ ├── XixiHaha.tsx # 神秘力量
|
||||
│ │ ├── ForbiddenForce.tsx # 嘉豪之力
|
||||
│ │ ├── SchemeDetail.tsx # 方案详情
|
||||
│ │ └── Settings.tsx # 设置页
|
||||
│ │
|
||||
│ ├── components/ # 通用组件
|
||||
│ │ ├── ui/ # UI 基础组件
|
||||
│ │ │ ├── Button.tsx # 直角切角按钮
|
||||
│ │ │ ├── Card.tsx # 模块化卡片
|
||||
│ │ │ ├── ProgressBar.tsx # 机械进度条
|
||||
│ │ │ ├── Panel.tsx # 半透明悬浮面板
|
||||
│ │ │ ├── Modal.tsx # 弹窗
|
||||
│ │ │ ├── Toast.tsx # 消息提示
|
||||
│ │ │ ├── Badge.tsx # 角标
|
||||
│ │ │ ├── Compass.tsx # 罗盘刻度装饰
|
||||
│ │ │ ├── Input.tsx # 输入框
|
||||
│ │ │ ├── Select.tsx # 下拉选择
|
||||
│ │ │ └── Skeleton.tsx # 加载骨架屏
|
||||
│ │ │
|
||||
│ │ ├── layout/ # 布局组件
|
||||
│ │ │ ├── DesktopGrid.tsx # 桌面图标网格
|
||||
│ │ │ ├── DesktopIcon.tsx # 桌面图标
|
||||
│ │ │ ├── BottomDock.tsx # 底部导航 Dock
|
||||
│ │ │ ├── TopBar.tsx # 顶部状态栏
|
||||
│ │ │ ├── SidePanel.tsx # 侧边面板
|
||||
│ │ │ └── PageContainer.tsx # 页面容器
|
||||
│ │ │
|
||||
│ │ ├── auth/ # 认证相关
|
||||
│ │ │ ├── LoginModal.tsx # 登录弹窗
|
||||
│ │ │ ├── VipBadge.tsx # VIP 标识
|
||||
│ │ │ └── VipActivate.tsx # VIP 激活
|
||||
│ │ │
|
||||
│ │ ├── hardware/ # 硬件监控
|
||||
│ │ │ ├── MonitorPanel.tsx # 监控面板
|
||||
│ │ │ ├── CpuGauge.tsx # CPU 仪表盘
|
||||
│ │ │ ├── GpuGauge.tsx # GPU 仪表盘
|
||||
│ │ │ └── TempIndicator.tsx # 温度指示
|
||||
│ │ │
|
||||
│ │ ├── schemes/ # 改枪方案
|
||||
│ │ │ ├── SchemeCard.tsx # 方案卡片
|
||||
│ │ │ ├── SchemeList.tsx # 方案列表
|
||||
│ │ │ ├── SchemeFilter.tsx # 方案筛选
|
||||
│ │ │ ├── SchemePreviewer.tsx # 方案预览
|
||||
│ │ │ └── SchemeEditor.tsx # 方案编辑
|
||||
│ │ │
|
||||
│ │ ├── filters/ # 画面滤镜
|
||||
│ │ │ ├── FilterCard.tsx # 滤镜卡片
|
||||
│ │ │ ├── FilterGrid.tsx # 滤镜网格
|
||||
│ │ │ ├── FilterPreview.tsx # 滤镜预览
|
||||
│ │ │ └── FilterEditor.tsx # 滤镜编辑
|
||||
│ │ │
|
||||
│ │ └── optimization/ # 系统优化
|
||||
│ │ ├── OptimizeItem.tsx # 优化项
|
||||
│ │ ├── OptimizePanel.tsx # 优化面板
|
||||
│ │ └── OptimizeResult.tsx # 优化结果
|
||||
│ │
|
||||
│ ├── hooks/ # 自定义 Hooks
|
||||
│ │ ├── useAuth.ts # 登录态
|
||||
│ │ ├── useApi.ts # API 请求
|
||||
│ │ ├── useVip.ts # VIP 状态
|
||||
│ │ ├── useElectron.ts # Electron API
|
||||
│ │ ├── useHardwareMonitor.ts # 硬件监控
|
||||
│ │ ├── useLocalStorage.ts # 本地存储
|
||||
│ │ └── useTheme.ts # 主题
|
||||
│ │
|
||||
│ ├── stores/ # 状态管理
|
||||
│ │ ├── authStore.ts # 认证
|
||||
│ │ ├── vipStore.ts # VIP
|
||||
│ │ ├── navigationStore.ts # 导航
|
||||
│ │ └── settingsStore.ts # 设置
|
||||
│ │
|
||||
│ ├── services/ # API 服务
|
||||
│ │ ├── api.ts # Axios 实例
|
||||
│ │ ├── auth.api.ts # 认证接口
|
||||
│ │ ├── schemes.api.ts # 方案接口
|
||||
│ │ ├── filters.api.ts # 滤镜接口
|
||||
│ │ ├── favorites.api.ts # 收藏接口
|
||||
│ │ ├── user.api.ts # 用户接口
|
||||
│ │ └── popups.api.ts # 广告接口
|
||||
│ │
|
||||
│ └── styles/ # 样式
|
||||
│ ├── globals.css # 全局样式
|
||||
│ ├── theme.ts # 主题变量
|
||||
│ ├── tailwind.ts # Tailwind 配置
|
||||
│ └── animations.css # 动画
|
||||
│
|
||||
├── native/ # 原生可执行文件
|
||||
│ ├── MaqiangTangh1.exe
|
||||
│ ├── MaqiangTangh2.exe
|
||||
│ ├── MaqiangTangHardwareMonitor.exe
|
||||
│ ├── nvidiaProfileInspector.exe
|
||||
│ ├── tools/
|
||||
│ │ ├── xixi-overlay-helper/
|
||||
│ │ │ └── MaqiangTangXiXiOverlay.exe
|
||||
│ │ ├── vibrance-bridge/
|
||||
│ │ │ ├── VibranceBridge.exe
|
||||
│ │ │ └── vibranceDLL.dll
|
||||
│ │ ├── DVChange.exe
|
||||
│ │ ├── gsynctoggle.exe
|
||||
│ │ └── dbInstaller.exe
|
||||
│ ├── Argyll/
|
||||
│ │ └── bin/
|
||||
│ │ ├── iccvcgt.exe
|
||||
│ │ └── dispwin.exe
|
||||
│ ├── icc-profiles/
|
||||
│ └── scripts/
|
||||
│ ├── apply-gamma-force.ps1
|
||||
│ └── restore-gamma-force.ps1
|
||||
│
|
||||
├── package.json
|
||||
├── electron-builder.yml # 打包配置
|
||||
├── tsconfig.json
|
||||
├── vite.config.ts # Vite 配置
|
||||
├── tailwind.config.ts
|
||||
├── postcss.config.js
|
||||
├── Dockerfile
|
||||
└── docker-compose.yml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、UI 设计规范
|
||||
|
||||
### 3.1 设计语言
|
||||
|
||||
```
|
||||
军工战术风 · 单兵头盔HUD · 暗区射击手游UI
|
||||
|
||||
配色方案:
|
||||
████ #0A0E17 炭黑(主背景)
|
||||
████ #1A1F2E 深碳灰(卡片/面板)
|
||||
████ #2D3349 铁灰(边框/分隔)
|
||||
████ #3B4A3B 军绿(强调色/按钮)
|
||||
████ #5C7A3E 橄榄绿(次要强调)
|
||||
████ #3A6B35 暗军绿(激活态)
|
||||
████ #4A3B22 泥土棕(装饰/辅助)
|
||||
████ #7A6B4A 暗金(VIP/高级元素)
|
||||
████ #C8A95B 淡金(VIP 高亮)
|
||||
████ #E8E0D0 米白(主文字)
|
||||
████ #A09888 灰白(次要文字)
|
||||
████ #5A5248 深灰(禁用文字)
|
||||
████ #3B2B1A 暗琥珀(警告/危险)
|
||||
|
||||
字体:
|
||||
- 主字体: "JetBrains Mono" (monospace, 军事终端感)
|
||||
- 备用: "Cascadia Code", "Fira Code", monospace
|
||||
|
||||
字号阶梯:
|
||||
10px 罗盘刻度标签
|
||||
12px 辅助信息
|
||||
14px 正文
|
||||
16px 突出文字
|
||||
18px 小标题
|
||||
24px 页面标题
|
||||
36px 大数字(监控数据)
|
||||
```
|
||||
|
||||
### 3.2 组件风格
|
||||
|
||||
```css
|
||||
/* 按钮: 直角切角 + 磨砂金属 */
|
||||
.btn-tactical {
|
||||
border: 1px solid rgba(59, 74, 59, 0.6);
|
||||
background: linear-gradient(180deg,
|
||||
rgba(26, 31, 46, 0.95) 0%,
|
||||
rgba(15, 20, 30, 0.98) 100%);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(200, 169, 91, 0.08),
|
||||
inset 0 -1px 0 rgba(0, 0, 0, 0.3);
|
||||
clip-path: polygon(
|
||||
4px 0, calc(100% - 4px) 0,
|
||||
100% 4px, 100% calc(100% - 4px),
|
||||
calc(100% - 4px) 100%, 4px 100%,
|
||||
0 calc(100% - 4px), 0 4px
|
||||
);
|
||||
}
|
||||
|
||||
/* 面板: 半透明悬浮 */
|
||||
.panel-hud {
|
||||
backdrop-filter: blur(12px);
|
||||
background: rgba(15, 20, 30, 0.75);
|
||||
border: 1px solid rgba(59, 74, 59, 0.3);
|
||||
box-shadow:
|
||||
0 4px 24px rgba(0, 0, 0, 0.4),
|
||||
inset 0 1px 0 rgba(92, 122, 62, 0.1);
|
||||
}
|
||||
|
||||
/* 进度条: 机械风格 */
|
||||
.progress-mechanical {
|
||||
height: 6px;
|
||||
background: rgba(26, 31, 46, 0.9);
|
||||
border: 1px solid rgba(59, 74, 59, 0.4);
|
||||
position: relative;
|
||||
}
|
||||
.progress-mechanical::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0; left: 0;
|
||||
height: 100%;
|
||||
width: var(--progress);
|
||||
background: linear-gradient(90deg,
|
||||
#3A6B35 0%, #5C7A3E 50%, #7A6B4A 100%);
|
||||
/* 添加扫描线纹理 */
|
||||
background-image: repeating-linear-gradient(
|
||||
90deg,
|
||||
transparent 0px,
|
||||
rgba(255,255,255,0.03) 2px,
|
||||
transparent 4px
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 动画效果
|
||||
|
||||
```
|
||||
按钮悬停: 边框微光流动 (border-glow animation)
|
||||
面板展开: HUD 扫描线展开效果 (scan-line reveal)
|
||||
页面切换: 十字准星聚合展开 (crosshair transition)
|
||||
数据更新: 数字滚动跳变 (digital roll)
|
||||
加载中: 扫描雷达动画 (radar scan spinner)
|
||||
错误提示: 红色警告闪烁 + 抖动 (shaker + flash)
|
||||
VIP 元素: 暗金流光 (gold shimmer flow)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、页面线框图
|
||||
|
||||
### 4.1 桌面首页 (Home.tsx)
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│ ████████████████████████████████████████████████████████ │ ← 顶部 HUD 条
|
||||
│ 10:45 │ FPS: 144 │ CPU: 52° │ GPU: 68° │ VIP ● │ 网络 ✓ │ (罗盘刻度装饰)
|
||||
│ ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ │
|
||||
│ │
|
||||
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
|
||||
│ │ ⚡ 快速 │ │ 🎨 画面 │ │ 📸 滤镜 │ │ 🔫 改枪 │ │ ← 桌面图标 2x3 网格
|
||||
│ │ 优化 │ │ 滤镜 │ │ 社区 │ │ 方案 │ │
|
||||
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
|
||||
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
|
||||
│ │ 🎯 游戏 │ │ 💫 神秘 │ │ 🛡️ 嘉豪 │ │
|
||||
│ │ 准星 │ │ 力量 │ │ 之力 │ │
|
||||
│ └─────────┘ └─────────┘ └─────────┘ │
|
||||
│ │
|
||||
│ ┌────────────────────────────────────────────────────┐ │
|
||||
│ │ 教程 │ 微信群 │ 官网 │ │ ← 社区快捷栏
|
||||
│ └────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌──────────────────────┐ ┌──────────────────────┐ │
|
||||
│ │ ▨ 启动硬件监控 │ │ ▣ 启动码枪堂2.0 │ │ ← 启动按钮
|
||||
│ │ [温度/频率监测] │ │ [游戏内Overlay] │ │
|
||||
│ └──────────────────────┘ └──────────────────────┘ │
|
||||
└──────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 4.2 改枪方案页 (WeaponSchemes.tsx)
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│ ◀ 返回 改枪方案 🔍 搜索 │ ← TopBar
|
||||
├──────────────────────────────────────────────────────────┤
|
||||
│ ┌──────────┐ ┌──────┐ ┌───┐ ┌──┐ ┌──┐ ┌───┐ ┌───┐ │
|
||||
│ │ 突击步枪 │ │冲锋枪│ │狙│ │轻│ │霰│ │手 │ │发 │ │ ← 武器分类标签
|
||||
│ │ AR │ │ SMG │ │SR│ │MG│ │SG│ │枪 │ │射 │ │
|
||||
│ └──────────┘ └──────┘ └───┘ └──┘ └──┘ └───┘ └───┘ │
|
||||
│ │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ 武器: XXXX │ │ 武器: XXXX │ │ 武器: XXXX │ │ ← 方案卡片网格
|
||||
│ │ 作者: xxx │ │ 作者: xxx │ │ 作者: xxx │ │
|
||||
│ │ ⭐ 4.8 142🔥│ │ ⭐ 4.5 89🔥 │ │ ⭐ 4.9 203🔥│ │
|
||||
│ │ 效果预览... │ │ 效果预览... │ │ 效果预览... │ │
|
||||
│ │ ┌──────┐ │ │ ┌──────┐ │ │ ┌──────┐ │ │
|
||||
│ │ │ 收藏 │ │ │ │ 收藏 │ │ │ │ 收藏 │ │ │
|
||||
│ │ └──────┘ │ │ └──────┘ │ │ └──────┘ │ │
|
||||
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
||||
│ │
|
||||
│ ← 1 2 3 4 → │ ← 分页
|
||||
├──────────────────────────────────────────────────────────┤
|
||||
│ [快速优化] [画面滤镜] [滤镜社区] [改枪方案] [设置] │ ← BottomDock
|
||||
└──────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 4.3 登录/注册 (Login.tsx)
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ ═══ 认证 ═══ │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────┐ │
|
||||
│ │ ┌────────────────────────────────┐ │ │
|
||||
│ │ │ 用户名/邮箱 │ │ │ ← 磨砂输入框
|
||||
│ │ └────────────────────────────────┘ │ │
|
||||
│ │ ┌────────────────────────────────┐ │ │
|
||||
│ │ │ 密码 │ │ │
|
||||
│ │ └────────────────────────────────┘ │ │
|
||||
│ │ │ │
|
||||
│ │ ┌────────────────────────────────┐ │ │
|
||||
│ │ │ █ 登 录 │ │ │ ← 军绿按钮
|
||||
│ │ └────────────────────────────────┘ │ │
|
||||
│ │ │ │
|
||||
│ │ ═══ 或 ═══ │ │
|
||||
│ │ │ │
|
||||
│ │ ┌────────────────────────────────┐ │ │
|
||||
│ │ │ 创建账号 │ │ │
|
||||
│ │ └────────────────────────────────┘ │ │
|
||||
│ │ │ │
|
||||
│ │ ────── VIP 卡密激活 ────── │ │
|
||||
│ │ ┌────────────────────────────────┐ │ │
|
||||
│ │ │ 输入卡密... │ │ │
|
||||
│ │ └────────────────────────────────┘ │ │
|
||||
│ │ ┌────────────────────────────────┐ │ │
|
||||
│ │ │ 激 活 │ │ │
|
||||
│ │ └────────────────────────────────┘ │ │
|
||||
│ └──────────────────────────────────────┘ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 4.4 硬件监控 (MonitorPanel)
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│ ░ 硬 件 监 控 ░ │
|
||||
│ │
|
||||
│ ┌────────────┐ ┌────────────┐ │
|
||||
│ │ ▓ CPU │ │ ▓ GPU │ │ ← 圆形仪表盘
|
||||
│ │ 52°C │ │ 68°C │ │
|
||||
│ │ ┌──┐ │ │ ┌──┐ │ │
|
||||
│ │ │52│ │ │ │68│ │ │
|
||||
│ │ └──┘ │ │ └──┘ │ │
|
||||
│ │ 2.4GHz │ │ 1.8GHz │ │
|
||||
│ └────────────┘ └────────────┘ │
|
||||
│ │
|
||||
│ ▓ 内存: ████████░░░░ 72% │ ← 机械进度条
|
||||
│ ▓ FPS: ████████████░ 144 │
|
||||
│ ▓ 网络: ████████████ ✓ │
|
||||
│ │
|
||||
│ [ 刷新 ] [ 最小化到托盘 ] │
|
||||
└──────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、功能操作逻辑地图
|
||||
|
||||
### 5.1 应用启动流程
|
||||
|
||||
```
|
||||
应用启动
|
||||
↓
|
||||
加载配置 (settingsStore)
|
||||
↓
|
||||
检查登录态 (localStorage token)
|
||||
├── 有 token → 验证 session-status
|
||||
│ ├── 有效 → 进入首页
|
||||
│ └── 无效 → 清除 token → 显示首页 + 登录提示
|
||||
└── 无 token → 显示首页 + 登录提示
|
||||
↓
|
||||
启动硬件监控 (后台)
|
||||
↓
|
||||
检查更新
|
||||
↓
|
||||
渲染桌面首页
|
||||
```
|
||||
|
||||
### 5.2 桌面图标交互
|
||||
|
||||
```
|
||||
点击桌面图标
|
||||
↓
|
||||
检查权限要求(选项:loginRequired / vipRequired)
|
||||
├── 需要 VIP 但非 VIP → 弹出 VIP 激活窗口
|
||||
├── 需要登录但未登录 → 弹出登录弹窗
|
||||
└── 权限通过 → 导航到目标页面
|
||||
↓
|
||||
隐藏桌面图标区域 → 显示页面内容
|
||||
↓
|
||||
底部 Dock 出现(包含导航按钮)
|
||||
```
|
||||
|
||||
### 5.3 XiXi Overlay 启动
|
||||
|
||||
```
|
||||
点击「启动码枪堂2.0」
|
||||
↓
|
||||
检查 XiXiOverlayNativeService.isAvailable()
|
||||
├── false → 提示"未找到 overlay 程序"
|
||||
└── true → 继续
|
||||
↓
|
||||
ensureProcess() → spawn MaqiangTangXiXiOverlay.exe
|
||||
├── spawn 失败 → 提示错误
|
||||
└── spawn 成功 → 等待 stdout "ready"
|
||||
↓
|
||||
发送 show 命令 (包含 label/gameId)
|
||||
↓
|
||||
等待 "ready" 信号 → overlay 显示
|
||||
```
|
||||
|
||||
### 5.4 改枪方案浏览
|
||||
|
||||
```
|
||||
进入改枪方案页
|
||||
↓
|
||||
加载武器分类 (GET /api/category/{type})
|
||||
↓
|
||||
默认选中 AR → 加载热榜方案 (GET /api/schemes?sort=hot)
|
||||
↓
|
||||
点击分类标签 → 切换武器类型 → 重新加载方案
|
||||
↓
|
||||
点击方案卡片 → 进入方案详情页
|
||||
│
|
||||
├── 收藏按钮 → POST /api/favorites (需登录)
|
||||
├── 使用按钮 → POST /api/schemes/{id}/use
|
||||
├── 预览模式 → 弹窗预览效果
|
||||
└── 分享 → 复制分享链接
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、API 接口清单
|
||||
|
||||
详见 `API_FULL_ANALYSIS.md`,关键端点:
|
||||
|
||||
```
|
||||
基础: POST /api/login
|
||||
POST /api/register
|
||||
GET /api/session-status
|
||||
GET /api/vip-status
|
||||
POST /api/activate-vip
|
||||
|
||||
用户: GET /api/user/stats/:id
|
||||
GET /api/user/limits/:id
|
||||
GET /api/user/schemes/:id
|
||||
GET /api/user/favorited-count/:id
|
||||
|
||||
方案: GET /api/schemes?sort=&page=&limit=&source=
|
||||
GET /api/schemes/:id
|
||||
POST /api/schemes/:id/use
|
||||
GET /api/category/:code
|
||||
|
||||
收藏: GET /api/favorites/count
|
||||
GET /api/favorites/check?schemeId=
|
||||
POST /api/favorites
|
||||
DELETE /api/favorites/:id
|
||||
|
||||
工具: GET /api/software-version-ad
|
||||
GET /api/popups/:id
|
||||
GET /api/aftersale-tutorial-popup
|
||||
GET /api/update-config.json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、开发阶段
|
||||
|
||||
### Phase 1: 脚手架 + UI 框架
|
||||
- [ ] Vite + Electron + React + Tailwind 脚手架
|
||||
- [ ] 主题系统(军武风格变量)
|
||||
- [ ] 路由 + 页面骨架
|
||||
- [ ] 桌面首页 + 桌面图标
|
||||
- [ ] 底部 Dock 导航
|
||||
- [ ] 顶部 HUD 状态栏
|
||||
- [ ] UI 组件库(按钮/卡片/面板/进度条)
|
||||
|
||||
### Phase 2: 认证 + API 对接
|
||||
- [ ] Axios 实例 + 拦截器
|
||||
- [ ] 登录/注册页面
|
||||
- [ ] VIP 激活
|
||||
- [ ] Token 管理 + 自动刷新
|
||||
- [ ] 各页面数据对接
|
||||
|
||||
### Phase 3: 改枪方案模块
|
||||
- [ ] 武器分类标签
|
||||
- [ ] 方案列表 + 分页
|
||||
- [ ] 方案详情
|
||||
- [ ] 收藏系统
|
||||
- [ ] 方案使用记录
|
||||
|
||||
### Phase 4: 画面滤镜模块
|
||||
- [ ] 滤镜列表
|
||||
- [ ] 滤镜预览
|
||||
- [ ] 滤镜编辑器
|
||||
- [ ] 上传/分享
|
||||
|
||||
### Phase 5: 系统优化模块
|
||||
- [ ] 系统优化项列表展示
|
||||
- [ ] IPC 调用优化程序
|
||||
- [ ] 优化结果展示
|
||||
- [ ] 一键优化
|
||||
|
||||
### Phase 6: 原生功能对接
|
||||
- [ ] XiXi Overlay 启动/控制
|
||||
- [ ] 硬件监控面板
|
||||
- [ ] NVIDIA 配置管理
|
||||
- [ ] 色彩校准
|
||||
- [ ] 分辨率切换
|
||||
|
||||
### Phase 7: 系统集成
|
||||
- [ ] 自动更新
|
||||
- [ ] 开机自启
|
||||
- [ ] 托盘图标
|
||||
- [ ] 权限管理
|
||||
- [ ] 打包发布
|
||||
|
||||
---
|
||||
|
||||
## 八、技术栈速览
|
||||
|
||||
| 层 | 技术 | 版本 |
|
||||
|----|------|------|
|
||||
| 桌面框架 | Electron | latest |
|
||||
| 构建工具 | Vite | latest |
|
||||
| UI | React + TypeScript | latest |
|
||||
| 样式 | Tailwind CSS | v3 |
|
||||
| 状态 | Zustand | latest |
|
||||
| 动画 | Framer Motion | latest |
|
||||
| IPC | @electron/remote / contextBridge | — |
|
||||
| 打包 | electron-builder | latest |
|
||||
| 后端 API | https://gch3n.online/delta | — |
|
||||
| 字体 | JetBrains Mono | — |
|
||||
| 图标 | Material Symbols + 自定义 SVG | — |
|
||||
215
WAVE2_LOGIC_SPEC.md
Normal file
215
WAVE2_LOGIC_SPEC.md
Normal file
@@ -0,0 +1,215 @@
|
||||
# maqt-desktop 相2:逻辑层 & 页面
|
||||
|
||||
## 任务
|
||||
在已搭建好的 maqt-desktop 项目中,创建状态管理、API 服务层、Hooks 和所有页面。
|
||||
|
||||
## 路径
|
||||
/Users/guchen/.openclaw/workspace/maqt-desktop/
|
||||
|
||||
## 后端 API
|
||||
https://gch3n.online/delta
|
||||
|
||||
## 1. 全局配置
|
||||
|
||||
**src/services/api.ts** - Axios 实例
|
||||
```typescript
|
||||
import axios from 'axios';
|
||||
|
||||
const API_BASE = 'https://gch3n.online/delta';
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: API_BASE,
|
||||
timeout: 15000,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
|
||||
// 请求拦截器 - 自动添加 token
|
||||
api.interceptors.request.use((config) => {
|
||||
const token = localStorage.getItem('auth_token');
|
||||
if (token) config.headers.Authorization = `Bearer ${token}`;
|
||||
return config;
|
||||
});
|
||||
|
||||
// 响应拦截器 - 401 自动清除 token
|
||||
api.interceptors.response.use(
|
||||
(res) => res,
|
||||
(err) => {
|
||||
if (err.response?.status === 401) {
|
||||
localStorage.removeItem('auth_token');
|
||||
localStorage.removeItem('auth_user');
|
||||
}
|
||||
return Promise.reject(err);
|
||||
}
|
||||
);
|
||||
|
||||
export default api;
|
||||
```
|
||||
|
||||
## 2. Stores (Zustand)
|
||||
|
||||
**src/stores/authStore.ts**
|
||||
```typescript
|
||||
// 状态: { token, user, isLoggedIn }
|
||||
// 动作: login, logout, setUser, checkSession (调 /api/session-status)
|
||||
// 持久化: token + user 存 localStorage
|
||||
// hydrate: 启动时从 localStorage 读取
|
||||
```
|
||||
|
||||
**src/stores/navigationStore.ts**
|
||||
```typescript
|
||||
// 状态: { currentPage: 'home' | 'optimization' | 'exposure' | ... }
|
||||
// 动作: navigate(page), goBack()
|
||||
// isHome: computed currentPage === 'home'
|
||||
```
|
||||
|
||||
**src/stores/settingsStore.ts**
|
||||
```typescript
|
||||
// 状态: { autoLaunch, closePreference, theme }
|
||||
// 持久化: localStorage
|
||||
```
|
||||
|
||||
## 3. API Services
|
||||
|
||||
每个文件导出函数,使用 api 实例调用对应端点。
|
||||
|
||||
**src/services/auth.api.ts**
|
||||
- login(username, password, installId, deviceHash, platform, osVersion, appVersion)
|
||||
- register(username, password, email)
|
||||
- getSessionStatus()
|
||||
- getVipStatus()
|
||||
- activateVip(cardKey)
|
||||
- getUserStats(userId)
|
||||
- getUserLimits(userId)
|
||||
|
||||
**src/services/schemes.api.ts**
|
||||
- getSchemes(params: {sort?, page?, limit?, source?, weaponCategory?, weaponName?})
|
||||
- getSchemeById(id)
|
||||
- getCategory(code)
|
||||
- useScheme(id, source?)
|
||||
- getUserSchemes(userId)
|
||||
- getUserFavoritedCount(userId)
|
||||
|
||||
**src/services/favorites.api.ts**
|
||||
- getFavorites()
|
||||
- getFavoritesCount()
|
||||
- checkFavorite(schemeId, source)
|
||||
- addFavorite(data)
|
||||
- removeFavorite(id)
|
||||
|
||||
**src/services/filters.api.ts**
|
||||
- getFilters(params?)
|
||||
- getFilterById(id)
|
||||
- uploadFilter(data)
|
||||
|
||||
**src/services/popups.api.ts**
|
||||
- getPopups(id)
|
||||
- getSoftwareVersionAd()
|
||||
- getAfterSaleTutorialPopup()
|
||||
- getUpdateConfig()
|
||||
|
||||
## 4. Hooks
|
||||
|
||||
**src/hooks/useAuth.ts** - 封装 authStore,提供便捷的登录/登出方法
|
||||
**src/hooks/useApi.ts** - 通用请求 hook,loading/error 状态
|
||||
**src/hooks/useVip.ts** - VIP 状态检查 + 自动刷新
|
||||
**src/hooks/useElectron.ts** - 类型安全地调用 window.electronAPI
|
||||
**src/hooks/useLocalStorage.ts** - 带类型安全的 localStorage
|
||||
|
||||
## 5. 页面
|
||||
|
||||
**src/pages/Home.tsx** - 桌面首页
|
||||
- TopBar (顶部HUD)
|
||||
- DesktopGrid (2x3 桌面图标)
|
||||
- 快速优化 (bolt icon)
|
||||
- 画面滤镜 (palette icon)
|
||||
- 滤镜社区 (photo_filter icon)
|
||||
- 改枪方案 (military_tech icon)
|
||||
- 游戏准星 (center_focus_strong icon)
|
||||
- 神秘力量 (bull icon)
|
||||
- 嘉豪之力 (stealth_ninja icon)
|
||||
- 社区快捷栏 (教程/微信/官网)
|
||||
- 启动按钮: 硬件监控 + 码枪堂2.0
|
||||
|
||||
图标点击 → 检查登录/VIP → 导航到对应页面
|
||||
|
||||
**src/pages/Login.tsx** - 登录/注册页
|
||||
- 切换 登录/注册 标签
|
||||
- 登录表单: username + password
|
||||
- 注册表单: username + password + email
|
||||
- VIP 卡密激活栏
|
||||
- 登录成功后 → 跳转首页
|
||||
|
||||
**src/pages/Settings.tsx** - 设置页
|
||||
- 开机自启 (toggle)
|
||||
- 关闭偏好 (ask/tray/quit)
|
||||
- 版本号
|
||||
|
||||
**其他页面** (骨架,含标题 + 返回按钮):
|
||||
- Optimization.tsx - 快速优化(暂空)
|
||||
- Exposure.tsx - 画面滤镜(暂空)
|
||||
- FilterCommunity.tsx - 滤镜社区(暂空)
|
||||
- WeaponSchemes.tsx - 改枪方案(暂空)
|
||||
- Crosshair.tsx - 游戏准星(暂空)
|
||||
- XixiHaha.tsx - 神秘力量(暂空)
|
||||
- ForbiddenForce.tsx - 嘉豪之力(暂空)
|
||||
- SchemeDetail.tsx - 方案详情(暂空)
|
||||
|
||||
## 6. App.tsx 路由配置
|
||||
|
||||
```tsx
|
||||
// 使用 react-router-dom v7
|
||||
// Routes 配置:
|
||||
// / → Home
|
||||
// /login → Login
|
||||
// /optimization → Optimization
|
||||
// /exposure → Exposure
|
||||
// /filter-community → FilterCommunity
|
||||
// /weapon → WeaponSchemes
|
||||
// /scheme/:id → SchemeDetail
|
||||
// /crosshair → Crosshair
|
||||
// /xixi-haha → XixiHaha
|
||||
// /forbidden-force → ForbiddenForce
|
||||
// /settings → Settings
|
||||
//
|
||||
// 布局: 所有 / 以外的路由共享 BottomDock
|
||||
// 首页 (/) 不显示 BottomDock
|
||||
```
|
||||
|
||||
## 7. TypeScript 类型定义
|
||||
|
||||
**src/types/index.ts**:
|
||||
```typescript
|
||||
export interface User {
|
||||
id: string;
|
||||
username: string;
|
||||
email: string;
|
||||
avatar: string | null;
|
||||
isVip: boolean;
|
||||
vipExpireAt: string | null;
|
||||
vipLevel: number;
|
||||
}
|
||||
|
||||
export interface Scheme {
|
||||
id: string;
|
||||
title: string | null;
|
||||
weaponName: string | null;
|
||||
category: string | null;
|
||||
userId: string;
|
||||
description: string | null;
|
||||
viewsCount: number;
|
||||
downloadsCount: number;
|
||||
likesCount: number;
|
||||
favoritesCount: number;
|
||||
price: number;
|
||||
status: string;
|
||||
isOfficial: boolean;
|
||||
createdAt: string;
|
||||
user?: { username: string; avatar: string | null };
|
||||
}
|
||||
|
||||
export interface Category {
|
||||
code: string;
|
||||
name: string;
|
||||
icon: string;
|
||||
}
|
||||
```
|
||||
244
WAVE2_UI_SPEC.md
Normal file
244
WAVE2_UI_SPEC.md
Normal file
@@ -0,0 +1,244 @@
|
||||
# maqt-desktop 相2:UI 组件系统 & 样式
|
||||
|
||||
## 任务
|
||||
在已搭建好的 maqt-desktop 项目中,创建完整的 UI 组件系统。
|
||||
|
||||
## 路径
|
||||
/Users/guchen/.openclaw/workspace/maqt-desktop/
|
||||
|
||||
## Tailwind 主题色 (tailwind.config.ts)
|
||||
|
||||
```javascript
|
||||
colors: {
|
||||
tactical: {
|
||||
black: '#0A0E17', // 主背景
|
||||
dark: '#1A1F2E', // 卡片/面板
|
||||
gray: '#2D3349', // 边框/分隔
|
||||
green: '#3B4A3B', // 强调色/按钮
|
||||
olive: '#5C7A3E', // 次要强调
|
||||
army: '#3A6B35', // 激活态
|
||||
brown: '#4A3B22', // 装饰/辅助
|
||||
gold: '#7A6B4A', // VIP/高级
|
||||
goldLight: '#C8A95B', // VIP 高亮
|
||||
text: '#E8E0D0', // 主文字
|
||||
textMuted: '#A09888', // 次要文字
|
||||
textDim: '#5A5248', // 禁用文字
|
||||
warn: '#3B2B1A', // 警告/危险
|
||||
}
|
||||
}
|
||||
fontFamily: {
|
||||
mono: ['JetBrains Mono', 'Cascadia Code', 'Fira Code', 'monospace'],
|
||||
}
|
||||
```
|
||||
|
||||
## 需要创建的组件
|
||||
|
||||
### UI 基础组件 (src/components/ui/)
|
||||
|
||||
**Button.tsx** - 直角切角按钮
|
||||
- variants: primary (军绿), secondary (铁灰), ghost (透明), danger (暗琥珀)
|
||||
- sizes: sm, md, lg
|
||||
- clip-path 切角效果 (4px)
|
||||
- 悬停边框微光动画
|
||||
- 加载状态 (spinner + disabled)
|
||||
- VIP 专用样式 (暗金边框)
|
||||
|
||||
**Card.tsx** - 模块化卡片
|
||||
- 半透明悬浮背景 (backdrop-blur)
|
||||
- 军绿色细边框
|
||||
- variants: default, highlighted (VIP), hoverable
|
||||
|
||||
**Panel.tsx** - 半透明悬浮面板
|
||||
- backdrop-filter: blur(12px)
|
||||
- 机械扫描线纹理 (repeating-linear-gradient)
|
||||
- 可折叠/展开
|
||||
|
||||
**ProgressBar.tsx** - 机械进度条
|
||||
- 双层结构:背景槽 + 填充条
|
||||
- 填充条梯度 (军绿→橄榄绿→暗金)
|
||||
- 扫描线纹理叠加
|
||||
- 进度数字标注
|
||||
|
||||
**Modal.tsx** - 弹窗
|
||||
- 背景毛玻璃遮罩
|
||||
- 居中卡片
|
||||
- 关闭动画 (十字准星收缩)
|
||||
- 各种尺寸
|
||||
|
||||
**Toast.tsx** - 消息提示
|
||||
- 位置: 右下角
|
||||
- types: success, error, warning, info
|
||||
- 自动消失 (3s)
|
||||
- 军武风格图标
|
||||
|
||||
**Badge.tsx** - 角标
|
||||
- VIP 暗金角标
|
||||
- HOT/NEW 标签
|
||||
- 数字角标
|
||||
|
||||
**Input.tsx** - 输入框
|
||||
- 磨砂暗色背景
|
||||
- 军绿色 focus 边框
|
||||
- 切角效果
|
||||
- 错误状态
|
||||
|
||||
**Skeleton.tsx** - 加载骨架屏
|
||||
- 脉冲动画
|
||||
- 暗色闪烁
|
||||
|
||||
**Compass.tsx** - 罗盘刻度装饰组件
|
||||
- SVG 罗盘刻度环
|
||||
- 用于顶部 HUD 条两端装饰
|
||||
|
||||
### 布局组件 (src/components/layout/)
|
||||
|
||||
**TopBar.tsx** - 顶部 HUD 状态栏
|
||||
- 左侧: 时间 + 罗盘刻度装饰
|
||||
- 中间: 硬件状态 (CPU温度/GPU温度/FPS)
|
||||
- 右侧: VIP 标识 + 网络状态
|
||||
- 窗口控制按钮 (最小化/最大化/关闭)
|
||||
- 半透明背景,底部渐变消失
|
||||
- 自定义拖拽区域 (-webkit-app-region: drag)
|
||||
|
||||
```
|
||||
[≡ 10:45 ───◆───] [CPU 52° ● GPU 68° ● FPS 144] [⋆VIP ✓ ⋆⚡100ms] [━ ☐ ✕]
|
||||
```
|
||||
|
||||
**BottomDock.tsx** - 底部导航 Dock
|
||||
- 固定到底部
|
||||
- 磨砂玻璃背景
|
||||
- 图标+文字按钮
|
||||
- 当前页高亮 (底部小三角)
|
||||
- 按钮: 快速优化 | 画面滤镜 | 滤镜社区 | 改枪方案 | 设置
|
||||
- 首页不显示 (页面状态为 home 时隐藏)
|
||||
|
||||
**DesktopGrid.tsx** - 桌面图标网格
|
||||
- 2x3 网格布局
|
||||
- 居中排列
|
||||
- 响应式间距
|
||||
|
||||
**DesktopIcon.tsx** - 桌面图标
|
||||
- 圆形/圆角图标
|
||||
- 下方文字标签
|
||||
- 悬停光晕效果
|
||||
- 点击导航
|
||||
- 尺寸: icon 64x64, 文字 12px
|
||||
|
||||
**PageContainer.tsx** - 页面容器
|
||||
- 内容区域包装器
|
||||
- 滚动容器 (隐藏滚动条)
|
||||
- 顶部 padding 避开 TopBar
|
||||
- 底部 padding 避开 Dock
|
||||
|
||||
### 样式文件
|
||||
|
||||
**src/styles/globals.css**
|
||||
```css
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&display=swap');
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--color-black: #0A0E17;
|
||||
--color-dark: #1A1F2E;
|
||||
--color-gray: #2D3349;
|
||||
--color-green: #3B4A3B;
|
||||
--color-olive: #5C7A3E;
|
||||
--color-army: #3A6B35;
|
||||
--color-brown: #4A3B22;
|
||||
--color-gold: #7A6B4A;
|
||||
--color-gold-light: #C8A95B;
|
||||
--color-text: #E8E0D0;
|
||||
--color-text-muted: #A09888;
|
||||
--color-text-dim: #5A5248;
|
||||
--color-warn: #3B2B1A;
|
||||
}
|
||||
|
||||
* { scrollbar-width: none; }
|
||||
body {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
background: var(--color-black);
|
||||
color: var(--color-text);
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
/* 切角按钮 */
|
||||
.btn-clip {
|
||||
clip-path: polygon(
|
||||
4px 0, calc(100% - 4px) 0,
|
||||
100% 4px, 100% calc(100% - 4px),
|
||||
calc(100% - 4px) 100%, 4px 100%,
|
||||
0 calc(100% - 4px), 0 4px
|
||||
);
|
||||
}
|
||||
|
||||
/* 扫描线纹理 */
|
||||
.scanlines {
|
||||
background-image: repeating-linear-gradient(
|
||||
0deg,
|
||||
transparent 0px,
|
||||
rgba(0,0,0,0.03) 1px,
|
||||
transparent 2px
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**src/styles/animations.css**
|
||||
```css
|
||||
/* 边框流光 */
|
||||
@keyframes border-glow {
|
||||
0%, 100% { border-color: rgba(59, 74, 59, 0.6); }
|
||||
50% { border-color: rgba(92, 122, 62, 0.8); }
|
||||
}
|
||||
|
||||
/* 扫描线展开 */
|
||||
@keyframes scan-reveal {
|
||||
0% { clip-path: inset(0 100% 0 0); }
|
||||
100% { clip-path: inset(0 0 0 0); }
|
||||
}
|
||||
|
||||
/* 十字准星展开 */
|
||||
@keyframes crosshair-expand {
|
||||
0% { transform: scale(0); opacity: 0; }
|
||||
50% { transform: scale(1.1); opacity: 0.5; }
|
||||
100% { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
|
||||
/* 数字滚动跳变 */
|
||||
@keyframes digit-roll {
|
||||
0% { transform: translateY(100%); }
|
||||
100% { transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* VIP 暗金流光 */
|
||||
@keyframes gold-shimmer {
|
||||
0% { background-position: -200% center; }
|
||||
100% { background-position: 200% center; }
|
||||
}
|
||||
|
||||
/* 脉冲扫描 */
|
||||
@keyframes radar-scan {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 抖动警告 */
|
||||
@keyframes shake {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
25% { transform: translateX(-4px); }
|
||||
75% { transform: translateX(4px); }
|
||||
}
|
||||
```
|
||||
|
||||
### 注意事项
|
||||
- 所有组件用 TypeScript + React 函数组件
|
||||
- 使用 Tailwind className,适当用 clsx 库处理条件样式
|
||||
- 组件支持 className prop 以扩展样式
|
||||
- 动画使用 CSS 动画 + Framer Motion (framer-motion)
|
||||
- 主题色用 Tailwind 类: bg-tactical-xxx, text-tactical-xxx
|
||||
50
docs/SCRAPING_PLAN.md
Normal file
50
docs/SCRAPING_PLAN.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# 码枪堂方案数据采集脚本
|
||||
|
||||
## 用途
|
||||
从 maqt.top 原始 API 采集改枪方案数据,解密后导入到我们的数据库
|
||||
|
||||
## 运行环境
|
||||
Windows 机器 (nplx@100.99.186.30)
|
||||
|
||||
## 依赖
|
||||
- Node.js
|
||||
- npm install axios crypto-js
|
||||
|
||||
## 思路
|
||||
1. 用已有的 token 调用原始 API
|
||||
2. 由于前端代码已有解密逻辑,我们需要找到正确的 AES 密钥
|
||||
3. 或更简单:在 Windows 上通过 deobfuscated 代码直接解密
|
||||
|
||||
## 解法一:找密钥
|
||||
|
||||
在 CfKCgw5l.js 中搜索以下模式:
|
||||
- `createDecipheriv` 或 `createCipheriv`
|
||||
- 附近会有 key 和 iv 的生成逻辑
|
||||
- 密钥可能来自环境变量或硬编码
|
||||
|
||||
具体位置需要从 CfKCgw5l.js 或 main.deobfuscated.js 中找到。
|
||||
|
||||
## 解法二:利用前端的解密能力
|
||||
|
||||
使用 Playwright 或 Puppeteer 加载原始 app 的页面:
|
||||
1. 加载首页
|
||||
2. 注入 token
|
||||
3. 导航到改枪方案页
|
||||
4. 等待页面渲染(JS 自动解密)
|
||||
5. 从 DOM 中提取数据
|
||||
6. 或拦截 XHR/fetch 请求获取解密后的 payload
|
||||
|
||||
## 采集流程
|
||||
|
||||
```
|
||||
1. 登录 → 获取 token
|
||||
2. 遍历所有分类 (AR/SMG/SR/LMG/SG/Pistol/Launcher)
|
||||
3. 对每个分类翻页 (page 1..N)
|
||||
4. 获取方案列表数据 → 解密
|
||||
5. 对每个方案获取详情 → 解密
|
||||
6. 整理格式 → 导入 PostgreSQL
|
||||
```
|
||||
|
||||
## 导入目标
|
||||
后端: http://100.105.17.52:3001 (开发) / https://gch3n.online/delta (生产)
|
||||
数据库: PostgreSQL (maqt 数据库, schemes 表)
|
||||
131
docs/scrape_schemes.js
Normal file
131
docs/scrape_schemes.js
Normal file
@@ -0,0 +1,131 @@
|
||||
// run `node scrape_schemes.js` on Windows
|
||||
// 从 maqt.top 采集方案数据并导入到我们后端
|
||||
|
||||
const https = require('https');
|
||||
const http = require('http');
|
||||
const crypto = require('crypto');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// ========== 配置 ==========
|
||||
const API_BASE = 'maqt.top';
|
||||
const OUR_API = 'http://100.105.17.52:3001'; // 先导入到本地后端
|
||||
const TOKEN = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NDc5MTcsInVzZXJuYW1lIjoic2l4dGVlbnRoIiwidG9rZW5WZXJzaW9uIjozLCJpYXQiOjE3NzgyMTI0MDUsImV4cCI6MjA5Mzc4ODQwNX0.B_J0CDtaiiF2jJ592yKtD4RtQDIR3cDF_EYgn2UM2ko';
|
||||
|
||||
const CATEGORIES = ['AR', 'SMG', 'SR', 'LMG', 'SG', 'Pistol', 'Launcher'];
|
||||
|
||||
// ========== AES 解密 ==========
|
||||
// 从 deobfuscated 代码中找到密钥后填入这里
|
||||
// 格式: Buffer.from(key, 'utf-8') 需要 32 字节
|
||||
const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY || '0123456789abcdef0123456789abcdef';
|
||||
|
||||
function aesDecrypt(ivHex, dataHex) {
|
||||
const iv = Buffer.from(ivHex, 'hex');
|
||||
const encrypted = Buffer.from(dataHex, 'hex');
|
||||
const key = Buffer.from(ENCRYPTION_KEY, 'utf-8');
|
||||
const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
|
||||
let decrypted = decipher.update(encrypted, undefined, 'utf-8');
|
||||
decrypted += decipher.final('utf-8');
|
||||
return JSON.parse(decrypted);
|
||||
}
|
||||
|
||||
// ========== HTTP 请求 ==========
|
||||
function fetch(url, postData) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const options = {
|
||||
hostname: API_BASE,
|
||||
path: url,
|
||||
method: postData ? 'POST' : 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${TOKEN}`,
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) MaQiangTang/7.0.4',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
};
|
||||
if (postData) {
|
||||
options.headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
const req = https.request(options, (res) => {
|
||||
let data = '';
|
||||
res.on('data', chunk => data += chunk);
|
||||
res.on('end', () => {
|
||||
try {
|
||||
const parsed = JSON.parse(data);
|
||||
console.log(` ${url.slice(0, 50)} → ${res.statusCode} ${parsed.encrypted ? '(encrypted)' : '(plain)'}`);
|
||||
resolve(parsed);
|
||||
} catch (e) {
|
||||
resolve({ error: e.message, raw: data.slice(0, 100) });
|
||||
}
|
||||
});
|
||||
});
|
||||
req.on('error', reject);
|
||||
if (postData) req.write(JSON.stringify(postData));
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
// ========== 导入到我们的后端 ==========
|
||||
function importScheme(scheme) {
|
||||
return new Promise((resolve) => {
|
||||
const data = JSON.stringify(scheme);
|
||||
const req = http.request(`${OUR_API}/api/schemes`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': Buffer.byteLength(data),
|
||||
'Authorization': `Bearer ${TOKEN}`,
|
||||
},
|
||||
}, (res) => {
|
||||
let d = '';
|
||||
res.on('data', c => d += c);
|
||||
res.on('end', () => resolve(JSON.parse(d)));
|
||||
});
|
||||
req.on('error', (e) => resolve({ error: e.message }));
|
||||
req.write(data);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
// ========== 采集流程 ==========
|
||||
async function scrape() {
|
||||
console.log('=== 开始采集改枪方案数据 ===\n');
|
||||
|
||||
// 1. 先测试分类 API
|
||||
console.log('--- 采集分类 ---');
|
||||
for (const cat of CATEGORIES.slice(0, 3)) {
|
||||
console.log(`\n分类: ${cat}`);
|
||||
const result = await fetch(`/api/category/${cat}`);
|
||||
if (!result.encrypted && Array.isArray(result)) {
|
||||
console.log(` 返回 ${result.length} 条`);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 采集方案列表
|
||||
console.log('\n--- 采集方案列表 ---');
|
||||
for (const cat of CATEGORIES.slice(0, 2)) {
|
||||
console.log(`\n分类: ${cat}`);
|
||||
const result = await fetch(`/api/schemes?sort=hot&page=1&limit=12&weaponCategory=${encodeURIComponent(cat)}`);
|
||||
|
||||
if (result.encrypted) {
|
||||
try {
|
||||
const decrypted = aesDecrypt(result.iv, result.data);
|
||||
console.log(` ✅ 解密成功! 数据:\n`, JSON.stringify(decrypted, null, 2).slice(0, 2000));
|
||||
|
||||
// 保存解密结果
|
||||
fs.writeFileSync(`schemes_${cat}.json`, JSON.stringify(decrypted, null, 2));
|
||||
console.log(` 已保存到 schemes_${cat}.json`);
|
||||
} catch (e) {
|
||||
console.log(` ❌ 解密失败: ${e.message}`);
|
||||
console.log(` 密钥可能不正确,需要从 deobfuscated 代码中查找`);
|
||||
console.log(` 密钥长度: ${Buffer.from(ENCRYPTION_KEY).length} bytes`);
|
||||
console.log(` 加密数据 iv=${result.iv} data=${result.data.slice(0, 50)}...`);
|
||||
}
|
||||
} else {
|
||||
console.log(` 明文响应:\n`, JSON.stringify(result).slice(0, 500));
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n=== 采集完成 ===');
|
||||
}
|
||||
|
||||
scrape().catch(console.error);
|
||||
65
electron/main.js
Normal file
65
electron/main.js
Normal file
@@ -0,0 +1,65 @@
|
||||
"use strict";
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const electron_1 = require("electron");
|
||||
const path = __importStar(require("path"));
|
||||
let mainWindow = null;
|
||||
function createWindow() {
|
||||
mainWindow = new electron_1.BrowserWindow({
|
||||
width: 1400,
|
||||
height: 900,
|
||||
minWidth: 1100,
|
||||
minHeight: 700,
|
||||
frame: false, // 无边框自定义标题栏
|
||||
transparent: false,
|
||||
backgroundColor: '#0A0E17',
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
devTools: true,
|
||||
},
|
||||
});
|
||||
if (process.env.VITE_DEV_SERVER_URL) {
|
||||
mainWindow.loadURL(process.env.VITE_DEV_SERVER_URL);
|
||||
mainWindow.webContents.openDevTools();
|
||||
}
|
||||
else {
|
||||
mainWindow.loadFile(path.join(__dirname, '../dist/index.html'));
|
||||
}
|
||||
}
|
||||
electron_1.app.whenReady().then(createWindow);
|
||||
electron_1.app.on('window-all-closed', () => { if (process.platform !== 'darwin')
|
||||
electron_1.app.quit(); });
|
||||
32
electron/main.ts
Normal file
32
electron/main.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { app, BrowserWindow, ipcMain } from 'electron';
|
||||
import * as path from 'path';
|
||||
|
||||
let mainWindow: BrowserWindow | null = null;
|
||||
|
||||
function createWindow() {
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1400,
|
||||
height: 900,
|
||||
minWidth: 1100,
|
||||
minHeight: 700,
|
||||
frame: false, // 无边框自定义标题栏
|
||||
transparent: false,
|
||||
backgroundColor: '#0A0E17',
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
devTools: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (process.env.VITE_DEV_SERVER_URL) {
|
||||
mainWindow.loadURL(process.env.VITE_DEV_SERVER_URL);
|
||||
mainWindow.webContents.openDevTools();
|
||||
} else {
|
||||
mainWindow.loadFile(path.join(__dirname, '../dist/index.html'));
|
||||
}
|
||||
}
|
||||
|
||||
app.whenReady().then(createWindow);
|
||||
app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit(); });
|
||||
26
electron/preload.js
Normal file
26
electron/preload.js
Normal file
@@ -0,0 +1,26 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const electron_1 = require("electron");
|
||||
electron_1.contextBridge.exposeInMainWorld('electronAPI', {
|
||||
// 通用
|
||||
getAppVersion: () => electron_1.ipcRenderer.invoke('get-app-version'),
|
||||
getPlatform: () => process.platform,
|
||||
// Overlay
|
||||
startOverlay: (options) => electron_1.ipcRenderer.invoke('overlay:start', options),
|
||||
stopOverlay: () => electron_1.ipcRenderer.invoke('overlay:stop'),
|
||||
// 硬件监控
|
||||
startMonitor: () => electron_1.ipcRenderer.invoke('monitor:start'),
|
||||
stopMonitor: () => electron_1.ipcRenderer.invoke('monitor:stop'),
|
||||
// 文件/路径
|
||||
openExternal: (url) => electron_1.ipcRenderer.invoke('open-external', url),
|
||||
getResourcesPath: () => electron_1.ipcRenderer.invoke('get-resources-path'),
|
||||
existsSync: (path) => electron_1.ipcRenderer.invoke('fs:exists', path),
|
||||
// 系统优化
|
||||
optimizeItem: (id) => electron_1.ipcRenderer.invoke('optimize:item', id),
|
||||
restoreItem: (id) => electron_1.ipcRenderer.invoke('optimize:restore', id),
|
||||
getOptimizeItems: () => electron_1.ipcRenderer.invoke('optimize:list'),
|
||||
// 窗口
|
||||
minimizeWindow: () => electron_1.ipcRenderer.invoke('window:minimize'),
|
||||
maximizeWindow: () => electron_1.ipcRenderer.invoke('window:maximize'),
|
||||
closeWindow: () => electron_1.ipcRenderer.invoke('window:close'),
|
||||
});
|
||||
30
electron/preload.ts
Normal file
30
electron/preload.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { contextBridge, ipcRenderer } from 'electron';
|
||||
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
// 通用
|
||||
getAppVersion: () => ipcRenderer.invoke('get-app-version'),
|
||||
getPlatform: () => process.platform,
|
||||
|
||||
// Overlay
|
||||
startOverlay: (options: any) => ipcRenderer.invoke('overlay:start', options),
|
||||
stopOverlay: () => ipcRenderer.invoke('overlay:stop'),
|
||||
|
||||
// 硬件监控
|
||||
startMonitor: () => ipcRenderer.invoke('monitor:start'),
|
||||
stopMonitor: () => ipcRenderer.invoke('monitor:stop'),
|
||||
|
||||
// 文件/路径
|
||||
openExternal: (url: string) => ipcRenderer.invoke('open-external', url),
|
||||
getResourcesPath: () => ipcRenderer.invoke('get-resources-path'),
|
||||
existsSync: (path: string) => ipcRenderer.invoke('fs:exists', path),
|
||||
|
||||
// 系统优化
|
||||
optimizeItem: (id: string) => ipcRenderer.invoke('optimize:item', id),
|
||||
restoreItem: (id: string) => ipcRenderer.invoke('optimize:restore', id),
|
||||
getOptimizeItems: () => ipcRenderer.invoke('optimize:list'),
|
||||
|
||||
// 窗口
|
||||
minimizeWindow: () => ipcRenderer.invoke('window:minimize'),
|
||||
maximizeWindow: () => ipcRenderer.invoke('window:maximize'),
|
||||
closeWindow: () => ipcRenderer.invoke('window:close'),
|
||||
});
|
||||
16
index.html
Normal file
16
index.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>码枪堂 2.0</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0,200" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
7120
package-lock.json
generated
Normal file
7120
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
34
package.json
Normal file
34
package.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "maqt-desktop",
|
||||
"version": "7.0.4",
|
||||
"main": "electron/main.js",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"electron:dev": "concurrently \"vite\" \"wait-on http://localhost:5173 && electron .\"",
|
||||
"electron:build": "vite build && electron-builder"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource-variable/inter": "^5.2.8",
|
||||
"@fontsource-variable/jetbrains-mono": "^5.2.8",
|
||||
"@fontsource-variable/material-symbols-outlined": "^5.2.43",
|
||||
"axios": "^1.16.0",
|
||||
"framer-motion": "^12.38.0",
|
||||
"react": "^19.2.6",
|
||||
"react-dom": "^19.2.6",
|
||||
"react-router-dom": "^6.30.3",
|
||||
"zustand": "^5.0.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-react": "^4.7.0",
|
||||
"autoprefixer": "^10.5.0",
|
||||
"concurrently": "^9.2.1",
|
||||
"electron": "^40.9.3",
|
||||
"electron-builder": "^25.1.8",
|
||||
"postcss": "^8.5.14",
|
||||
"tailwindcss": "^3.4.19",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^6.4.2",
|
||||
"wait-on": "^8.0.5"
|
||||
}
|
||||
}
|
||||
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
128
scripts/scrape_schemes.js
Normal file
128
scripts/scrape_schemes.js
Normal file
@@ -0,0 +1,128 @@
|
||||
// 改枪方案数据采集器
|
||||
// 在 Windows 上运行: node scrape_schemes.js
|
||||
// 原理: 加载原版 app 的解密模块来解密方案数据
|
||||
|
||||
const https = require('https');
|
||||
const http = require('http');
|
||||
const crypto = require('crypto');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// ========== 配置 ==========
|
||||
const API_BASE = 'maqt.top';
|
||||
const TOKEN = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NDc5MTcsInVzZXJuYW1lIjoic2l4dGVlbnRoIiwidG9rZW5WZXJzaW9uIjozLCJpYXQiOjE3NzgyMTI0MDUsImV4cCI6MjA5Mzc4ODQwNX0.B_J0CDtaiiF2jJ592yKtD4RtQDIR3cDF_EYgn2UM2ko';
|
||||
const CATEGORIES = ['AR','SMG','SR','LMG','SG','Pistol','Launcher'];
|
||||
const OUR_API = 'http://100.105.17.52:3001'; // 导入到自建后端
|
||||
|
||||
// ========== 尝试多种 AES 密钥 ==========
|
||||
const KEYS_TO_TRY = [
|
||||
Buffer.from('maqt-encryption-key-32bytes!', 'utf-8'), // 28 bytes
|
||||
Buffer.from('maqt-encryption-key-32bytes!!', 'utf-8'), // 29 bytes
|
||||
Buffer.from('0123456789abcdef0123456789abcdef', 'utf-8'), // 32 bytes (HEX)
|
||||
Buffer.from('maqt-encryption-key-32byte!', 'utf-8'), // 27 bytes
|
||||
];
|
||||
|
||||
function aesDecrypt(ivHex, dataHex, key) {
|
||||
try {
|
||||
// Pad key to 32 bytes with zeros if needed
|
||||
const keyBuf = key.length === 32 ? key : Buffer.concat([key, Buffer.alloc(32 - key.length)]);
|
||||
const iv = Buffer.from(ivHex, 'hex');
|
||||
const encrypted = Buffer.from(dataHex, 'hex');
|
||||
const decipher = crypto.createDecipheriv('aes-256-cbc', keyBuf, iv);
|
||||
let d = decipher.update(encrypted, undefined, 'utf-8');
|
||||
d += decipher.final('utf-8');
|
||||
return JSON.parse(d);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function fetch(url, postData) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const opts = {
|
||||
hostname: API_BASE, path: url,
|
||||
method: postData ? 'POST' : 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${TOKEN}`,
|
||||
'User-Agent': 'MaQiangTang/7.0.4',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
};
|
||||
if (postData) { opts.headers['Content-Type'] = 'application/json'; }
|
||||
const req = https.request(opts, res => {
|
||||
let d = '';
|
||||
res.on('data', c => d += c);
|
||||
res.on('end', () => {
|
||||
try { resolve(JSON.parse(d)); }
|
||||
catch { resolve({ error: 'parse', raw: d.slice(0,100) }); }
|
||||
});
|
||||
});
|
||||
req.on('error', reject);
|
||||
if (postData) req.write(JSON.stringify(postData));
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
function tryDecrypt(encrypted) {
|
||||
if (!encrypted.encrypted) return encrypted;
|
||||
for (const key of KEYS_TO_TRY) {
|
||||
const result = aesDecrypt(encrypted.iv, encrypted.data, key);
|
||||
if (result) {
|
||||
console.log(` ✅ 解密成功! 密钥: ${key.toString('utf-8')}`);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('=== 码枪堂方案数据采集器 ===\n');
|
||||
|
||||
// 1. 测试解密
|
||||
console.log('--- 获取测试数据 ---');
|
||||
const testData = await fetch('/api/schemes?sort=hot&page=1&limit=2&weaponCategory=AR');
|
||||
|
||||
if (testData.encrypted) {
|
||||
console.log(` 加密数据: iv=${testData.iv} data_len=${testData.data.length}`);
|
||||
const decrypted = tryDecrypt(testData);
|
||||
if (!decrypted) {
|
||||
console.log(' ❌ 所有密钥都不匹配');
|
||||
console.log(' 请从 deobfuscated main.js 中找到正确的密钥');
|
||||
console.log(' 或在原始应用中运行:');
|
||||
console.log(' require("crypto").createDecipheriv("aes-256-cbc", <key>, <iv>)');
|
||||
return;
|
||||
}
|
||||
fs.writeFileSync('decrypted_schemes.json', JSON.stringify(decrypted, null, 2));
|
||||
console.log(' 已保存到 decrypted_schemes.json');
|
||||
} else {
|
||||
console.log(' 明文响应:', JSON.stringify(testData).slice(0, 500));
|
||||
}
|
||||
|
||||
// 2. 如果解密成功,采集所有分类
|
||||
if (fs.existsSync('decrypted_schemes.json')) {
|
||||
console.log('\n--- 全量采集 ---');
|
||||
const allSchemes = [];
|
||||
|
||||
for (const cat of CATEGORIES) {
|
||||
console.log(`\n分类: ${cat}`);
|
||||
for (let page = 1; page <= 5; page++) {
|
||||
const data = await fetch(`/api/schemes?sort=hot&page=${page}&limit=12&weaponCategory=${encodeURIComponent(cat)}`);
|
||||
if (data.encrypted) {
|
||||
const decrypted = tryDecrypt(data);
|
||||
if (decrypted && Array.isArray(decrypted)) {
|
||||
allSchemes.push(...decrypted);
|
||||
console.log(` 第${page}页: ${decrypted.length} 条`);
|
||||
if (decrypted.length < 12) break;
|
||||
} else break;
|
||||
} else break;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n共采集 ${allSchemes.length} 条方案`);
|
||||
fs.writeFileSync('all_schemes.json', JSON.stringify(allSchemes, null, 2));
|
||||
}
|
||||
|
||||
console.log('\n完成');
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
13
src/App.tsx
Normal file
13
src/App.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import AppRouter from './router';
|
||||
import ToastContainer from './components/ui/Toast';
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<AppRouter />
|
||||
<ToastContainer />
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
65
src/components/filters/FilterCard.tsx
Normal file
65
src/components/filters/FilterCard.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import React from 'react';
|
||||
|
||||
export interface FilterData {
|
||||
id: string;
|
||||
title: string;
|
||||
author: string;
|
||||
category: string;
|
||||
likes: number;
|
||||
downloads: number;
|
||||
preview?: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface FilterCardProps {
|
||||
filter: FilterData;
|
||||
liked: boolean;
|
||||
onLike: (id: string) => void;
|
||||
onDownload: (id: string) => void;
|
||||
}
|
||||
|
||||
export default function FilterCard({ filter, liked, onLike, onDownload }: FilterCardProps) {
|
||||
return (
|
||||
<div className="border border-[#333] bg-[#1a1a1a] relative transition-colors duration-75 hover:border-[#555]">
|
||||
<div className="absolute top-0 left-0 w-2 h-2 border-t border-l border-[#ff4500]/30 pointer-events-none" />
|
||||
<div className="absolute bottom-0 right-0 w-2 h-2 border-b border-r border-[#ff4500]/30 pointer-events-none" />
|
||||
|
||||
{/* 预览占位 */}
|
||||
<div className="h-20 bg-[#222] flex items-center justify-center border-b border-[#333]">
|
||||
<span className="material-symbols-outlined text-[20px] text-[#444]">image</span>
|
||||
</div>
|
||||
|
||||
{/* 信息 */}
|
||||
<div className="p-2 space-y-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-[9px] font-semibold tracking-[0.1em] uppercase text-[#e0e0e0] truncate">
|
||||
{filter.title}
|
||||
</h3>
|
||||
</div>
|
||||
<p className="text-[8px] font-mono text-[#555] truncate">{filter.author}</p>
|
||||
|
||||
<div className="flex items-center justify-between pt-1 border-t border-[#333]">
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => onLike(filter.id)}
|
||||
className={`text-[8px] font-mono transition-colors duration-75
|
||||
${liked ? 'text-[#ff4500]' : 'text-[#555] hover:text-[#888]'}`}
|
||||
>
|
||||
♥ {filter.likes + (liked ? 1 : 0)}
|
||||
</button>
|
||||
<span className="text-[8px] font-mono text-[#555]">↓ {filter.downloads}</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => onDownload(filter.id)}
|
||||
className="text-[7px] font-semibold tracking-[0.1em] uppercase text-[#888] border border-[#333] px-1.5 py-0.5
|
||||
hover:border-[#ff4500] hover:text-[#ff4500] transition-colors duration-75"
|
||||
>
|
||||
获取
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span className="tech-sn absolute top-1 right-1">{filter.category.toUpperCase().slice(0, 4)}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
152
src/components/filters/FilterEditor.tsx
Normal file
152
src/components/filters/FilterEditor.tsx
Normal file
@@ -0,0 +1,152 @@
|
||||
import React, { useState } from 'react';
|
||||
import Card from '../ui/Card';
|
||||
import Button from '../ui/Button';
|
||||
|
||||
interface FilterParams {
|
||||
brightness: number;
|
||||
contrast: number;
|
||||
saturation: number;
|
||||
gamma: number;
|
||||
temperature: number;
|
||||
rGain: number;
|
||||
gGain: number;
|
||||
bGain: number;
|
||||
vibrance: number;
|
||||
}
|
||||
|
||||
interface FilterEditorProps {
|
||||
initialParams?: Partial<FilterParams>;
|
||||
onSave?: (params: FilterParams) => void;
|
||||
onCancel?: () => void;
|
||||
}
|
||||
|
||||
function SliderControl({ label, value, min, max, step, unit, onChange }: {
|
||||
label: string;
|
||||
value: number;
|
||||
min: number;
|
||||
max: number;
|
||||
step: number;
|
||||
unit: string;
|
||||
onChange: (v: number) => void;
|
||||
}) {
|
||||
const pct = ((value - min) / (max - min)) * 100;
|
||||
return (
|
||||
<div className="flex items-center gap-2 py-1.5 border-b border-[#333] last:border-b-0">
|
||||
<span className="text-[8px] font-mono text-[#555] w-16 shrink-0 uppercase tracking-wider">{label}</span>
|
||||
<div className="flex-1 relative h-2 border border-[#333] bg-[#111]">
|
||||
<div className="absolute left-0 top-0 h-full bg-[#ff4500]" style={{ width: `${pct}%` }} />
|
||||
<input
|
||||
type="range"
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
value={value}
|
||||
onChange={e => onChange(+e.target.value)}
|
||||
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
<span className="text-[8px] font-mono text-[#888] w-12 text-right">{value.toFixed(step < 0.1 ? 2 : 1)}{unit}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const DEFAULT_PARAMS: FilterParams = {
|
||||
brightness: 0,
|
||||
contrast: 0,
|
||||
saturation: 0,
|
||||
gamma: 1.0,
|
||||
temperature: 6500,
|
||||
rGain: 1.0,
|
||||
gGain: 1.0,
|
||||
bGain: 1.0,
|
||||
vibrance: 0,
|
||||
};
|
||||
|
||||
export default function FilterEditor({ initialParams, onSave, onCancel }: FilterEditorProps) {
|
||||
const [params, setParams] = useState<FilterParams>({ ...DEFAULT_PARAMS, ...initialParams });
|
||||
const [name, setName] = useState('');
|
||||
|
||||
const update = (key: keyof FilterParams) => (val: number) => setParams(prev => ({ ...prev, [key]: val }));
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
{/* Name */}
|
||||
<Card serial="FLT-EDIT">
|
||||
<div className="p-2.5 space-y-2">
|
||||
<div className="flex items-center gap-2 border-b border-[#333] pb-1.5">
|
||||
<span className="material-symbols-outlined text-[14px] text-[#ff4500]">tune</span>
|
||||
<h2 className="text-[9px] font-bold tracking-[0.15em] uppercase text-[#e0e0e0]">滤镜编辑器</h2>
|
||||
</div>
|
||||
|
||||
<input
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
placeholder="滤镜名称"
|
||||
className="w-full border border-[#333] bg-[#111] text-[#e0e0e0] px-2 py-1.5 text-[9px] font-mono
|
||||
placeholder:text-[#444] outline-none focus:border-[#ff4500] transition-colors duration-75"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Tone controls */}
|
||||
<Card serial="FLT-TONE">
|
||||
<div className="p-2.5">
|
||||
<h3 className="text-[8px] font-bold tracking-[0.15em] uppercase text-[#888] mb-1">色调</h3>
|
||||
<SliderControl label="亮度" value={params.brightness} min={-100} max={100} step={1} unit="" onChange={update('brightness')} />
|
||||
<SliderControl label="对比度" value={params.contrast} min={-100} max={100} step={1} unit="" onChange={update('contrast')} />
|
||||
<SliderControl label="饱和度" value={params.saturation} min={-100} max={100} step={1} unit="" onChange={update('saturation')} />
|
||||
<SliderControl label="鲜艳度" value={params.vibrance} min={-100} max={100} step={1} unit="" onChange={update('vibrance')} />
|
||||
<SliderControl label="Gamma" value={params.gamma} min={0.1} max={3.0} step={0.01} unit="" onChange={update('gamma')} />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Color balance */}
|
||||
<Card serial="FLT-RGB">
|
||||
<div className="p-2.5">
|
||||
<h3 className="text-[8px] font-bold tracking-[0.15em] uppercase text-[#888] mb-1">色彩平衡</h3>
|
||||
<SliderControl label="R" value={params.rGain} min={0} max={2} step={0.01} unit="" onChange={update('rGain')} />
|
||||
<SliderControl label="G" value={params.gGain} min={0} max={2} step={0.01} unit="" onChange={update('gGain')} />
|
||||
<SliderControl label="B" value={params.bGain} min={0} max={2} step={0.01} unit="" onChange={update('bGain')} />
|
||||
<SliderControl label="色温" value={params.temperature} min={3000} max={10000} step={50} unit="K" onChange={update('temperature')} />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Preview bar */}
|
||||
<Card serial="FLT-PREV">
|
||||
<div className="p-2.5 space-y-1">
|
||||
<h3 className="text-[8px] font-bold tracking-[0.15em] uppercase text-[#888]">预览</h3>
|
||||
<div className="flex gap-1 h-16">
|
||||
{/* Color gradient preview affected by params */}
|
||||
<div
|
||||
className="flex-1 border border-[#333] transition-all duration-200"
|
||||
style={{
|
||||
background: `linear-gradient(90deg,
|
||||
hsl(0, ${50 + params.saturation * 0.5}%, ${50 + params.brightness * 0.3 + params.contrast * 0.2}%),
|
||||
hsl(60, ${50 + params.saturation * 0.5}%, ${50 + params.brightness * 0.3 + params.contrast * 0.2}%),
|
||||
hsl(120, ${50 + params.saturation * 0.5}%, ${50 + params.brightness * 0.3 + params.contrast * 0.2}%),
|
||||
hsl(180, ${50 + params.saturation * 0.5}%, ${50 + params.brightness * 0.3 + params.contrast * 0.2}%),
|
||||
hsl(240, ${50 + params.saturation * 0.5}%, ${50 + params.brightness * 0.3 + params.contrast * 0.2}%),
|
||||
hsl(300, ${50 + params.saturation * 0.5}%, ${50 + params.brightness * 0.3 + params.contrast * 0.2}%)
|
||||
),
|
||||
linear-gradient(180deg, transparent 0%, rgba(255,69,0,${params.temperature > 6500 ? 0.1 : 0}) 100%)`,
|
||||
}}
|
||||
/>
|
||||
{/* RGB indicator */}
|
||||
<div className="flex flex-col justify-center gap-0.5 w-6 border border-[#333] bg-[#111] items-center">
|
||||
<div className="w-4 h-1" style={{ background: `rgba(255,0,0,${params.rGain})` }} />
|
||||
<div className="w-4 h-1" style={{ background: `rgba(0,255,0,${params.gGain})` }} />
|
||||
<div className="w-4 h-1" style={{ background: `rgba(0,0,255,${params.bGain})` }} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex gap-2">
|
||||
<Button variant="primary" size="sm" className="flex-1 text-[9px]" onClick={() => onSave?.(params)}>保存滤镜</Button>
|
||||
<Button variant="ghost" size="sm" className="text-[9px]" onClick={onCancel}>取消</Button>
|
||||
<Button variant="ghost" size="sm" className="text-[9px]" onClick={() => setParams({ ...DEFAULT_PARAMS })}>重置</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
51
src/components/filters/FilterGrid.tsx
Normal file
51
src/components/filters/FilterGrid.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import React from 'react';
|
||||
import FilterCard from './FilterCard';
|
||||
import type { FilterData } from './FilterCard';
|
||||
|
||||
interface FilterGridProps {
|
||||
filters: FilterData[];
|
||||
loading: boolean;
|
||||
likedIds: Set<string>;
|
||||
onLike: (id: string) => void;
|
||||
onDownload: (id: string) => void;
|
||||
}
|
||||
|
||||
export default function FilterGrid({ filters, loading, likedIds, onLike, onDownload }: FilterGridProps) {
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-1.5">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<div key={i} className="border border-[#333] bg-[#1a1a1a] p-2 animate-pulse">
|
||||
<div className="h-20 bg-[#222] mb-2" />
|
||||
<div className="h-3 bg-[#222] w-3/4 mb-1" />
|
||||
<div className="h-2 bg-[#222] w-1/2" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (filters.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-12 gap-2 border border-[#333]">
|
||||
<span className="material-symbols-outlined text-[24px] text-[#444]">photo_library</span>
|
||||
<p className="text-[10px] font-mono text-[#555]">暂无滤镜数据</p>
|
||||
<span className="tech-sn">空状态 / 暂无数据</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-1.5">
|
||||
{filters.map(f => (
|
||||
<FilterCard
|
||||
key={f.id}
|
||||
filter={f}
|
||||
liked={likedIds.has(f.id)}
|
||||
onLike={onLike}
|
||||
onDownload={onDownload}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
34
src/components/filters/FilterPreview.tsx
Normal file
34
src/components/filters/FilterPreview.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import Card from '../ui/Card';
|
||||
|
||||
interface FilterPreviewProps {
|
||||
filter: { title: string; description?: string; previewUrl?: string };
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export default function FilterPreview({ filter, onClose }: FilterPreviewProps) {
|
||||
return (
|
||||
<Card className="max-w-2xl mx-auto p-5">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-[15px] font-bold tracking-[0.12em]">{filter.title}</h2>
|
||||
{onClose && <button onClick={onClose} className="text-tactical-textDim hover:text-tactical-text text-[14px]">✕</button>}
|
||||
</div>
|
||||
|
||||
{/* Preview image area */}
|
||||
<div className="h-48 rounded-[4px] border border-tactical-gray/30 bg-gradient-to-br from-tactical-dark via-tactical-gray/20 to-tactical-dark flex items-center justify-center mb-4">
|
||||
{filter.previewUrl ? (
|
||||
<img src={filter.previewUrl} alt={filter.title} className="w-full h-full object-contain" />
|
||||
) : (
|
||||
<div className="flex flex-col items-center gap-2 text-tactical-textDim/50">
|
||||
<span className="material-symbols-outlined text-[48px]">image</span>
|
||||
<span className="text-[11px]">预览区域</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{filter.description && (
|
||||
<p className="text-[12px] text-tactical-textMuted/70 leading-relaxed">{filter.description}</p>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
43
src/components/layout/BottomDock.tsx
Normal file
43
src/components/layout/BottomDock.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
|
||||
interface DockItem {
|
||||
id: string;
|
||||
label: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
interface BottomDockProps {
|
||||
items: DockItem[];
|
||||
currentPage: string;
|
||||
onNavigate: (page: string) => void;
|
||||
visible?: boolean;
|
||||
}
|
||||
|
||||
export default function BottomDock({ items, currentPage, onNavigate, visible = true }: BottomDockProps) {
|
||||
if (!visible) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-0 left-0 right-0 z-30 flex justify-center pb-2">
|
||||
<div className="flex border border-[#333] bg-[#1a1a1a]">
|
||||
{items.map(item => {
|
||||
const isActive = currentPage === item.id;
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={() => onNavigate(item.id)}
|
||||
className={`relative px-3.5 py-2 text-[9px] font-semibold tracking-[0.12em] uppercase
|
||||
border-r border-[#333] last:border-r-0 transition-colors duration-75
|
||||
${isActive ? 'bg-[#ff4500]/10 text-[#ff4500]' : 'text-[#555] hover:text-[#e0e0e0] hover:bg-[#222]'}`}
|
||||
>
|
||||
{isActive && (
|
||||
<span className="absolute top-0 left-0 right-0 h-[1px] bg-[#ff4500]" />
|
||||
)}
|
||||
<span className="material-symbols-outlined text-[14px] mr-1.5 align-middle">{item.icon}</span>
|
||||
{item.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
43
src/components/layout/DesktopGrid.tsx
Normal file
43
src/components/layout/DesktopGrid.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import DesktopIcon from './DesktopIcon';
|
||||
|
||||
interface DesktopItem {
|
||||
id: string;
|
||||
icon: string;
|
||||
label: string;
|
||||
vip?: boolean;
|
||||
loginRequired?: boolean;
|
||||
locked?: boolean;
|
||||
action: () => void;
|
||||
}
|
||||
|
||||
interface DesktopGridProps {
|
||||
items: DesktopItem[];
|
||||
rows?: number;
|
||||
cols?: number;
|
||||
}
|
||||
|
||||
export default function DesktopGrid({ items, rows = 2, cols = 4 }: DesktopGridProps) {
|
||||
return (
|
||||
<div
|
||||
className="grid border border-[#333] bg-[#1a1a1a]"
|
||||
style={{
|
||||
gridTemplateColumns: `repeat(${cols}, 1fr)`,
|
||||
gridTemplateRows: `repeat(${rows}, 1fr)`,
|
||||
}}
|
||||
>
|
||||
{items.map((item, idx) => (
|
||||
<DesktopIcon
|
||||
key={item.id}
|
||||
icon={item.icon}
|
||||
label={item.label}
|
||||
onClick={item.action}
|
||||
disabled={false}
|
||||
locked={item.locked}
|
||||
vip={item.vip}
|
||||
index={idx}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
64
src/components/layout/DesktopIcon.tsx
Normal file
64
src/components/layout/DesktopIcon.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import React from 'react';
|
||||
|
||||
interface DesktopIconProps {
|
||||
icon: string;
|
||||
label: string;
|
||||
onClick?: () => void;
|
||||
disabled?: boolean;
|
||||
locked?: boolean;
|
||||
vip?: boolean;
|
||||
index?: number;
|
||||
}
|
||||
|
||||
export default function DesktopIcon({ icon, label, onClick, disabled, locked, vip, index = 0 }: DesktopIconProps) {
|
||||
const isBlocked = locked || disabled;
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
className="relative flex flex-col items-center justify-center gap-1.5 py-5 border border-[#333]
|
||||
transition-colors duration-75 cursor-pointer disabled:cursor-not-allowed
|
||||
hover:bg-[#222] hover:border-[#555] group"
|
||||
>
|
||||
{/* 坐标标记 */}
|
||||
<span className="tech-sn absolute top-1 left-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
X{index % 4}Y{Math.floor(index / 4)}
|
||||
</span>
|
||||
|
||||
{/* 锁定遮罩 */}
|
||||
{isBlocked && (
|
||||
<div className="absolute inset-0 bg-[#1a1a1a]/80 flex items-center justify-center z-10">
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<span className="text-[#555] text-[12px]">🔒</span>
|
||||
<span className="text-[7px] font-mono text-[#444] tracking-wider">
|
||||
{vip ? 'VIP ONLY' : '需登录'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 图标 */}
|
||||
<span className={`material-symbols-outlined text-[24px] transition-colors duration-75
|
||||
${isBlocked ? 'text-[#444]' : 'text-[#888] group-hover:text-[#ff4500]'}`}>
|
||||
{icon}
|
||||
</span>
|
||||
|
||||
{/* 标签 */}
|
||||
<span className={`text-[9px] font-semibold tracking-[0.12em] uppercase transition-colors duration-75
|
||||
${isBlocked ? 'text-[#444]' : 'text-[#555] group-hover:text-[#e0e0e0]'}`}>
|
||||
{label}
|
||||
</span>
|
||||
|
||||
{/* VIP 标记 */}
|
||||
{vip && !isBlocked && (
|
||||
<span className="absolute top-1.5 right-1.5 text-[7px] text-[#ff4500]">✦</span>
|
||||
)}
|
||||
|
||||
{/* 底部强调线 */}
|
||||
{!isBlocked && (
|
||||
<span className="absolute bottom-0 left-1/4 right-1/4 h-[1px] bg-[#ff4500] scale-x-0 group-hover:scale-x-100 transition-transform duration-75" />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
14
src/components/layout/PageContainer.tsx
Normal file
14
src/components/layout/PageContainer.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
|
||||
interface PageContainerProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function PageContainer({ children, className = '' }: PageContainerProps) {
|
||||
return (
|
||||
<div className={`h-screen overflow-y-auto border border-[#333] bg-[#1a1a1a] flex flex-col ${className}`}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
79
src/components/layout/TopBar.tsx
Normal file
79
src/components/layout/TopBar.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import React from 'react';
|
||||
import Compass from '../ui/Compass';
|
||||
|
||||
interface TopBarProps {
|
||||
cpuTemp?: number;
|
||||
gpuTemp?: number;
|
||||
fps?: number;
|
||||
isVip?: boolean;
|
||||
ping?: number;
|
||||
onMinimize?: () => void;
|
||||
onMaximize?: () => void;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export default function TopBar({
|
||||
cpuTemp = 52, gpuTemp = 68, fps = 144, isVip, ping = 20,
|
||||
onMinimize, onMaximize, onClose,
|
||||
}: TopBarProps) {
|
||||
return (
|
||||
<div
|
||||
className="fixed top-0 left-0 right-0 z-40 flex items-center justify-between px-4 py-2.5
|
||||
backdrop-blur-[12px] bg-tactical-black/70 border-b border-tactical-gray/20 select-none"
|
||||
style={{ WebkitAppRegion: 'drag' as any }}
|
||||
>
|
||||
{/* Left: Time + Compass */}
|
||||
<div className="flex items-center gap-3" style={{ WebkitAppRegion: 'no-drag' as any }}>
|
||||
<Compass className="w-5 h-5" />
|
||||
<span className="text-[12px] font-semibold text-tactical-textMuted tracking-wider">
|
||||
{new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })}
|
||||
</span>
|
||||
<div className="flex items-center gap-1 text-[10px] text-tactical-textMuted">
|
||||
<span className="w-1 h-1 rounded-full bg-tactical-army" />
|
||||
<span>ONLINE</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Center: Hardware status */}
|
||||
<div className="flex items-center gap-4" style={{ WebkitAppRegion: 'no-drag' as any }}>
|
||||
<div className="flex items-center gap-1.5 text-[11px]">
|
||||
<span className="text-tactical-textDim">CPU</span>
|
||||
<span className="font-bold text-tactical-textMuted">{cpuTemp}°</span>
|
||||
<div className="w-12 h-[3px] bg-tactical-dark/80 rounded-full overflow-hidden">
|
||||
<div className="h-full bg-tactical-army transition-all" style={{ width: `${Math.min(cpuTemp, 100)}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 text-[11px]">
|
||||
<span className="text-tactical-textDim">GPU</span>
|
||||
<span className="font-bold text-tactical-textMuted">{gpuTemp}°</span>
|
||||
<div className="w-12 h-[3px] bg-tactical-dark/80 rounded-full overflow-hidden">
|
||||
<div className="h-full bg-tactical-olive transition-all" style={{ width: `${Math.min(gpuTemp, 100)}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 text-[11px]">
|
||||
<span className="text-tactical-textDim">FPS</span>
|
||||
<span className="font-bold text-tactical-textMuted">{fps}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right: VIP + Network + Controls */}
|
||||
<div className="flex items-center gap-3" style={{ WebkitAppRegion: 'no-drag' as any }}>
|
||||
{isVip && (
|
||||
<div className="flex items-center gap-1 text-[10px] tracking-[0.15em] text-tactical-goldLight bg-tactical-gold/10 px-2 py-0.5 border border-tactical-gold/30">
|
||||
<span>✦ VIP</span>
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-tactical-goldLight animate-pulse" />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-1 text-[10px] text-tactical-army">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-tactical-army" />
|
||||
{ping}ms
|
||||
</div>
|
||||
<div className="flex items-center gap-1 ml-2">
|
||||
<button onClick={onMinimize} className="text-tactical-textDim hover:text-tactical-textMuted text-[12px] px-1.5 py-0.5 transition-colors">━</button>
|
||||
<button onClick={onMaximize} className="text-tactical-textDim hover:text-tactical-textMuted text-[12px] px-1.5 py-0.5 transition-colors">☐</button>
|
||||
<button onClick={onClose} className="text-tactical-textDim hover:text-tactical-warn text-[12px] px-1.5 py-0.5 transition-colors">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
34
src/components/layout/TopHud.tsx
Normal file
34
src/components/layout/TopHud.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
|
||||
interface TopHudProps {
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
sections?: { label: string; value: string }[];
|
||||
}
|
||||
|
||||
export default function TopHud({ title = '码枪堂 2.0', subtitle, sections = [] }: TopHudProps) {
|
||||
return (
|
||||
<div className="flex items-center justify-between border-b border-[#333] bg-[#1a1a1a] px-3 py-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[#ff4500] text-[8px]">◆</span>
|
||||
<h1 className="text-[9px] font-bold tracking-[0.15em] uppercase text-[#e0e0e0]">{title}</h1>
|
||||
{subtitle && (
|
||||
<>
|
||||
<span className="text-[#444] text-[8px]">|</span>
|
||||
<span className="tech-sn">{subtitle}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{sections.map((s, i) => (
|
||||
<span key={i} className="flex items-center gap-1">
|
||||
<span className="text-[7px] tracking-[0.15em] uppercase text-[#555]">{s.label}</span>
|
||||
<span className="text-[9px] font-mono text-[#e0e0e0]">{s.value}</span>
|
||||
</span>
|
||||
))}
|
||||
<span className="w-[1px] h-3 bg-[#333]" />
|
||||
<span className="text-[8px] font-mono text-[#444]">V0.2.1</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
77
src/components/optimization/OptimizeItem.tsx
Normal file
77
src/components/optimization/OptimizeItem.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import React from 'react';
|
||||
|
||||
export interface OptimizeItemData {
|
||||
id: string;
|
||||
label: string;
|
||||
description: string;
|
||||
category: string;
|
||||
icon: string;
|
||||
status: 'pending' | 'optimized' | 'restored' | 'error';
|
||||
}
|
||||
|
||||
interface OptimizeItemProps {
|
||||
item: OptimizeItemData;
|
||||
onToggle: (id: string, action: 'optimize' | 'restore') => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const statusStyles: Record<string, string> = {
|
||||
pending: 'border-[#333]',
|
||||
optimized: 'border-[#ff4500]/50 bg-[#ff4500]/5',
|
||||
restored: 'border-[#333]',
|
||||
error: 'border-[#cc3300]/50 bg-[#cc3300]/5',
|
||||
};
|
||||
|
||||
const statusTextStyles: Record<string, string> = {
|
||||
pending: 'text-[#555]',
|
||||
optimized: 'text-[#ff4500]',
|
||||
restored: 'text-[#555]',
|
||||
error: 'text-[#cc3300]',
|
||||
};
|
||||
|
||||
const statusLabels: Record<string, string> = {
|
||||
pending: '待优化',
|
||||
optimized: '已优化',
|
||||
restored: '已恢复',
|
||||
error: '失败',
|
||||
};
|
||||
|
||||
export default function OptimizeItem({ item, onToggle, disabled }: OptimizeItemProps) {
|
||||
const isOptimized = item.status === 'optimized';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`border transition-colors duration-75 bg-[#1a1a1a]/60 ${statusStyles[item.status]}`}
|
||||
>
|
||||
<div className="flex items-center gap-2 px-2 py-2">
|
||||
<div className={`w-1 h-1 shrink-0 rounded-full ${isOptimized ? 'bg-[#ff4500]' : 'bg-[#333]'}`} />
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="material-symbols-outlined text-[12px] text-[#555]">{item.icon}</span>
|
||||
<span className="text-[9px] font-semibold tracking-[0.1em] uppercase text-[#e0e0e0] truncate">
|
||||
{item.label}
|
||||
</span>
|
||||
<span className={`text-[7px] font-mono tracking-wider px-1 py-[1px] border ${statusTextStyles[item.status]} border-current/30`}>
|
||||
{statusLabels[item.status]}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-[8px] text-[#555] mt-0.5 truncate">{item.description}</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => onToggle(item.id, isOptimized ? 'restore' : 'optimize')}
|
||||
disabled={disabled || item.status === 'error'}
|
||||
className={`w-[64px] shrink-0 py-1.5 text-[8px] font-semibold tracking-[0.1em] uppercase border transition-colors duration-75
|
||||
${isOptimized
|
||||
? 'border-[#555] text-[#555] hover:border-[#ff4500] hover:text-[#ff4500]'
|
||||
: 'border-[#555] text-[#888] hover:border-[#ff4500] hover:text-[#ff4500]'
|
||||
}
|
||||
disabled:opacity-40 disabled:cursor-not-allowed`}
|
||||
>
|
||||
{isOptimized ? '恢复' : '优化'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
27
src/components/optimization/OptimizePanel.tsx
Normal file
27
src/components/optimization/OptimizePanel.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import Panel from '../ui/Panel';
|
||||
import OptimizeItem from './OptimizeItem';
|
||||
import type { OptimizeItemData } from './OptimizeItem';
|
||||
|
||||
interface OptimizePanelProps {
|
||||
title: string;
|
||||
icon: string;
|
||||
items: OptimizeItemData[];
|
||||
onToggle: (id: string, action: 'optimize' | 'restore') => void;
|
||||
disabled?: boolean;
|
||||
defaultOpen?: boolean;
|
||||
serial?: string;
|
||||
}
|
||||
|
||||
export default function OptimizePanel({ title, icon, items, onToggle, disabled, defaultOpen = true, serial }: OptimizePanelProps) {
|
||||
const optimized = items.filter(i => i.status === 'optimized').length;
|
||||
return (
|
||||
<Panel title={`${icon} ${title} [${optimized}/${items.length}]`} collapsible defaultOpen={defaultOpen} serial={serial}>
|
||||
<div className="space-y-1">
|
||||
{items.map(item => (
|
||||
<OptimizeItem key={item.id} item={item} onToggle={onToggle} disabled={disabled} />
|
||||
))}
|
||||
</div>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
38
src/components/optimization/OptimizeResult.tsx
Normal file
38
src/components/optimization/OptimizeResult.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
|
||||
interface OptimizeResultProps {
|
||||
success: boolean;
|
||||
message: string;
|
||||
details?: string[];
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export default function OptimizeResult({ success, message, details, onClose }: OptimizeResultProps) {
|
||||
return (
|
||||
<div className={`border p-2.5 ${success ? 'border-[#ff4500] bg-[#ff4500]/5' : 'border-[#cc3300] bg-[#cc3300]/5'}`}>
|
||||
<div className="flex items-start gap-2">
|
||||
<span className={`text-[14px] leading-none ${success ? 'text-[#ff4500]' : 'text-[#cc3300]'}`}>
|
||||
{success ? '▸' : '✕'}
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-[10px] font-bold tracking-[0.12em] uppercase text-[#e0e0e0]">
|
||||
{success ? '优化完成' : '优化失败'}
|
||||
</h3>
|
||||
<p className="text-[8px] font-mono text-[#888] mt-0.5">{message}</p>
|
||||
{details && details.length > 0 && (
|
||||
<div className="mt-1.5 space-y-0.5">
|
||||
{details.map((d, i) => (
|
||||
<p key={i} className="text-[7px] font-mono text-[#555]">▸ {d}</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{onClose && (
|
||||
<button onClick={onClose} className="text-[#555] hover:text-[#e0e0e0] text-[10px] leading-none transition-colors duration-75">
|
||||
✕
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
39
src/components/schemes/CategoryTabs.tsx
Normal file
39
src/components/schemes/CategoryTabs.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
|
||||
interface Category {
|
||||
code: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface CategoryTabsProps {
|
||||
categories: Category[];
|
||||
activeCode: string;
|
||||
onSelect: (code: string) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function CategoryTabs({ categories, activeCode, onSelect, className = '' }: CategoryTabsProps) {
|
||||
return (
|
||||
<div className={`overflow-x-auto scrollbar-none ${className}`}>
|
||||
<div className="flex gap-0 border border-[#333] bg-[#1a1a1a]">
|
||||
{categories.map(cat => (
|
||||
<button
|
||||
key={cat.code}
|
||||
onClick={() => onSelect(cat.code)}
|
||||
className={`relative px-2.5 py-1.5 text-[8px] font-semibold tracking-[0.08em] uppercase border-r border-[#333] last:border-r-0 transition-colors duration-75
|
||||
${activeCode === cat.code
|
||||
? 'text-[#ff4500] bg-[#ff4500]/10'
|
||||
: 'text-[#555] hover:text-[#888] hover:bg-[#222]'
|
||||
}`}
|
||||
>
|
||||
{cat.name}
|
||||
<span className="ml-1 text-[7px] text-[#444]">{cat.code}</span>
|
||||
{activeCode === cat.code && (
|
||||
<span className="absolute bottom-0 left-1 right-1 h-[1px] bg-[#ff4500]" />
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
89
src/components/schemes/SchemeCard.tsx
Normal file
89
src/components/schemes/SchemeCard.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import React from 'react';
|
||||
import Button from '../ui/Button';
|
||||
import Card from '../ui/Card';
|
||||
|
||||
export interface SchemeData {
|
||||
id: string;
|
||||
title: string | null;
|
||||
weaponName: string | null;
|
||||
category: string | null;
|
||||
viewsCount: number;
|
||||
downloadsCount: number;
|
||||
likesCount: number;
|
||||
favoritesCount: number;
|
||||
price: number;
|
||||
isOfficial: boolean;
|
||||
user?: { username: string };
|
||||
}
|
||||
|
||||
interface SchemeCardProps {
|
||||
scheme: SchemeData;
|
||||
isFavorited?: boolean;
|
||||
onFavorite?: (id: string) => void;
|
||||
onUse?: (id: string) => void;
|
||||
onClick?: (id: string) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function SchemeCard({ scheme, isFavorited, onFavorite, onUse, onClick, className = '' }: SchemeCardProps) {
|
||||
return (
|
||||
<Card
|
||||
hoverable
|
||||
onClick={() => onClick?.(scheme.id)}
|
||||
className={`flex flex-col p-3 gap-2 ${className}`}
|
||||
serial={scheme.isOfficial ? 'OFFICIAL' : undefined}
|
||||
>
|
||||
{/* 标题 */}
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="min-w-0 flex-1">
|
||||
<h3 className="text-[10px] font-bold tracking-[0.08em] uppercase text-[#e0e0e0] truncate">
|
||||
{scheme.title || '未命名方案'}
|
||||
</h3>
|
||||
<p className="text-[8px] font-mono text-[#555] mt-0.5">{scheme.weaponName || scheme.category || '通用'}</p>
|
||||
</div>
|
||||
{scheme.isOfficial && (
|
||||
<span className="text-[7px] font-semibold tracking-[0.1em] uppercase text-[#ff4500] border border-[#ff4500]/30 px-1 py-0.5">
|
||||
官方
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 作者 */}
|
||||
{scheme.user?.username && (
|
||||
<p className="text-[8px] font-mono text-[#555]">作者: {scheme.user.username}</p>
|
||||
)}
|
||||
|
||||
{/* 价格 */}
|
||||
{scheme.price > 0 && (
|
||||
<p className="text-[8px] font-mono text-[#555]">💰 {scheme.price}</p>
|
||||
)}
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div className="flex items-center gap-2 mt-auto pt-1 border-t border-[#333]">
|
||||
<button
|
||||
onClick={e => { e.stopPropagation(); onFavorite?.(scheme.id); }}
|
||||
className={`text-[8px] font-mono px-1.5 py-1 border transition-colors duration-75
|
||||
${isFavorited
|
||||
? 'border-[#ff4500] text-[#ff4500]'
|
||||
: 'border-[#333] text-[#555] hover:border-[#555]'}`}
|
||||
>
|
||||
{isFavorited ? '★' : '☆'} {scheme.favoritesCount}
|
||||
</button>
|
||||
<button
|
||||
onClick={e => { e.stopPropagation(); onUse?.(scheme.id); }}
|
||||
className="ml-auto text-[8px] font-semibold tracking-[0.1em] uppercase text-[#888] border border-[#333] px-1.5 py-1
|
||||
hover:border-[#ff4500] hover:text-[#ff4500] transition-colors duration-75"
|
||||
>
|
||||
使用
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 统计数据 */}
|
||||
<div className="flex items-center gap-3 text-[7px] font-mono text-[#555]">
|
||||
<span>👁 {scheme.viewsCount}</span>
|
||||
<span>⬇ {scheme.downloadsCount}</span>
|
||||
<span>👍 {scheme.likesCount}</span>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
11
src/components/schemes/SchemeEditor.tsx
Normal file
11
src/components/schemes/SchemeEditor.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function SchemeEditor() {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-16 gap-3">
|
||||
<span className="material-symbols-outlined text-[40px] text-tactical-textDim">construction</span>
|
||||
<p className="text-[13px] text-tactical-textMuted tracking-wider">方案编辑器</p>
|
||||
<p className="text-[11px] text-tactical-textDim">开发中</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
95
src/components/schemes/SchemeList.tsx
Normal file
95
src/components/schemes/SchemeList.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import React from 'react';
|
||||
import SchemeCard from './SchemeCard';
|
||||
import type { SchemeData } from './SchemeCard';
|
||||
|
||||
interface SchemeListProps {
|
||||
schemes: SchemeData[];
|
||||
loading?: boolean;
|
||||
favoritedIds?: Set<string>;
|
||||
page: number;
|
||||
totalPages: number;
|
||||
onPageChange: (page: number) => void;
|
||||
onFavorite: (id: string) => void;
|
||||
onUse: (id: string) => void;
|
||||
onClick: (id: string) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function SchemeList({
|
||||
schemes, loading, favoritedIds = new Set(),
|
||||
page, totalPages, onPageChange,
|
||||
onFavorite, onUse, onClick, className = ''
|
||||
}: SchemeListProps) {
|
||||
if (loading) {
|
||||
return (
|
||||
<div className={`grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-1.5 ${className}`}>
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<div key={i} className="border border-[#333] bg-[#1a1a1a] p-3 animate-pulse">
|
||||
<div className="h-3 bg-[#222] w-3/4 mb-2" />
|
||||
<div className="h-2.5 bg-[#222] w-1/2 mb-3" />
|
||||
<div className="h-6 bg-[#222] w-full" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!schemes.length) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-12 gap-2 border border-[#333]">
|
||||
<span className="text-[24px] text-[#444]">📭</span>
|
||||
<p className="text-[10px] font-mono text-[#555]">暂无方案数据</p>
|
||||
<span className="tech-sn">EMPTY / 404</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`flex flex-col gap-3 ${className}`}>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-1.5">
|
||||
{schemes.map(s => (
|
||||
<SchemeCard
|
||||
key={s.id}
|
||||
scheme={s}
|
||||
isFavorited={favoritedIds.has(s.id)}
|
||||
onFavorite={onFavorite}
|
||||
onUse={onUse}
|
||||
onClick={onClick}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 分页 */}
|
||||
{totalPages > 1 && (
|
||||
<div className="flex items-center justify-center gap-1 border-t border-[#333] pt-2">
|
||||
<button
|
||||
disabled={page <= 1}
|
||||
onClick={() => onPageChange(page - 1)}
|
||||
className="px-2 py-1 text-[8px] font-mono text-[#555] border border-[#333] disabled:opacity-30 hover:border-[#555]"
|
||||
>
|
||||
◀
|
||||
</button>
|
||||
{Array.from({ length: totalPages }).map((_, i) => (
|
||||
<button
|
||||
key={i}
|
||||
onClick={() => onPageChange(i + 1)}
|
||||
className={`px-2 py-1 text-[8px] font-mono border transition-colors duration-75
|
||||
${page === i + 1
|
||||
? 'border-[#ff4500] text-[#ff4500] bg-[#ff4500]/10'
|
||||
: 'border-[#333] text-[#555] hover:border-[#555]'}`}
|
||||
>
|
||||
{i + 1}
|
||||
</button>
|
||||
))}
|
||||
<button
|
||||
disabled={page >= totalPages}
|
||||
onClick={() => onPageChange(page + 1)}
|
||||
className="px-2 py-1 text-[8px] font-mono text-[#555] border border-[#333] disabled:opacity-30 hover:border-[#555]"
|
||||
>
|
||||
▶
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
35
src/components/schemes/SchemePreviewer.tsx
Normal file
35
src/components/schemes/SchemePreviewer.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
|
||||
interface SchemePreviewerProps {
|
||||
content?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function SchemePreviewer({ content, className = '' }: SchemePreviewerProps) {
|
||||
if (!content) {
|
||||
return (
|
||||
<div className={`flex items-center justify-center py-8 text-[12px] text-tactical-textDim ${className}`}>
|
||||
暂无预览内容
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(content);
|
||||
return (
|
||||
<div className={`rounded-[4px] border border-tactical-gray/30 bg-tactical-dark/50 p-4 ${className}`}>
|
||||
<pre className="text-[11px] text-tactical-textMuted/80 leading-relaxed whitespace-pre-wrap break-all max-h-[400px] overflow-y-auto scrollbar-none">
|
||||
{JSON.stringify(parsed, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
} catch {
|
||||
return (
|
||||
<div className={`rounded-[4px] border border-tactical-gray/30 bg-tactical-dark/50 p-4 ${className}`}>
|
||||
<p className="text-[11px] text-tactical-textMuted/80 whitespace-pre-wrap break-all max-h-[400px] overflow-y-auto scrollbar-none">
|
||||
{content}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
25
src/components/ui/Badge.tsx
Normal file
25
src/components/ui/Badge.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
|
||||
type BadgeVariant = 'default' | 'vip' | 'success' | 'warning' | 'error';
|
||||
|
||||
interface BadgeProps {
|
||||
children: React.ReactNode;
|
||||
variant?: BadgeVariant;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const styleMap: Record<BadgeVariant, string> = {
|
||||
default: 'border-[#333] text-[#555]',
|
||||
vip: 'border-[#ff4500]/40 text-[#ff4500]',
|
||||
success: 'border-[#ff4500]/40 text-[#ff4500]',
|
||||
warning: 'border-[#ff4500]/40 text-[#ff4500]',
|
||||
error: 'border-[#cc3300]/40 text-[#cc3300]',
|
||||
};
|
||||
|
||||
export default function Badge({ children, variant = 'default', className = '' }: BadgeProps) {
|
||||
return (
|
||||
<span className={`inline-block px-1.5 py-0.5 text-[8px] font-semibold tracking-[0.08em] uppercase border ${styleMap[variant]} ${className}`}>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
55
src/components/ui/Button.tsx
Normal file
55
src/components/ui/Button.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import React from 'react';
|
||||
|
||||
type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'danger';
|
||||
type ButtonSize = 'sm' | 'md' | 'lg';
|
||||
|
||||
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: ButtonVariant;
|
||||
size?: ButtonSize;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
const variantStyles: Record<ButtonVariant, string> = {
|
||||
primary:
|
||||
'bg-[#ff4500] text-[#111] border-[#ff4500] hover:bg-[#111] hover:text-[#ff4500]',
|
||||
secondary:
|
||||
'bg-transparent text-[#e0e0e0] border-[#555] hover:bg-[#e0e0e0] hover:text-[#111]',
|
||||
ghost:
|
||||
'bg-transparent text-[#888] border-[#333] hover:border-[#e0e0e0] hover:text-[#e0e0e0]',
|
||||
danger:
|
||||
'bg-transparent text-[#cc3300] border-[#cc3300] hover:bg-[#cc3300] hover:text-[#111]',
|
||||
};
|
||||
|
||||
const sizeStyles: Record<ButtonSize, string> = {
|
||||
sm: 'px-3 py-1.5 text-[10px]',
|
||||
md: 'px-5 py-2.5 text-[11px]',
|
||||
lg: 'px-7 py-3 text-[13px]',
|
||||
};
|
||||
|
||||
export default function Button({
|
||||
variant = 'primary',
|
||||
size = 'md',
|
||||
loading,
|
||||
disabled,
|
||||
className = '',
|
||||
children,
|
||||
...props
|
||||
}: ButtonProps) {
|
||||
return (
|
||||
<button
|
||||
disabled={disabled || loading}
|
||||
className={`inline-flex items-center justify-center gap-2 font-semibold tracking-[0.08em] uppercase
|
||||
border transition-colors duration-75
|
||||
${variantStyles[variant]}
|
||||
${sizeStyles[size]}
|
||||
${disabled ? 'opacity-40 cursor-not-allowed' : 'cursor-pointer'}
|
||||
${className}`}
|
||||
{...props}
|
||||
>
|
||||
{loading && (
|
||||
<span className="w-3 h-3 border border-current border-t-transparent animate-spin" />
|
||||
)}
|
||||
{!loading && children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
39
src/components/ui/Card.tsx
Normal file
39
src/components/ui/Card.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
|
||||
interface CardProps {
|
||||
children: React.ReactNode;
|
||||
highlighted?: boolean;
|
||||
hoverable?: boolean;
|
||||
className?: string;
|
||||
onClick?: () => void;
|
||||
serial?: string;
|
||||
}
|
||||
|
||||
export default function Card({
|
||||
children,
|
||||
highlighted,
|
||||
hoverable,
|
||||
className = '',
|
||||
onClick,
|
||||
serial,
|
||||
}: CardProps) {
|
||||
return (
|
||||
<div
|
||||
onClick={onClick}
|
||||
className={`relative border bg-[#1a1a1a] transition-colors duration-75
|
||||
${highlighted ? 'border-[#ff4500]' : 'border-[#333]'}
|
||||
${hoverable ? 'hover:border-[#ff4500] cursor-pointer' : ''}
|
||||
${className}`}
|
||||
>
|
||||
{/* Cut corner decorations */}
|
||||
<div className="absolute top-0 left-0 w-2 h-2 border-t border-l border-[#ff4500]/40 pointer-events-none" />
|
||||
<div className="absolute bottom-0 right-0 w-2 h-2 border-b border-r border-[#ff4500]/40 pointer-events-none" />
|
||||
|
||||
{serial && (
|
||||
<span className="tech-sn absolute top-1 right-1">{serial}</span>
|
||||
)}
|
||||
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
28
src/components/ui/Compass.tsx
Normal file
28
src/components/ui/Compass.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function Compass({ className = '' }: { className?: string }) {
|
||||
return (
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" className={className}>
|
||||
<circle cx="16" cy="16" r="14" fill="none" stroke="rgba(200,169,91,0.2)" strokeWidth="1" />
|
||||
<circle cx="16" cy="16" r="11" fill="none" stroke="rgba(200,169,91,0.15)" strokeWidth="0.5" strokeDasharray="2 2" />
|
||||
{[0, 30, 60, 90, 120, 150, 180, 210, 240, 270, 300, 330].map((deg, i) => {
|
||||
const rad = (deg - 90) * (Math.PI / 180);
|
||||
const inner = i % 3 === 0 ? 12 : 13;
|
||||
const outer = i % 3 === 0 ? 15 : 14.5;
|
||||
return (
|
||||
<line
|
||||
key={deg}
|
||||
x1={16 + inner * Math.cos(rad)}
|
||||
y1={16 + inner * Math.sin(rad)}
|
||||
x2={16 + outer * Math.cos(rad)}
|
||||
y2={16 + outer * Math.sin(rad)}
|
||||
stroke={i % 3 === 0 ? 'rgba(200,169,91,0.4)' : 'rgba(200,169,91,0.2)'}
|
||||
strokeWidth={i % 3 === 0 ? 1.2 : 0.8}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<polygon points="16,4 17,14 16,15 15,14" fill="rgba(200,169,91,0.6)" />
|
||||
<polygon points="16,28 17,18 16,17 15,18" fill="rgba(200,169,91,0.3)" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
20
src/components/ui/Input.tsx
Normal file
20
src/components/ui/Input.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
|
||||
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export default function Input({ label, className = '', ...props }: InputProps) {
|
||||
return (
|
||||
<div className="flex flex-col gap-0.5">
|
||||
{label && <span className="text-[8px] font-mono text-[#555] uppercase tracking-wider">{label}</span>}
|
||||
<input
|
||||
className={`border border-[#333] bg-[#111] text-[#e0e0e0] px-2.5 py-2 text-[10px] font-mono outline-none
|
||||
placeholder:text-[#444] transition-colors duration-75
|
||||
focus:border-[#ff4500] focus:bg-[#1a1a1a]
|
||||
${className}`}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
45
src/components/ui/Modal.tsx
Normal file
45
src/components/ui/Modal.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import React from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
|
||||
interface ModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
title?: string;
|
||||
children: React.ReactNode;
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
}
|
||||
|
||||
const sizeMap = { sm: 'max-w-sm', md: 'max-w-lg', lg: 'max-w-2xl' };
|
||||
|
||||
export default function Modal({ open, onClose, title, children, size = 'md' }: ModalProps) {
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{open && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="absolute inset-0 backdrop-blur-[4px] bg-black/60"
|
||||
onClick={onClose}
|
||||
/>
|
||||
<motion.div
|
||||
initial={{ scale: 0.8, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
exit={{ scale: 0.8, opacity: 0 }}
|
||||
transition={{ type: 'spring', stiffness: 300, damping: 25 }}
|
||||
className={`relative w-full ${sizeMap[size]} mx-4 border border-tactical-gray/40 backdrop-blur-[16px] bg-tactical-black/85`}
|
||||
>
|
||||
{title && (
|
||||
<div className="flex items-center justify-between px-5 py-4 border-b border-tactical-gray/30">
|
||||
<h2 className="text-[15px] font-bold tracking-[0.12em] text-tactical-text">{title}</h2>
|
||||
<button onClick={onClose} className="text-tactical-textMuted hover:text-tactical-text transition-colors text-lg">✕</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="p-5">{children}</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
54
src/components/ui/Panel.tsx
Normal file
54
src/components/ui/Panel.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
interface PanelProps {
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
collapsible?: boolean;
|
||||
defaultOpen?: boolean;
|
||||
className?: string;
|
||||
serial?: string;
|
||||
}
|
||||
|
||||
export default function Panel({
|
||||
title,
|
||||
children,
|
||||
collapsible,
|
||||
defaultOpen = true,
|
||||
className = '',
|
||||
serial,
|
||||
}: PanelProps) {
|
||||
const [open, setOpen] = useState(defaultOpen);
|
||||
|
||||
return (
|
||||
<div className={`border border-[#333] bg-[#1a1a1a] relative ${className}`}>
|
||||
{/* Cut corner */}
|
||||
<div className="absolute top-0 left-0 w-2 h-2 border-t border-l border-[#ff4500]/40 pointer-events-none z-10" />
|
||||
<div className="absolute bottom-0 right-0 w-2 h-2 border-b border-r border-[#ff4500]/40 pointer-events-none z-10" />
|
||||
|
||||
{/* Header */}
|
||||
<div
|
||||
className={`flex items-center justify-between px-3 py-2.5 border-b border-[#333] select-none
|
||||
${collapsible ? 'cursor-pointer hover:bg-[#222]' : ''}`}
|
||||
onClick={() => collapsible && setOpen(!open)}
|
||||
>
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<span className="text-[#555] text-[9px] font-mono">▌</span>
|
||||
<h3 className="text-[10px] font-semibold tracking-[0.12em] uppercase text-[#e0e0e0] truncate">
|
||||
{title}
|
||||
</h3>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{serial && <span className="tech-sn">{serial}</span>}
|
||||
{collapsible && (
|
||||
<span className={`text-[#555] text-[8px] transition-transform duration-75 ${open ? 'rotate-180' : ''}`}>
|
||||
▼
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
{open && <div className="px-3 py-2.5">{children}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
29
src/components/ui/ProgressBar.tsx
Normal file
29
src/components/ui/ProgressBar.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
|
||||
interface ProgressBarProps {
|
||||
value: number;
|
||||
max?: number;
|
||||
label?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function ProgressBar({ value, max = 100, label, className = '' }: ProgressBarProps) {
|
||||
const pct = Math.min((value / max) * 100, 100);
|
||||
|
||||
return (
|
||||
<div className={`flex items-center gap-3 ${className}`}>
|
||||
<div className="flex-1 h-[6px] bg-tactical-dark/90 border border-tactical-gray/40 relative overflow-hidden">
|
||||
<div
|
||||
className="h-full transition-all duration-500 ease-out relative"
|
||||
style={{ width: `${pct}%`, background: 'linear-gradient(90deg, #3A6B35 0%, #5C7A3E 50%, #7A6B4A 100%)' }}
|
||||
>
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
style={{ backgroundImage: 'repeating-linear-gradient(90deg, transparent 0px, rgba(255,255,255,0.03) 2px, transparent 4px)' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{label && <span className="text-[11px] font-bold text-tactical-textMuted whitespace-nowrap">{label}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
9
src/components/ui/Skeleton.tsx
Normal file
9
src/components/ui/Skeleton.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
|
||||
interface SkeletonProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function Skeleton({ className = '' }: SkeletonProps) {
|
||||
return <div className={`animate-pulse bg-[#222] ${className}`} />;
|
||||
}
|
||||
57
src/components/ui/Toast.tsx
Normal file
57
src/components/ui/Toast.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
type ToastType = 'success' | 'error' | 'warning' | 'info';
|
||||
|
||||
interface ToastMessage {
|
||||
id: number;
|
||||
type: ToastType;
|
||||
text: string;
|
||||
}
|
||||
|
||||
let toastId = 0;
|
||||
const listeners: Set<(msg: ToastMessage) => void> = new Set();
|
||||
|
||||
export function showToast(text: string, type: ToastType = 'info') {
|
||||
const msg: ToastMessage = { id: ++toastId, type, text };
|
||||
listeners.forEach(fn => fn(msg));
|
||||
}
|
||||
|
||||
export function useToast() {
|
||||
return (type: ToastType, text: string) => showToast(text, type);
|
||||
}
|
||||
|
||||
const typeColors: Record<ToastType, string> = {
|
||||
success: 'border-[#ff4500] text-[#ff4500]',
|
||||
error: 'border-[#cc3300] text-[#cc3300]',
|
||||
warning: 'border-[#ff4500] text-[#ff4500]',
|
||||
info: 'border-[#555] text-[#e0e0e0]',
|
||||
};
|
||||
|
||||
export default function ToastContainer() {
|
||||
const [toasts, setToasts] = useState<ToastMessage[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (msg: ToastMessage) => {
|
||||
setToasts(prev => [...prev, msg]);
|
||||
setTimeout(() => {
|
||||
setToasts(prev => prev.filter(t => t.id !== msg.id));
|
||||
}, 3000);
|
||||
};
|
||||
listeners.add(handler);
|
||||
return () => { listeners.delete(handler); };
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="fixed top-4 right-4 z-50 flex flex-col gap-1.5 pointer-events-none">
|
||||
{toasts.map(t => (
|
||||
<div
|
||||
key={t.id}
|
||||
className={`border bg-[#1a1a1a] px-3 py-2 font-mono text-[10px] tracking-wider
|
||||
animate-in slide-in-from-right-2 ${typeColors[t.type]}`}
|
||||
>
|
||||
▸ {t.text}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
25
src/hooks/useApi.ts
Normal file
25
src/hooks/useApi.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
|
||||
export function useApi<T = any>() {
|
||||
const [data, setData] = useState<T | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const execute = useCallback(async (fn: () => Promise<any>) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const res = await fn();
|
||||
setData(res.data || res);
|
||||
return res;
|
||||
} catch (e: any) {
|
||||
const msg = e?.response?.data?.message || e.message || '请求失败';
|
||||
setError(msg);
|
||||
throw e;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return { data, loading, error, execute };
|
||||
}
|
||||
23
src/hooks/useAuth.ts
Normal file
23
src/hooks/useAuth.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { useAuthStore } from '../stores/authStore';
|
||||
import { login as loginApi } from '../services/auth.api';
|
||||
import type { LoginRequest } from '../types';
|
||||
|
||||
export function useAuth() {
|
||||
const store = useAuthStore();
|
||||
return {
|
||||
...store,
|
||||
login: async (data: LoginRequest) => {
|
||||
try {
|
||||
const res = await loginApi(data);
|
||||
if (res.success && res.token && res.user) {
|
||||
store.login(res.token, res.user);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (e: any) {
|
||||
const msg = e?.response?.data?.message || '登录失败';
|
||||
throw new Error(msg);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
16
src/hooks/useElectron.ts
Normal file
16
src/hooks/useElectron.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export function useElectron() {
|
||||
if (typeof window !== 'undefined' && window.electronAPI) {
|
||||
return window.electronAPI;
|
||||
}
|
||||
// Fallback for browser dev
|
||||
return {
|
||||
getAppVersion: async () => '7.0.4-dev',
|
||||
getPlatform: () => 'web',
|
||||
minimizeWindow: () => {},
|
||||
maximizeWindow: () => {},
|
||||
closeWindow: () => {},
|
||||
openExternal: async (url: string) => window.open(url, '_blank'),
|
||||
startOverlay: async () => {},
|
||||
stopOverlay: async () => {},
|
||||
} as any;
|
||||
}
|
||||
15
src/hooks/useLocalStorage.ts
Normal file
15
src/hooks/useLocalStorage.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
|
||||
export function useLocalStorage<T>(key: string, initial: T): [T, (v: T) => void] {
|
||||
const [value, setValue] = useState<T>(() => {
|
||||
try {
|
||||
const stored = localStorage.getItem(key);
|
||||
return stored ? JSON.parse(stored) : initial;
|
||||
} catch { return initial; }
|
||||
});
|
||||
const set = useCallback((v: T) => {
|
||||
setValue(v);
|
||||
localStorage.setItem(key, JSON.stringify(v));
|
||||
}, [key]);
|
||||
return [value, set];
|
||||
}
|
||||
25
src/hooks/useVip.ts
Normal file
25
src/hooks/useVip.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { useAuthStore } from '../stores/authStore';
|
||||
import { getVipStatus } from '../services/auth.api';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export function useVip() {
|
||||
const user = useAuthStore(s => s.user);
|
||||
const isLoggedIn = useAuthStore(s => s.isLoggedIn);
|
||||
const [days, setDays] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoggedIn || !user?.isVip) return;
|
||||
setLoading(true);
|
||||
getVipStatus().then(r => {
|
||||
if (r.success) setDays(r.data?.daysRemaining || 0);
|
||||
}).finally(() => setLoading(false));
|
||||
}, [isLoggedIn, user?.isVip]);
|
||||
|
||||
return {
|
||||
isVip: user?.isVip ?? false,
|
||||
vipLevel: user?.vipLevel ?? 0,
|
||||
daysRemaining: days,
|
||||
loading,
|
||||
};
|
||||
}
|
||||
13
src/main.tsx
Normal file
13
src/main.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
import '@fontsource-variable/inter';
|
||||
import '@fontsource-variable/material-symbols-outlined';
|
||||
import './styles/globals.css';
|
||||
import './styles/animations.css';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
16
src/pages/Crosshair.tsx
Normal file
16
src/pages/Crosshair.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import Card from '../components/ui/Card';
|
||||
|
||||
export default function Crosshair() {
|
||||
return (
|
||||
<div className="flex flex-col gap-3 p-3 flex-1">
|
||||
<div className="flex items-center justify-between border-b border-[#333] pb-2">
|
||||
<h1 className="text-[11px] font-bold tracking-[0.15em] uppercase">游戏准星</h1>
|
||||
<span className="tech-sn">DEV-01</span>
|
||||
</div>
|
||||
<Card className="p-3" serial="WIP-01">
|
||||
<p className="text-[9px] font-mono text-[#555]">游戏准星开发中</p>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
53
src/pages/Exposure.tsx
Normal file
53
src/pages/Exposure.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import Panel from '../components/ui/Panel';
|
||||
import Card from '../components/ui/Card';
|
||||
import Button from '../components/ui/Button';
|
||||
|
||||
export default function Exposure() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3 p-3 flex-1">
|
||||
{/* 标题 */}
|
||||
<div className="flex items-center justify-between border-b border-[#333] pb-2">
|
||||
<h1 className="text-[11px] font-bold tracking-[0.15em] uppercase">画面滤镜</h1>
|
||||
<span className="tech-sn">PHASE-04 / DSP</span>
|
||||
</div>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div className="flex gap-2">
|
||||
<Button variant="primary" size="sm" onClick={() => navigate('/filter-community')} className="flex-1 text-[9px]">
|
||||
浏览滤镜社区
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" className="flex-1 text-[9px]">
|
||||
导入 ICC
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 已应用滤镜 */}
|
||||
<Panel title="已应用滤镜" serial="FLT-01">
|
||||
<div className="flex flex-col items-center justify-center py-8 gap-2">
|
||||
<span className="material-symbols-outlined text-[24px] text-[#444]">blur_on</span>
|
||||
<p className="text-[10px] font-mono text-[#555]">暂无已应用的滤镜</p>
|
||||
<p className="text-[8px] font-mono text-[#444]">从滤镜社区下载或导入 ICC 配置文件</p>
|
||||
</div>
|
||||
</Panel>
|
||||
|
||||
{/* 本地滤镜 */}
|
||||
<Panel title="本地滤镜" serial="FLT-02" defaultOpen={false}>
|
||||
<div className="flex flex-col items-center justify-center py-6 gap-2">
|
||||
<span className="material-symbols-outlined text-[24px] text-[#444]">folder</span>
|
||||
<p className="text-[10px] font-mono text-[#555]">暂无本地滤镜</p>
|
||||
</div>
|
||||
</Panel>
|
||||
|
||||
{/* 提示 */}
|
||||
<Card className="p-2 mt-auto" serial="INFO-01">
|
||||
<p className="text-[8px] font-mono text-[#555] leading-relaxed">
|
||||
[提示] 滤镜使用 ICC 色彩配置文件进行显示校准。需要兼容的显示器和显卡驱动支持。
|
||||
</p>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
98
src/pages/FilterCommunity.tsx
Normal file
98
src/pages/FilterCommunity.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import FilterGrid from '../components/filters/FilterGrid';
|
||||
import type { FilterData } from '../components/filters/FilterCard';
|
||||
import Button from '../components/ui/Button';
|
||||
import Card from '../components/ui/Card';
|
||||
import { useToast } from '../components/ui/Toast';
|
||||
import { useAuthStore } from '../stores/authStore';
|
||||
|
||||
const CATEGORIES = [
|
||||
{ code: '', name: '全部' }, { code: 'game', name: '游戏' }, { code: 'movie', name: '电影' },
|
||||
{ code: 'portrait', name: '人像' }, { code: 'landscape', name: '风景' },
|
||||
{ code: 'custom', name: '自定义' }, { code: 'vibrant', name: '鲜艳' },
|
||||
];
|
||||
|
||||
const PLACEHOLDER_FILTERS: FilterData[] = [];
|
||||
|
||||
export default function FilterCommunity() {
|
||||
const navigate = useNavigate();
|
||||
const toast = useToast();
|
||||
const isLoggedIn = useAuthStore(s => s.isLoggedIn);
|
||||
|
||||
const [filters, setFilters] = useState<FilterData[]>(PLACEHOLDER_FILTERS);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [activeCategory, setActiveCategory] = useState('');
|
||||
const [sort, setSort] = useState<'likes' | 'new'>('likes');
|
||||
const [likedIds, setLikedIds] = useState<Set<string>>(new Set());
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
setTimeout(() => { setLoading(false); }, 800);
|
||||
}, [activeCategory, sort]);
|
||||
|
||||
const handleLike = async (id: string) => {
|
||||
if (!isLoggedIn) { toast('warning', '请先登录'); navigate('/login'); return; }
|
||||
setLikedIds(prev => { const n = new Set(prev); n.has(id) ? n.delete(id) : n.add(id); return n; });
|
||||
};
|
||||
|
||||
const handleDownload = async (_id: string) => {
|
||||
toast('success', '滤镜已下载');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3 p-3 flex-1">
|
||||
{/* 标题 */}
|
||||
<div className="flex items-center justify-between border-b border-[#333] pb-2">
|
||||
<h1 className="text-[11px] font-bold tracking-[0.15em] uppercase">滤镜社区</h1>
|
||||
<span className="tech-sn">NET-01 / 公开</span>
|
||||
</div>
|
||||
|
||||
{/* 排序切换 */}
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={() => setSort('likes')}
|
||||
className={`text-[9px] font-semibold tracking-[0.12em] uppercase transition-colors duration-75
|
||||
${sort === 'likes' ? 'text-[#ff4500]' : 'text-[#555] hover:text-[#888]'}`}
|
||||
>
|
||||
▸ 热门
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setSort('new')}
|
||||
className={`text-[9px] font-semibold tracking-[0.12em] uppercase transition-colors duration-75
|
||||
${sort === 'new' ? 'text-[#ff4500]' : 'text-[#555] hover:text-[#888]'}`}
|
||||
>
|
||||
▸ 最新
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 分类标签 */}
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{CATEGORIES.map(cat => (
|
||||
<button
|
||||
key={cat.code}
|
||||
onClick={() => setActiveCategory(cat.code)}
|
||||
className={`px-2 py-1 text-[8px] font-semibold tracking-[0.1em] uppercase border transition-colors duration-75
|
||||
${activeCategory === cat.code
|
||||
? 'border-[#ff4500] text-[#ff4500] bg-[#ff4500]/10'
|
||||
: 'border-[#333] text-[#555] hover:border-[#555] hover:text-[#888]'}`}
|
||||
>
|
||||
{cat.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 列表 */}
|
||||
<div className="flex-1">
|
||||
<FilterGrid filters={filters} loading={loading} likedIds={likedIds} onLike={handleLike} onDownload={handleDownload} />
|
||||
</div>
|
||||
|
||||
{/* 提示 */}
|
||||
<Card className="p-2 mt-auto" serial="INFO-02">
|
||||
<p className="text-[8px] font-mono text-[#555] leading-relaxed">
|
||||
[提示] 社区滤镜由用户创建的 ICC 配置文件。不同显示器效果可能不同,请谨慎使用。
|
||||
</p>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
16
src/pages/ForbiddenForce.tsx
Normal file
16
src/pages/ForbiddenForce.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import Card from '../components/ui/Card';
|
||||
|
||||
export default function ForbiddenForce() {
|
||||
return (
|
||||
<div className="flex flex-col gap-3 p-3 flex-1">
|
||||
<div className="flex items-center justify-between border-b border-[#333] pb-2">
|
||||
<h1 className="text-[11px] font-bold tracking-[0.15em] uppercase">嘉豪之力</h1>
|
||||
<span className="tech-sn">DEV-02</span>
|
||||
</div>
|
||||
<Card className="p-3" serial="WIP-02">
|
||||
<p className="text-[9px] font-mono text-[#555]">此功能开发中</p>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
171
src/pages/HardwareMonitor.tsx
Normal file
171
src/pages/HardwareMonitor.tsx
Normal file
@@ -0,0 +1,171 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import Card from '../components/ui/Card';
|
||||
|
||||
interface SensorData {
|
||||
cpuTemp: number;
|
||||
cpuUsage: number;
|
||||
cpuFreq: number;
|
||||
gpuTemp: number;
|
||||
gpuUsage: number;
|
||||
gpuFreq: number;
|
||||
gpuMem: number;
|
||||
gpuMemTotal: number;
|
||||
ramUsage: number;
|
||||
ramTotal: number;
|
||||
}
|
||||
|
||||
function randAround(base: number, range: number): number {
|
||||
return base + (Math.random() - 0.5) * range;
|
||||
}
|
||||
|
||||
function mockSensorData(): SensorData {
|
||||
return {
|
||||
cpuTemp: +randAround(65, 10).toFixed(1),
|
||||
cpuUsage: +randAround(35, 20).toFixed(1),
|
||||
cpuFreq: +randAround(3.8, 0.6).toFixed(2),
|
||||
gpuTemp: +randAround(62, 8).toFixed(1),
|
||||
gpuUsage: +randAround(40, 25).toFixed(1),
|
||||
gpuFreq: +randAround(1850, 150).toFixed(0),
|
||||
gpuMem: +randAround(4.2, 1.2).toFixed(1),
|
||||
gpuMemTotal: 8,
|
||||
ramUsage: +randAround(12.8, 2.0).toFixed(1),
|
||||
ramTotal: 32,
|
||||
};
|
||||
}
|
||||
|
||||
function GaugeArc({ value, max, label, unit, color }: { value: number; max: number; label: string; unit: string; color: string }) {
|
||||
const pct = Math.min(value / max, 1);
|
||||
const dash = 150; // circumference of arc
|
||||
const offset = dash * (1 - pct);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<svg width="72" height="44" viewBox="0 0 72 44" className="overflow-visible">
|
||||
{/* Background arc */}
|
||||
<path
|
||||
d="M 8 40 A 28 28 0 0 1 64 40"
|
||||
fill="none"
|
||||
stroke="#222"
|
||||
strokeWidth="4"
|
||||
strokeLinecap="butt"
|
||||
/>
|
||||
{/* Value arc */}
|
||||
<path
|
||||
d="M 8 40 A 28 28 0 0 1 64 40"
|
||||
fill="none"
|
||||
stroke={color}
|
||||
strokeWidth="4"
|
||||
strokeLinecap="butt"
|
||||
strokeDasharray={dash}
|
||||
strokeDashoffset={offset}
|
||||
style={{ transition: 'stroke-dashoffset 0.3s ease' }}
|
||||
/>
|
||||
{/* Center value */}
|
||||
<text x="36" y="22" textAnchor="middle" fill={color} fontSize="14" fontFamily="JetBrains Mono" fontWeight="700">
|
||||
{value}
|
||||
</text>
|
||||
</svg>
|
||||
<span className="text-[7px] font-mono tracking-[0.08em] uppercase text-[#555]">{label} ({unit})</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function BarGauge({ value, max, label, unit, color }: { value: number; max: number; label: string; unit: string; color: string }) {
|
||||
const pct = Math.min(value / max, 1);
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1 h-2 border border-[#333] bg-[#111]">
|
||||
<div
|
||||
className="h-full transition-all duration-300"
|
||||
style={{ width: `${pct * 100}%`, backgroundColor: color }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-[8px] font-mono text-[#888] w-16 text-right">{value}{unit}</span>
|
||||
<span className="text-[7px] font-mono text-[#444] uppercase w-12">{label}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function HardwareMonitor() {
|
||||
const [data, setData] = useState(mockSensorData());
|
||||
const intervalRef = useRef<ReturnType<typeof setInterval>>();
|
||||
|
||||
useEffect(() => {
|
||||
setData(mockSensorData());
|
||||
intervalRef.current = setInterval(() => {
|
||||
setData(mockSensorData());
|
||||
}, 1500);
|
||||
return () => clearInterval(intervalRef.current);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3 p-3 flex-1">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between border-b border-[#333] pb-2">
|
||||
<h1 className="text-[11px] font-bold tracking-[0.15em] uppercase">硬件监控</h1>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="w-1.5 h-1.5 bg-[#ff4500] animate-pulse" />
|
||||
<span className="tech-sn">实时</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CPU */}
|
||||
<Card serial="CPU-01">
|
||||
<div className="p-2.5 space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="material-symbols-outlined text-[14px] text-[#ff4500]">memory</span>
|
||||
<h2 className="text-[9px] font-bold tracking-[0.15em] uppercase text-[#e0e0e0]">CPU</h2>
|
||||
<span className="tech-sn">Intel Core i9-13900K</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-around py-1">
|
||||
<GaugeArc value={data.cpuTemp} max={100} label="温度" unit="°C" color="#ff4500" />
|
||||
<GaugeArc value={data.cpuUsage} max={100} label="负载" unit="%" color="#ff4500" />
|
||||
<GaugeArc value={data.cpuFreq} max={6.0} label="频率" unit="GHz" color="#ff4500" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* GPU */}
|
||||
<Card serial="GPU-01">
|
||||
<div className="p-2.5 space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="material-symbols-outlined text-[14px] text-[#ff4500]">videocam</span>
|
||||
<h2 className="text-[9px] font-bold tracking-[0.15em] uppercase text-[#e0e0e0]">GPU</h2>
|
||||
<span className="tech-sn">NVIDIA RTX 4090</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-around py-1">
|
||||
<GaugeArc value={data.gpuTemp} max={90} label="温度" unit="°C" color="#ff4500" />
|
||||
<GaugeArc value={data.gpuUsage} max={100} label="负载" unit="%" color="#ff4500" />
|
||||
<GaugeArc value={data.gpuFreq / 100} max={33} label="频率" unit="MHz" color="#ff4500" />
|
||||
</div>
|
||||
|
||||
<div className="border-t border-[#333] pt-2 space-y-1">
|
||||
<BarGauge value={data.gpuMem} max={data.gpuMemTotal} label="显存" unit="GB" color="#ff4500" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* RAM */}
|
||||
<Card serial="RAM-01">
|
||||
<div className="p-2.5 space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="material-symbols-outlined text-[14px] text-[#ff4500]">storage</span>
|
||||
<h2 className="text-[9px] font-bold tracking-[0.15em] uppercase text-[#e0e0e0]">内存</h2>
|
||||
<span className="tech-sn">DDR5 32GB</span>
|
||||
</div>
|
||||
<BarGauge value={data.ramUsage} max={data.ramTotal} label="" unit="GB" color="#ff4500" />
|
||||
<p className="text-[7px] font-mono text-[#555] text-right">{data.ramUsage} / {data.ramTotal} GB</p>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Status footer */}
|
||||
<Card className="p-2 mt-auto" serial="HWM-01">
|
||||
<p className="text-[8px] font-mono text-[#555] leading-relaxed">
|
||||
[INFO] 数据模拟中。对接 MaqiangTangHardwareMonitor.exe 后显示真实传感器数据。
|
||||
</p>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
104
src/pages/Home.tsx
Normal file
104
src/pages/Home.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import DesktopGrid from '../components/layout/DesktopGrid';
|
||||
import Card from '../components/ui/Card';
|
||||
import { useAuthStore } from '../stores/authStore';
|
||||
|
||||
export default function Home() {
|
||||
const navigate = useNavigate();
|
||||
const user = useAuthStore(s => s.user);
|
||||
const isLoggedIn = !!user;
|
||||
const isVip = isLoggedIn; // TODO: proper VIP check
|
||||
|
||||
// 模块定义:名称/权限对齐原版
|
||||
const desktopItems = [
|
||||
{ id: 'optimization', icon: 'tune', label: '快速优化', action: () => navigate('/optimization'), vip: true },
|
||||
{ id: 'exposure', icon: 'palette', label: '画面滤镜', action: () => navigate('/exposure'), vip: true },
|
||||
{ id: 'filter-community', icon: 'photo_camera', label: '滤镜社区', action: () => navigate('/filter-community'), vip: true },
|
||||
{ id: 'hardware-monitor', icon: 'monitor_heart', label: '硬件监控', action: () => navigate('/hardware-monitor') },
|
||||
{ id: 'weapon', icon: 'crosshair', label: '改枪方案', action: () => navigate('/weapon'), loginRequired: true },
|
||||
{ id: 'crosshair', icon: 'gps_fixed', label: '游戏准星', action: () => navigate('/crosshair'), loginRequired: true },
|
||||
{ id: 'xixi-haha', icon: 'sentiment_satisfied', label: '神秘力量', action: () => navigate('/xixi-haha'), loginRequired: true },
|
||||
{ id: 'forbidden-force', icon: 'block', label: '嘉豪之力', action: () => navigate('/forbidden-force'), vip: true },
|
||||
];
|
||||
|
||||
const handleAction = (item: typeof desktopItems[0]) => {
|
||||
if (item.vip && !isVip) { navigate('/login'); return; }
|
||||
if (item.loginRequired && !isLoggedIn) { navigate('/login'); return; }
|
||||
item.action();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-1 gap-3 p-3">
|
||||
{/* 状态条 */}
|
||||
<Card className="flex items-center justify-between px-3 py-2" serial="SYS-01A">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-1.5 h-1.5 bg-[#ff4500]" />
|
||||
<span className="text-[10px] font-semibold tracking-[0.12em] uppercase text-[#e0e0e0]">
|
||||
{user ? `用户: ${user.username}` : '未登录'}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => navigate(user ? '/settings' : '/login')}
|
||||
className="text-[9px] font-mono text-[#555] hover:text-[#ff4500] transition-colors duration-75"
|
||||
>
|
||||
{user ? '设置 ▸' : '登录 ▸'}
|
||||
</button>
|
||||
</Card>
|
||||
|
||||
{/* 功能网格 */}
|
||||
<div className="relative">
|
||||
<span className="tech-sn absolute -top-2 left-0">功能模块 v2</span>
|
||||
<DesktopGrid
|
||||
items={desktopItems.map(item => ({
|
||||
...item,
|
||||
action: () => handleAction(item),
|
||||
locked: (item.vip && !isVip) || (item.loginRequired && !isLoggedIn),
|
||||
}))}
|
||||
rows={2}
|
||||
cols={4}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 社区动态 */}
|
||||
<Card className="p-3 mt-1" serial="FEED-01">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h2 className="text-[9px] font-semibold tracking-[0.15em] uppercase text-[#888]">最新动态</h2>
|
||||
<span className="tech-sn">实时</span>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
{[1, 2, 3].map(i => (
|
||||
<div key={i} className="flex items-center gap-2 text-[9px] text-[#555] border-b border-[#333] pb-1 last:border-b-0 last:pb-0">
|
||||
<span className="w-1 h-1 bg-[#333] rounded-full" />
|
||||
<span className="font-mono text-[8px] text-[#444]">[{String(i).padStart(4, '0')}]</span>
|
||||
<span>社区动态 #{i}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 底部启动按钮 */}
|
||||
<div className="flex gap-2 border border-[#333] p-2">
|
||||
<button
|
||||
onClick={() => {}}
|
||||
className="flex-1 border border-[#333] py-2 text-[8px] font-semibold tracking-[0.1em] uppercase text-[#555] hover:text-[#e0e0e0] hover:border-[#555] transition-colors duration-75"
|
||||
>
|
||||
启动硬件监控
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {}}
|
||||
className="flex-1 border border-[#ff4500]/30 py-2 text-[8px] font-semibold tracking-[0.1em] uppercase text-[#ff4500] hover:bg-[#ff4500]/10 transition-colors duration-75"
|
||||
>
|
||||
启动码枪堂 2.0
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 底部技术信息 */}
|
||||
<div className="flex items-center justify-between mt-auto pt-2">
|
||||
<span className="tech-sn">系统: {typeof navigator !== 'undefined' && navigator.platform ? navigator.platform.substring(0, 8) : 'UNKNOWN'}</span>
|
||||
<span className="tech-sn">节点: MBKPro-01</span>
|
||||
<span className="tech-sn">运行: --</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
140
src/pages/Login.tsx
Normal file
140
src/pages/Login.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import Button from '../components/ui/Button';
|
||||
import Input from '../components/ui/Input';
|
||||
import Card from '../components/ui/Card';
|
||||
import { useAuth } from '../hooks/useAuth';
|
||||
import { useAuthStore } from '../stores/authStore';
|
||||
import { getVipStatus, activateVip } from '../services/auth.api';
|
||||
|
||||
export default function Login() {
|
||||
const navigate = useNavigate();
|
||||
const { login } = useAuth();
|
||||
const isLoggedIn = useAuthStore(s => s.isLoggedIn);
|
||||
|
||||
const [tab, setTab] = useState<'login' | 'register' | 'vip'>('login');
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
const [cardKey, setCardKey] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const handleLogin = async () => {
|
||||
setLoading(true); setError('');
|
||||
try {
|
||||
await login({
|
||||
username, password,
|
||||
installId: 'maqt-desktop',
|
||||
deviceHash: 'maqt-desktop',
|
||||
platform: navigator.platform || 'win32',
|
||||
osVersion: navigator.userAgent || '',
|
||||
appVersion: '7.0.4',
|
||||
});
|
||||
navigate('/');
|
||||
} catch (e: any) {
|
||||
setError(e.message || '登录失败');
|
||||
} finally { setLoading(false); }
|
||||
};
|
||||
|
||||
const handleActivateVip = async () => {
|
||||
setLoading(true); setError('');
|
||||
try {
|
||||
await activateVip(cardKey);
|
||||
const status = await getVipStatus();
|
||||
if (status) navigate('/');
|
||||
} catch (e: any) {
|
||||
setError(e.message || '激活失败');
|
||||
} finally { setLoading(false); }
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-screen bg-[#1a1a1a] flex flex-col items-center justify-center p-6">
|
||||
{/* 品牌 */}
|
||||
<div className="text-center mb-6">
|
||||
<h1 className="text-[14px] font-bold tracking-[0.2em] uppercase text-[#e0e0e0]">码枪堂 2.0</h1>
|
||||
<p className="text-[8px] font-mono text-[#555] mt-1">用户认证系统</p>
|
||||
</div>
|
||||
|
||||
{/* 认证卡片 */}
|
||||
<Card className="w-full max-w-xs" serial="AUTH-01">
|
||||
{/* 标签页 */}
|
||||
<div className="flex border-b border-[#333]">
|
||||
{(['login', 'register', 'vip'] as const).map(t => (
|
||||
<button
|
||||
key={t}
|
||||
onClick={() => setTab(t)}
|
||||
className={`flex-1 py-2 text-[8px] font-semibold tracking-[0.12em] uppercase transition-colors duration-75
|
||||
border-r border-[#333] last:border-r-0
|
||||
${tab === t ? 'bg-[#ff4500]/10 text-[#ff4500]' : 'text-[#555] hover:text-[#888]'}`}
|
||||
>
|
||||
{t === 'login' ? '登录' : t === 'register' ? '注册' : 'VIP'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 表单 */}
|
||||
<div className="p-3 space-y-2.5">
|
||||
{tab === 'vip' ? (
|
||||
<>
|
||||
<Input
|
||||
placeholder="VIP 卡密"
|
||||
value={cardKey}
|
||||
onChange={e => setCardKey(e.target.value)}
|
||||
style={{ borderRadius: 0, borderColor: '#333', background: '#111', color: '#e0e0e0', fontSize: 10 }}
|
||||
/>
|
||||
<Button variant="primary" size="sm" className="w-full text-[9px]" onClick={handleActivateVip} loading={loading}>
|
||||
激活 VIP
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Input
|
||||
placeholder="用户名"
|
||||
value={username}
|
||||
onChange={e => setUsername(e.target.value)}
|
||||
style={{ borderRadius: 0, borderColor: '#333', background: '#111', color: '#e0e0e0', fontSize: 10 }}
|
||||
/>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="密码"
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
style={{ borderRadius: 0, borderColor: '#333', background: '#111', color: '#e0e0e0', fontSize: 10 }}
|
||||
/>
|
||||
{tab === 'register' && (
|
||||
<Input
|
||||
placeholder="邮箱(可选)"
|
||||
value={email}
|
||||
onChange={e => setEmail(e.target.value)}
|
||||
style={{ borderRadius: 0, borderColor: '#333', background: '#111', color: '#e0e0e0', fontSize: 10 }}
|
||||
/>
|
||||
)}
|
||||
<Button variant="primary" size="sm" className="w-full text-[9px]" onClick={handleLogin} loading={loading}>
|
||||
{tab === 'login' ? '登录' : '注册'}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<p className="text-[8px] font-mono text-[#cc3300] border border-[#cc3300]/30 p-1.5 bg-[#cc3300]/5">
|
||||
✕ {error}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<p className="text-[7px] font-mono text-[#444] text-center">
|
||||
SYS-AUTH v1.0
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 返回 */}
|
||||
<button
|
||||
onClick={() => navigate('/')}
|
||||
className="mt-4 text-[8px] font-mono text-[#555] hover:text-[#888] transition-colors duration-75"
|
||||
>
|
||||
← 返回桌面
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
114
src/pages/Optimization.tsx
Normal file
114
src/pages/Optimization.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import React, { useState } from 'react';
|
||||
import OptimizePanel from '../components/optimization/OptimizePanel';
|
||||
import OptimizeResult from '../components/optimization/OptimizeResult';
|
||||
import Button from '../components/ui/Button';
|
||||
import Card from '../components/ui/Card';
|
||||
import { useToast } from '../components/ui/Toast';
|
||||
import { useAuthStore } from '../stores/authStore';
|
||||
import type { OptimizeItemData } from '../components/optimization/OptimizeItem';
|
||||
|
||||
const INITIAL_ITEMS: OptimizeItemData[] = [
|
||||
{ id: 'power-plan', label: '卓越性能电源', description: '启用 Windows 卓越性能电源计划,释放 CPU/GPU 全部性能', category: 'system', icon: 'bolt', status: 'pending' },
|
||||
{ id: 'gpu-sched', label: 'GPU 硬件加速', description: '启用硬件加速 GPU 调度,降低游戏输入延迟', category: 'system', icon: 'speed', status: 'pending' },
|
||||
{ id: 'game-mode', label: '游戏模式', description: '启用 Windows 游戏模式,优化游戏资源分配', category: 'system', icon: 'sports_esports', status: 'pending' },
|
||||
{ id: 'hyperv', label: '关闭 Hyper-V', description: '关闭 Hyper-V 虚拟机服务,释放系统资源', category: 'services', icon: 'memory', status: 'pending' },
|
||||
{ id: 'services', label: '系统服务优化', description: '关闭非必要的 Windows 后台服务,减少资源占用', category: 'services', icon: 'build', status: 'pending' },
|
||||
{ id: 'hpet', label: 'HPET 高性能', description: '切换 HPET 为高性能模式,提升计时精度', category: 'services', icon: 'timer', status: 'pending' },
|
||||
{ id: 'network', label: '网络延迟优化', description: '优化 TCP/IP 参数,降低网络延迟', category: 'network', icon: 'wifi', status: 'pending' },
|
||||
{ id: 'dns', label: 'DNS 缓存重置', description: '重置 DNS 解析缓存,优化域名解析速度', category: 'network', icon: 'dns', status: 'pending' },
|
||||
{ id: 'mouse', label: '关闭鼠标加速', description: '关闭鼠标加速度,提升瞄准操控精准度', category: 'display', icon: 'mouse', status: 'pending' },
|
||||
{ id: 'visual', label: '关闭视觉特效', description: '关闭窗口动画和透明效果,提升帧率', category: 'display', icon: 'visibility_off', status: 'pending' },
|
||||
{ id: 'memory', label: '内存清理', description: '清理非必要内存占用,释放可用内存', category: 'memory', icon: 'clear_all', status: 'pending' },
|
||||
];
|
||||
|
||||
export default function Optimization() {
|
||||
const isLoggedIn = useAuthStore(s => s.isLoggedIn);
|
||||
const toast = useToast();
|
||||
|
||||
const [items, setItems] = useState<OptimizeItemData[]>(INITIAL_ITEMS);
|
||||
const [result, setResult] = useState<{ success: boolean; message: string } | null>(null);
|
||||
const [running, setRunning] = useState(false);
|
||||
|
||||
const handleToggle = async (id: string, action: 'optimize' | 'restore') => {
|
||||
if (!isLoggedIn) { toast('warning', '请先登录'); return; }
|
||||
setRunning(true);
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
const success = Math.random() > 0.1;
|
||||
setItems(prev => prev.map(i =>
|
||||
i.id === id
|
||||
? { ...i, status: success ? (action === 'optimize' ? 'optimized' as const : 'restored' as const) : 'error' as const }
|
||||
: i
|
||||
));
|
||||
toast(success ? 'success' : 'error', `${id} ${action === 'optimize' ? '优化' : '恢复'}${success ? '成功' : '失败'}`);
|
||||
setRunning(false);
|
||||
};
|
||||
|
||||
const handleOneClick = async () => {
|
||||
if (!isLoggedIn) { toast('warning', '请先登录'); return; }
|
||||
setRunning(true);
|
||||
const pending = items.filter(i => i.status !== 'optimized');
|
||||
setItems(INITIAL_ITEMS);
|
||||
|
||||
for (const item of pending) {
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
setItems(prev => prev.map(i => i.id === item.id ? { ...i, status: 'optimized' as const } : i));
|
||||
}
|
||||
setResult({ success: true, message: `已完成 ${pending.length} 项优化` });
|
||||
setRunning(false);
|
||||
};
|
||||
|
||||
const systemItems = items.filter(i => i.category === 'system');
|
||||
const serviceItems = items.filter(i => i.category === 'services');
|
||||
const networkItems = items.filter(i => i.category === 'network');
|
||||
const displayItems = items.filter(i => i.category === 'display');
|
||||
const memoryItems = items.filter(i => i.category === 'memory');
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3 p-3 flex-1">
|
||||
{/* 标题 */}
|
||||
<div className="flex items-center justify-between border-b border-[#333] pb-2">
|
||||
<h1 className="text-[11px] font-bold tracking-[0.15em] uppercase">快速优化</h1>
|
||||
<span className="tech-sn">PHASE-05</span>
|
||||
</div>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={handleOneClick}
|
||||
loading={running}
|
||||
className="flex-1 text-[9px]"
|
||||
>
|
||||
⚡ 一键优化 ({items.filter(i => i.status !== 'optimized').length})
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" onClick={() => setItems(INITIAL_ITEMS)} disabled={running}>
|
||||
重置
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 结果 */}
|
||||
{result && (
|
||||
<OptimizeResult
|
||||
success={result.success}
|
||||
message={result.message}
|
||||
onClose={() => setResult(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 优化面板 */}
|
||||
<OptimizePanel title="系统性能" icon="⚡" items={systemItems} onToggle={handleToggle} disabled={running} serial="SYS-01" />
|
||||
<OptimizePanel title="系统服务" icon="⚙" items={serviceItems} onToggle={handleToggle} disabled={running} serial="SVC-02" defaultOpen={false} />
|
||||
<OptimizePanel title="网络优化" icon="🌐" items={networkItems} onToggle={handleToggle} disabled={running} serial="NET-03" defaultOpen={false} />
|
||||
<OptimizePanel title="显示优化" icon="🖥" items={displayItems} onToggle={handleToggle} disabled={running} serial="DSP-04" defaultOpen={false} />
|
||||
<OptimizePanel title="内存优化" icon="💾" items={memoryItems} onToggle={handleToggle} disabled={running} serial="MEM-05" defaultOpen={false} />
|
||||
|
||||
{/* 提示 */}
|
||||
<Card className="p-2 mt-auto" serial="HINT-01">
|
||||
<p className="text-[8px] font-mono text-[#555] leading-relaxed">
|
||||
[提示] 快速优化需要管理员权限。部分优化项重启后生效。
|
||||
</p>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
114
src/pages/SchemeDetail.tsx
Normal file
114
src/pages/SchemeDetail.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import Card from '../components/ui/Card';
|
||||
import Button from '../components/ui/Button';
|
||||
import { getSchemeById, useScheme } from '../services/schemes.api';
|
||||
import { addFavorite, removeFavorite } from '../services/favorites.api';
|
||||
import { useAuthStore } from '../stores/authStore';
|
||||
import { useToast } from '../components/ui/Toast';
|
||||
import type { SchemeData } from '../components/schemes/SchemeCard';
|
||||
|
||||
export default function SchemeDetail() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const toast = useToast();
|
||||
const isLoggedIn = useAuthStore(s => s.isLoggedIn);
|
||||
|
||||
const [scheme, setScheme] = useState<SchemeData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [isFavorited, setIsFavorited] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return;
|
||||
setLoading(true);
|
||||
getSchemeById(id).then(r => {
|
||||
if (r.success && r.data) setScheme(r.data as SchemeData);
|
||||
}).catch(() => {}).finally(() => setLoading(false));
|
||||
}, [id]);
|
||||
|
||||
const handleUse = async () => {
|
||||
if (!isLoggedIn) { toast('warning', '请先登录'); navigate('/login'); return; }
|
||||
if (!scheme) return;
|
||||
try {
|
||||
await useScheme(scheme.id, scheme.category || 'AR');
|
||||
toast('success', '方案使用记录已保存');
|
||||
} catch { toast('error', '操作失败'); }
|
||||
};
|
||||
|
||||
const handleFavorite = async () => {
|
||||
if (!isLoggedIn) { toast('warning', '请先登录'); navigate('/login'); return; }
|
||||
if (!scheme) return;
|
||||
try {
|
||||
if (isFavorited) {
|
||||
await removeFavorite(scheme.id);
|
||||
setIsFavorited(false);
|
||||
toast('success', '已取消收藏');
|
||||
} else {
|
||||
await addFavorite({ targetId: scheme.id, targetType: 'Scheme' });
|
||||
setIsFavorited(true);
|
||||
toast('success', '已收藏');
|
||||
}
|
||||
} catch { toast('error', '操作失败'); }
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex flex-col gap-3 p-3 flex-1">
|
||||
<div className="flex items-center justify-between border-b border-[#333] pb-2">
|
||||
<h1 className="text-[11px] font-bold tracking-[0.15em] uppercase">方案详情</h1>
|
||||
<span className="tech-sn">LOADING...</span>
|
||||
</div>
|
||||
<Card className="p-4"><div className="h-20 animate-pulse bg-[#222]" /></Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!scheme) {
|
||||
return (
|
||||
<div className="flex flex-col gap-3 p-3 flex-1">
|
||||
<div className="flex items-center justify-between border-b border-[#333] pb-2">
|
||||
<h1 className="text-[11px] font-bold tracking-[0.15em] uppercase">方案详情</h1>
|
||||
<span className="tech-sn">404</span>
|
||||
</div>
|
||||
<Card className="p-3" serial="ERR-01">
|
||||
<p className="text-[9px] font-mono text-[#555]">方案未找到或已删除</p>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3 p-3 flex-1">
|
||||
<div className="flex items-center justify-between border-b border-[#333] pb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<button onClick={() => navigate(-1)} className="text-[#555] hover:text-[#e0e0e0] text-[10px] transition-colors duration-75">←</button>
|
||||
<h1 className="text-[11px] font-bold tracking-[0.15em] uppercase">方案详情</h1>
|
||||
</div>
|
||||
{scheme.isOfficial && <span className="text-[7px] font-semibold tracking-[0.1em] uppercase text-[#ff4500] border border-[#ff4500]/30 px-1 py-0.5">官方</span>}
|
||||
</div>
|
||||
|
||||
<Card serial={`SCM-${scheme.id.slice(0, 4)}`}>
|
||||
<div className="p-3 space-y-2">
|
||||
<h2 className="text-[12px] font-bold tracking-[0.1em] uppercase">{scheme.title || '未命名方案'}</h2>
|
||||
{scheme.weaponName && <p className="text-[9px] font-mono text-[#555]">武器: {scheme.weaponName}</p>}
|
||||
{scheme.user?.username && <p className="text-[9px] font-mono text-[#555]">作者: {scheme.user.username}</p>}
|
||||
{scheme.price > 0 && <p className="text-[9px] font-mono text-[#555]">价格: {scheme.price}</p>}
|
||||
|
||||
<div className="flex items-center gap-3 text-[8px] font-mono text-[#555] border-t border-[#333] pt-2">
|
||||
<span>👁 浏览 {scheme.viewsCount}</span>
|
||||
<span>⬇ 下载 {scheme.downloadsCount}</span>
|
||||
<span>👍 点赞 {scheme.likesCount}</span>
|
||||
<span>★ 收藏 {scheme.favoritesCount}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button variant="primary" size="sm" className="flex-1 text-[9px]" onClick={handleUse}>使用方案</Button>
|
||||
<Button variant="ghost" size="sm" className="text-[9px]" onClick={handleFavorite}>
|
||||
{isFavorited ? '★ 已收藏' : '☆ 收藏'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
39
src/pages/Settings.tsx
Normal file
39
src/pages/Settings.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
import Card from '../components/ui/Card';
|
||||
|
||||
export default function Settings() {
|
||||
return (
|
||||
<div className="flex flex-col gap-3 p-3 flex-1">
|
||||
<div className="flex items-center justify-between border-b border-[#333] pb-2">
|
||||
<h1 className="text-[11px] font-bold tracking-[0.15em] uppercase">设置</h1>
|
||||
<span className="tech-sn">CFG-01</span>
|
||||
</div>
|
||||
|
||||
<Card serial="CFG-ACT">
|
||||
<div className="p-3">
|
||||
<h2 className="text-[10px] font-bold tracking-[0.12em] uppercase mb-2">账户</h2>
|
||||
<div className="space-y-1.5 text-[9px] font-mono text-[#555]">
|
||||
<p>用户状态: 未登录</p>
|
||||
<p>VIP: 未激活</p>
|
||||
<p>设备ID: maqt-desktop-01</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card serial="CFG-APP">
|
||||
<div className="p-3">
|
||||
<h2 className="text-[10px] font-bold tracking-[0.12em] uppercase mb-2">应用</h2>
|
||||
<div className="space-y-1.5 text-[9px] font-mono text-[#555]">
|
||||
<p>版本: 0.2.1</p>
|
||||
<p>API: 待配置</p>
|
||||
<p>代理: 无</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-2 mt-auto" serial="HINT-02">
|
||||
<p className="text-[8px] font-mono text-[#555] leading-relaxed">更多设置项开发中</p>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
115
src/pages/WeaponSchemes.tsx
Normal file
115
src/pages/WeaponSchemes.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import CategoryTabs from '../components/schemes/CategoryTabs';
|
||||
import SchemeList from '../components/schemes/SchemeList';
|
||||
import type { SchemeData } from '../components/schemes/SchemeCard';
|
||||
import { getSchemes, getCategory, useScheme } from '../services/schemes.api';
|
||||
import { addFavorite, removeFavorite, checkFavorite } from '../services/favorites.api';
|
||||
import { useAuthStore } from '../stores/authStore';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useToast } from '../components/ui/Toast';
|
||||
|
||||
const DEFAULT_CATEGORIES = [
|
||||
{ code: 'AR', name: '突击步枪' }, { code: 'SMG', name: '冲锋枪' },
|
||||
{ code: 'SR', name: '狙击枪' }, { code: 'LMG', name: '轻机枪' },
|
||||
{ code: 'SG', name: '霰弹枪' }, { code: 'Pistol', name: '手枪' },
|
||||
{ code: 'Launcher', name: '发射器' },
|
||||
];
|
||||
|
||||
export default function WeaponSchemes() {
|
||||
const navigate = useNavigate();
|
||||
const toast = useToast();
|
||||
const isLoggedIn = useAuthStore(s => s.isLoggedIn);
|
||||
|
||||
const [categories, setCategories] = useState<Array<{ code: string; name: string }>>(DEFAULT_CATEGORIES);
|
||||
const [activeCode, setActiveCode] = useState('AR');
|
||||
const [schemes, setSchemes] = useState<SchemeData[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [page, setPage] = useState(1);
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
const [favoritedIds, setFavoritedIds] = useState<Set<string>>(new Set());
|
||||
const [sort, setSort] = useState<'hot' | 'new'>('hot');
|
||||
|
||||
useEffect(() => {
|
||||
getCategory('AR').then(r => {
|
||||
if (r.success && Array.isArray(r.data) && r.data.length > 0) {
|
||||
setCategories(r.data);
|
||||
}
|
||||
}).catch(() => {});
|
||||
}, []);
|
||||
|
||||
const loadSchemes = useCallback(async (code: string, p: number, s: string) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await getSchemes({ sort: s, page: p, limit: 12, source: code, weaponCategory: code });
|
||||
if (res.success && Array.isArray(res.data)) {
|
||||
setSchemes(res.data || []);
|
||||
setTotalPages(Math.ceil((res as any).total || res.data.length / 12));
|
||||
} else { setSchemes([]); }
|
||||
} catch { setSchemes([]); }
|
||||
finally { setLoading(false); }
|
||||
}, []);
|
||||
|
||||
useEffect(() => { loadSchemes(activeCode, page, sort); }, [activeCode, page, sort, loadSchemes]);
|
||||
|
||||
const handleCategoryChange = (code: string) => { setActiveCode(code); setPage(1); };
|
||||
|
||||
const handleFavorite = async (id: string) => {
|
||||
if (!isLoggedIn) { toast('warning', '请先登录'); navigate('/login'); return; }
|
||||
try {
|
||||
if (favoritedIds.has(id)) {
|
||||
await removeFavorite(id);
|
||||
setFavoritedIds(prev => { const n = new Set(prev); n.delete(id); return n; });
|
||||
toast('success', '已取消收藏');
|
||||
} else {
|
||||
await addFavorite({ targetId: id, targetType: 'Scheme' });
|
||||
setFavoritedIds(prev => new Set(prev).add(id));
|
||||
toast('success', '已收藏');
|
||||
}
|
||||
} catch { toast('error', '操作失败'); }
|
||||
};
|
||||
|
||||
const handleUse = async (id: string) => {
|
||||
if (!isLoggedIn) { toast('warning', '请先登录'); navigate('/login'); return; }
|
||||
try {
|
||||
await useScheme(id, activeCode);
|
||||
toast('success', '方案使用记录已保存');
|
||||
} catch { toast('error', '操作失败'); }
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3 p-3 flex-1">
|
||||
{/* 标题 */}
|
||||
<div className="flex items-center justify-between border-b border-[#333] pb-2">
|
||||
<h1 className="text-[11px] font-bold tracking-[0.15em] uppercase">改枪方案</h1>
|
||||
<div className="flex gap-1">
|
||||
{(['hot', 'new'] as const).map(s => (
|
||||
<button
|
||||
key={s}
|
||||
onClick={() => { setSort(s); setPage(1); }}
|
||||
className={`px-2 py-1 text-[8px] font-semibold tracking-[0.08em] uppercase border transition-colors duration-75
|
||||
${sort === s ? 'border-[#ff4500] text-[#ff4500] bg-[#ff4500]/10' : 'border-[#333] text-[#555] hover:border-[#555]'}`}
|
||||
>
|
||||
{s === 'hot' ? '🔥 热门' : '🕐 最新'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 分类标签 */}
|
||||
<CategoryTabs categories={categories} activeCode={activeCode} onSelect={handleCategoryChange} />
|
||||
|
||||
{/* 方案列表 */}
|
||||
<SchemeList
|
||||
schemes={schemes}
|
||||
loading={loading}
|
||||
favoritedIds={favoritedIds}
|
||||
page={page}
|
||||
totalPages={totalPages}
|
||||
onPageChange={setPage}
|
||||
onFavorite={handleFavorite}
|
||||
onUse={handleUse}
|
||||
onClick={(id) => navigate(`/scheme-detail/${id}`)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
16
src/pages/XixiHaha.tsx
Normal file
16
src/pages/XixiHaha.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import Card from '../components/ui/Card';
|
||||
|
||||
export default function XixiHaha() {
|
||||
return (
|
||||
<div className="flex flex-col gap-3 p-3 flex-1">
|
||||
<div className="flex items-center justify-between border-b border-[#333] pb-2">
|
||||
<h1 className="text-[11px] font-bold tracking-[0.15em] uppercase">神秘力量</h1>
|
||||
<span className="tech-sn">DEV-03</span>
|
||||
</div>
|
||||
<Card className="p-3" serial="WIP-03">
|
||||
<p className="text-[9px] font-mono text-[#555]">此功能开发中</p>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
80
src/router.tsx
Normal file
80
src/router.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import React, { Suspense, lazy } from 'react';
|
||||
import { Routes, Route } from 'react-router-dom';
|
||||
import PageContainer from './components/layout/PageContainer';
|
||||
import TopHud from './components/layout/TopHud';
|
||||
|
||||
const Home = lazy(() => import('./pages/Home'));
|
||||
const Optimization = lazy(() => import('./pages/Optimization'));
|
||||
const Exposure = lazy(() => import('./pages/Exposure'));
|
||||
const FilterCommunity = lazy(() => import('./pages/FilterCommunity'));
|
||||
const WeaponSchemes = lazy(() => import('./pages/WeaponSchemes'));
|
||||
const SchemeDetail = lazy(() => import('./pages/SchemeDetail'));
|
||||
const Crosshair = lazy(() => import('./pages/Crosshair'));
|
||||
const ForbiddenForce = lazy(() => import('./pages/ForbiddenForce'));
|
||||
const XixiHaha = lazy(() => import('./pages/XixiHaha'));
|
||||
const HardwareMonitor = lazy(() => import('./pages/HardwareMonitor'));
|
||||
const Settings = lazy(() => import('./pages/Settings'));
|
||||
const Login = lazy(() => import('./pages/Login'));
|
||||
|
||||
const pageTitles: Record<string, string> = {
|
||||
home: '桌面',
|
||||
optimization: '快速优化',
|
||||
exposure: '画面滤镜',
|
||||
'filter-community': '滤镜社区',
|
||||
weapon: '改枪方案',
|
||||
crosshair: '游戏准星',
|
||||
'forbidden-force': '嘉豪之力',
|
||||
'xixi-haha': '神秘力量',
|
||||
'hardware-monitor': '硬件监控',
|
||||
settings: '设置',
|
||||
};
|
||||
|
||||
function PageShell({ children, currentPage }: { children: React.ReactNode; currentPage: string }) {
|
||||
return (
|
||||
<div className="h-screen bg-[#1a1a1a] text-[#e0e0e0] antialiased overflow-hidden flex flex-col" style={{ fontFamily: 'Inter, sans-serif' }}>
|
||||
<TopHud
|
||||
title="码枪堂 2.0"
|
||||
subtitle={pageTitles[currentPage] || currentPage.toUpperCase()}
|
||||
sections={[
|
||||
{ label: '状态', value: '在线' },
|
||||
{ label: '模式', value: '本地' },
|
||||
]}
|
||||
/>
|
||||
<PageContainer>
|
||||
<Suspense fallback={<LoadingFallback />}>
|
||||
{children}
|
||||
</Suspense>
|
||||
</PageContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function LoadingFallback() {
|
||||
return (
|
||||
<div className="flex items-center justify-center flex-1">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="w-2 h-2 border border-[#555] animate-pulse" />
|
||||
<span className="text-[9px] font-mono text-[#555] tracking-[0.15em] uppercase">加载中...</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function AppRouter() {
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/" element={<PageShell currentPage="home"><Home /></PageShell>} />
|
||||
<Route path="/optimization" element={<PageShell currentPage="optimization"><Optimization /></PageShell>} />
|
||||
<Route path="/exposure" element={<PageShell currentPage="exposure"><Exposure /></PageShell>} />
|
||||
<Route path="/filter-community" element={<PageShell currentPage="filter-community"><FilterCommunity /></PageShell>} />
|
||||
<Route path="/weapon" element={<PageShell currentPage="weapon"><WeaponSchemes /></PageShell>} />
|
||||
<Route path="/scheme-detail/:id" element={<PageShell currentPage="weapon"><SchemeDetail /></PageShell>} />
|
||||
<Route path="/crosshair" element={<PageShell currentPage="crosshair"><Crosshair /></PageShell>} />
|
||||
<Route path="/forbidden-force" element={<PageShell currentPage="forbidden-force"><ForbiddenForce /></PageShell>} />
|
||||
<Route path="/xixi-haha" element={<PageShell currentPage="xixi-haha"><XixiHaha /></PageShell>} />
|
||||
<Route path="/hardware-monitor" element={<PageShell currentPage="hardware-monitor"><HardwareMonitor /></PageShell>} />
|
||||
<Route path="/settings" element={<PageShell currentPage="settings"><Settings /></PageShell>} />
|
||||
<Route path="/login" element={<Login />} />
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
30
src/services/api.ts
Normal file
30
src/services/api.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import axios from 'axios';
|
||||
|
||||
const API_BASE = 'https://gch3n.online/delta';
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: API_BASE,
|
||||
timeout: 15000,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
|
||||
api.interceptors.request.use((config) => {
|
||||
if (typeof window !== 'undefined') {
|
||||
const token = localStorage.getItem('auth_token');
|
||||
if (token) config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
api.interceptors.response.use(
|
||||
(res) => res,
|
||||
(err) => {
|
||||
if (err.response?.status === 401 && typeof window !== 'undefined') {
|
||||
localStorage.removeItem('auth_token');
|
||||
localStorage.removeItem('auth_user');
|
||||
}
|
||||
return Promise.reject(err);
|
||||
}
|
||||
);
|
||||
|
||||
export default api;
|
||||
37
src/services/auth.api.ts
Normal file
37
src/services/auth.api.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import api from './api';
|
||||
import type { LoginRequest, LoginResponse, User, ApiResponse } from '../types';
|
||||
|
||||
export async function login(data: LoginRequest): Promise<LoginResponse> {
|
||||
const res = await api.post('/api/login', data);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function register(username: string, password: string, email: string): Promise<ApiResponse> {
|
||||
const res = await api.post('/api/register', { username, password, email });
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function getSessionStatus(): Promise<ApiResponse<{ valid: boolean; user: User }>> {
|
||||
const res = await api.get('/api/session-status');
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function getVipStatus(): Promise<ApiResponse<{ isVip: boolean; daysRemaining: number }>> {
|
||||
const res = await api.get('/api/vip-status');
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function activateVip(cardKey: string): Promise<ApiResponse> {
|
||||
const res = await api.post('/api/activate-vip', { cardKey });
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function getUserStats(userId: string): Promise<ApiResponse> {
|
||||
const res = await api.get(`/api/user/stats/${userId}`);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function getUserLimits(userId: string): Promise<ApiResponse> {
|
||||
const res = await api.get(`/api/user/limits/${userId}`);
|
||||
return res.data;
|
||||
}
|
||||
26
src/services/favorites.api.ts
Normal file
26
src/services/favorites.api.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import api from './api';
|
||||
|
||||
export async function getFavorites() {
|
||||
const res = await api.get('/api/favorites');
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function getFavoritesCount() {
|
||||
const res = await api.get('/api/favorites/count');
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function checkFavorite(schemeId: string, source?: string) {
|
||||
const res = await api.get('/api/favorites/check', { params: { schemeId, source } });
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function addFavorite(data: { targetId: string; targetType: string }) {
|
||||
const res = await api.post('/api/favorites', data);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function removeFavorite(id: string) {
|
||||
const res = await api.delete(`/api/favorites/${id}`);
|
||||
return res.data;
|
||||
}
|
||||
36
src/services/schemes.api.ts
Normal file
36
src/services/schemes.api.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import api from './api';
|
||||
import type { Scheme, ApiResponse, Category } from '../types';
|
||||
|
||||
interface SchemesParams {
|
||||
sort?: string;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
source?: string;
|
||||
weaponCategory?: string;
|
||||
weaponName?: string;
|
||||
}
|
||||
|
||||
export async function getSchemes(params?: SchemesParams): Promise<ApiResponse<Scheme[]>> {
|
||||
const res = await api.get('/api/schemes', { params });
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function getSchemeById(id: string): Promise<ApiResponse<Scheme>> {
|
||||
const res = await api.get(`/api/schemes/${id}`);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function getCategory(code: string): Promise<ApiResponse<Category[]>> {
|
||||
const res = await api.get(`/api/category/${code}`);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function useScheme(id: string, source?: string): Promise<ApiResponse<{ downloadsCount: number }>> {
|
||||
const res = await api.post(`/api/schemes/${id}/use`, { source });
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function getUserSchemes(userId: string): Promise<ApiResponse<Scheme[]>> {
|
||||
const res = await api.get(`/api/user/schemes/${userId}`);
|
||||
return res.data;
|
||||
}
|
||||
70
src/stores/authStore.ts
Normal file
70
src/stores/authStore.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { create } from 'zustand';
|
||||
import api from '../services/api';
|
||||
import type { User } from '../types';
|
||||
|
||||
interface AuthState {
|
||||
token: string | null;
|
||||
user: User | null;
|
||||
isLoggedIn: boolean;
|
||||
loading: boolean;
|
||||
login: (token: string, user: User) => void;
|
||||
logout: () => void;
|
||||
checkSession: () => Promise<boolean>;
|
||||
setUser: (user: User) => void;
|
||||
hydrate: () => void;
|
||||
}
|
||||
|
||||
export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
token: null,
|
||||
user: null,
|
||||
isLoggedIn: false,
|
||||
loading: false,
|
||||
|
||||
hydrate: () => {
|
||||
const token = localStorage.getItem('auth_token');
|
||||
const userStr = localStorage.getItem('auth_user');
|
||||
if (token && userStr) {
|
||||
try {
|
||||
const user = JSON.parse(userStr);
|
||||
set({ token, user, isLoggedIn: true });
|
||||
} catch { set({ token: null, user: null, isLoggedIn: false }); }
|
||||
}
|
||||
},
|
||||
|
||||
login: (token, user) => {
|
||||
localStorage.setItem('auth_token', token);
|
||||
localStorage.setItem('auth_user', JSON.stringify(user));
|
||||
set({ token, user, isLoggedIn: true });
|
||||
},
|
||||
|
||||
logout: () => {
|
||||
localStorage.removeItem('auth_token');
|
||||
localStorage.removeItem('auth_user');
|
||||
set({ token: null, user: null, isLoggedIn: false });
|
||||
},
|
||||
|
||||
checkSession: async () => {
|
||||
const { token } = get();
|
||||
if (!token) { set({ isLoggedIn: false }); return false; }
|
||||
set({ loading: true });
|
||||
try {
|
||||
const res = await api.get('/api/session-status');
|
||||
if (res.data.success && res.data.user) {
|
||||
set({ user: res.data.user, isLoggedIn: true, loading: false });
|
||||
return true;
|
||||
}
|
||||
get().logout();
|
||||
set({ loading: false });
|
||||
return false;
|
||||
} catch {
|
||||
get().logout();
|
||||
set({ loading: false });
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
setUser: (user) => {
|
||||
localStorage.setItem('auth_user', JSON.stringify(user));
|
||||
set({ user });
|
||||
},
|
||||
}));
|
||||
13
src/stores/navigationStore.ts
Normal file
13
src/stores/navigationStore.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
interface NavState {
|
||||
currentPage: string;
|
||||
navigate: (page: string) => void;
|
||||
goBack: () => void;
|
||||
}
|
||||
|
||||
export const useNavigationStore = create<NavState>((set) => ({
|
||||
currentPage: 'home',
|
||||
navigate: (page) => set({ currentPage: page }),
|
||||
goBack: () => set({ currentPage: 'home' }),
|
||||
}));
|
||||
24
src/stores/settingsStore.ts
Normal file
24
src/stores/settingsStore.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
interface SettingsState {
|
||||
autoLaunch: boolean;
|
||||
closePreference: 'ask' | 'tray' | 'quit';
|
||||
setAutoLaunch: (v: boolean) => void;
|
||||
setClosePreference: (v: 'ask' | 'tray' | 'quit') => void;
|
||||
hydrate: () => void;
|
||||
}
|
||||
|
||||
export const useSettingsStore = create<SettingsState>((set) => ({
|
||||
autoLaunch: false,
|
||||
closePreference: 'ask',
|
||||
setAutoLaunch: (v) => { localStorage.setItem('settings_autoLaunch', String(v)); set({ autoLaunch: v }); },
|
||||
setClosePreference: (v) => { localStorage.setItem('settings_closePref', v); set({ closePreference: v }); },
|
||||
hydrate: () => {
|
||||
const a = localStorage.getItem('settings_autoLaunch');
|
||||
const c = localStorage.getItem('settings_closePref');
|
||||
set({
|
||||
autoLaunch: a === 'true',
|
||||
closePreference: (c as any) || 'ask',
|
||||
});
|
||||
},
|
||||
}));
|
||||
252
src/styles/animations.css
Normal file
252
src/styles/animations.css
Normal file
@@ -0,0 +1,252 @@
|
||||
/* ===== 扫描线效果 ===== */
|
||||
@keyframes scanline {
|
||||
0% {
|
||||
transform: translateY(-100%);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(100%);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-scanline {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.animate-scanline::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
transparent 0%,
|
||||
rgba(42, 157, 111, 0.1) 50%,
|
||||
transparent 100%
|
||||
);
|
||||
animation: scanline 3s linear infinite;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* ===== 十字准星展开动画 ===== */
|
||||
@keyframes crosshair-expand {
|
||||
0% {
|
||||
transform: scale(0) rotate(45deg);
|
||||
opacity: 0;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1) rotate(0deg);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-crosshair {
|
||||
animation: crosshair-expand 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
|
||||
}
|
||||
|
||||
/* 十字准星线条 */
|
||||
.crosshair-line {
|
||||
position: absolute;
|
||||
background: var(--military-400);
|
||||
}
|
||||
|
||||
.crosshair-line.horizontal {
|
||||
width: 20px;
|
||||
height: 2px;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.crosshair-line.vertical {
|
||||
width: 2px;
|
||||
height: 20px;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
/* ===== 数字滚动动画 ===== */
|
||||
@keyframes digit-scroll {
|
||||
0% {
|
||||
transform: translateY(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
20% {
|
||||
opacity: 1;
|
||||
}
|
||||
80% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(-100%);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-digit {
|
||||
display: inline-block;
|
||||
animation: digit-scroll 0.3s ease-out forwards;
|
||||
}
|
||||
|
||||
/* 数字递增效果 */
|
||||
@keyframes count-up {
|
||||
from {
|
||||
--num: 0;
|
||||
}
|
||||
to {
|
||||
--num: var(--target);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-count-up {
|
||||
animation: count-up 0.5s ease-out forwards;
|
||||
counter-reset: num var(--num);
|
||||
}
|
||||
|
||||
.animate-count-up::after {
|
||||
content: counter(num);
|
||||
}
|
||||
|
||||
/* ===== 边框流光效果 ===== */
|
||||
@keyframes border-flow {
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-border-flow {
|
||||
position: relative;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
var(--charcoal-800),
|
||||
var(--military-600),
|
||||
var(--darkgold-500),
|
||||
var(--military-600),
|
||||
var(--charcoal-800)
|
||||
);
|
||||
background-size: 300% 100%;
|
||||
animation: border-flow 3s ease infinite;
|
||||
}
|
||||
|
||||
/* 流光边框容器 */
|
||||
.border-flow-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.border-flow-container::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
padding: 1px;
|
||||
border-radius: inherit;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
var(--military-600),
|
||||
var(--darkgold-500),
|
||||
var(--military-600)
|
||||
);
|
||||
background-size: 200% 100%;
|
||||
animation: border-flow 2s linear infinite;
|
||||
-webkit-mask:
|
||||
linear-gradient(#fff 0 0) content-box,
|
||||
linear-gradient(#fff 0 0);
|
||||
mask:
|
||||
linear-gradient(#fff 0 0) content-box,
|
||||
linear-gradient(#fff 0 0);
|
||||
-webkit-mask-composite: xor;
|
||||
mask-composite: exclude;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* ===== 脉冲效果 ===== */
|
||||
@keyframes pulse-glow {
|
||||
0%, 100% {
|
||||
box-shadow: 0 0 5px var(--military-500), 0 0 10px var(--military-500);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 15px var(--military-400), 0 0 25px var(--military-400);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-pulse-glow {
|
||||
animation: pulse-glow 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* ===== 闪烁效果 ===== */
|
||||
@keyframes blink {
|
||||
0%, 50% {
|
||||
opacity: 1;
|
||||
}
|
||||
51%, 100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-blink {
|
||||
animation: blink 1s step-end infinite;
|
||||
}
|
||||
|
||||
/* ===== 渐变扫描 ===== */
|
||||
@keyframes gradient-shift {
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-gradient {
|
||||
background-size: 200% 200%;
|
||||
animation: gradient-shift 5s ease infinite;
|
||||
}
|
||||
|
||||
/* ===== 悬浮动画 ===== */
|
||||
@keyframes float {
|
||||
0%, 100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-float {
|
||||
animation: float 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* ===== 数据流动效果 ===== */
|
||||
@keyframes data-flow {
|
||||
0% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-data-flow {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
rgba(42, 157, 111, 0.3),
|
||||
transparent
|
||||
);
|
||||
background-size: 50% 100%;
|
||||
animation: data-flow 2s linear infinite;
|
||||
}
|
||||
117
src/styles/globals.css
Normal file
117
src/styles/globals.css
Normal file
@@ -0,0 +1,117 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
scrollbar-width: none;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', 'Helvetica', sans-serif;
|
||||
background: #1a1a1a;
|
||||
color: #e0e0e0;
|
||||
overflow: hidden;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.material-symbols-outlined {
|
||||
font-family: 'Material Symbols Outlined';
|
||||
font-weight: 300;
|
||||
font-style: normal;
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
letter-spacing: normal;
|
||||
text-transform: none;
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
word-wrap: normal;
|
||||
direction: ltr;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-variation-settings: 'FILL' 0, 'wght' 300, 'GRAD' 0, 'opsz' 20;
|
||||
}
|
||||
|
||||
/* Tech label annotation — tiny all-caps text for corners/edges */
|
||||
.tech-tag {
|
||||
@apply text-[9px] tracking-[0.15em] uppercase font-mono;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
/* Crosshair decoration */
|
||||
.crosshair {
|
||||
position: relative;
|
||||
}
|
||||
.crosshair::before,
|
||||
.crosshair::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
background: #ff4500;
|
||||
opacity: 0.3;
|
||||
}
|
||||
.crosshair::before {
|
||||
width: 1px;
|
||||
height: 8px;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
.crosshair::after {
|
||||
width: 8px;
|
||||
height: 1px;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
/* Cut corner decoration (top-left) */
|
||||
.cut-corner-tl {
|
||||
position: relative;
|
||||
}
|
||||
.cut-corner-tl::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-top: 1px solid #ff4500;
|
||||
border-left: 1px solid #ff4500;
|
||||
opacity: 0.4;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Cut corner decoration (bottom-right) */
|
||||
.cut-corner-br {
|
||||
position: relative;
|
||||
}
|
||||
.cut-corner-br::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-bottom: 1px solid #ff4500;
|
||||
border-right: 1px solid #ff4500;
|
||||
opacity: 0.4;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Tech serial label */
|
||||
.tech-sn {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 8px;
|
||||
letter-spacing: 0.1em;
|
||||
color: #444;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
36
src/types/electron.d.ts
vendored
Normal file
36
src/types/electron.d.ts
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
export interface ElectronAPI {
|
||||
// 通用
|
||||
getAppVersion: () => Promise<string>;
|
||||
getPlatform: () => string;
|
||||
|
||||
// Overlay
|
||||
startOverlay: (options: any) => Promise<any>;
|
||||
stopOverlay: () => Promise<void>;
|
||||
|
||||
// 硬件监控
|
||||
startMonitor: () => Promise<void>;
|
||||
stopMonitor: () => Promise<void>;
|
||||
|
||||
// 文件/路径
|
||||
openExternal: (url: string) => Promise<void>;
|
||||
getResourcesPath: () => Promise<string>;
|
||||
existsSync: (path: string) => Promise<boolean>;
|
||||
|
||||
// 系统优化
|
||||
optimizeItem: (id: string) => Promise<any>;
|
||||
restoreItem: (id: string) => Promise<any>;
|
||||
getOptimizeItems: () => Promise<any[]>;
|
||||
|
||||
// 窗口控制
|
||||
minimizeWindow: () => Promise<void>;
|
||||
maximizeWindow: () => Promise<void>;
|
||||
closeWindow: () => Promise<void>;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
electronAPI: ElectronAPI;
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
101
src/types/global.d.ts
vendored
Normal file
101
src/types/global.d.ts
vendored
Normal file
@@ -0,0 +1,101 @@
|
||||
/// <reference types="electron" />
|
||||
|
||||
// 全局类型声明
|
||||
|
||||
declare namespace AppTypes {
|
||||
// 用户信息
|
||||
interface User {
|
||||
id: string;
|
||||
username: string;
|
||||
email: string;
|
||||
avatar?: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
// 硬件信息
|
||||
interface HardwareInfo {
|
||||
cpu: CPUInfo;
|
||||
gpu: GPUInfo;
|
||||
memory: MemoryInfo;
|
||||
storage: StorageInfo[];
|
||||
motherboard: MotherboardInfo;
|
||||
}
|
||||
|
||||
interface CPUInfo {
|
||||
name: string;
|
||||
cores: number;
|
||||
threads: number;
|
||||
baseFrequency: number;
|
||||
maxFrequency: number;
|
||||
temperature: number;
|
||||
usage: number;
|
||||
}
|
||||
|
||||
interface GPUInfo {
|
||||
name: string;
|
||||
vram: number;
|
||||
temperature: number;
|
||||
usage: number;
|
||||
clockSpeed: number;
|
||||
}
|
||||
|
||||
interface MemoryInfo {
|
||||
total: number;
|
||||
used: number;
|
||||
free: number;
|
||||
speed: number;
|
||||
}
|
||||
|
||||
interface StorageInfo {
|
||||
name: string;
|
||||
type: 'SSD' | 'HDD' | 'NVMe';
|
||||
total: number;
|
||||
used: number;
|
||||
health: number;
|
||||
}
|
||||
|
||||
interface MotherboardInfo {
|
||||
manufacturer: string;
|
||||
model: string;
|
||||
biosVersion: string;
|
||||
}
|
||||
|
||||
// 优化项
|
||||
interface OptimizeItem {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
category: 'system' | 'network' | 'gaming' | 'privacy';
|
||||
status: 'idle' | 'optimizing' | 'optimized' | 'failed';
|
||||
impact: 'low' | 'medium' | 'high';
|
||||
}
|
||||
|
||||
// 配置方案
|
||||
interface Scheme {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
game: string;
|
||||
filters: FilterConfig[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
interface FilterConfig {
|
||||
id: string;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
settings: Record<string, any>;
|
||||
}
|
||||
}
|
||||
|
||||
// CSS Modules
|
||||
declare module '*.module.css' {
|
||||
const classes: { [key: string]: string };
|
||||
export default classes;
|
||||
}
|
||||
|
||||
declare module '*.module.scss' {
|
||||
const classes: { [key: string]: string };
|
||||
export default classes;
|
||||
}
|
||||
56
src/types/index.ts
Normal file
56
src/types/index.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
export interface User {
|
||||
id: string;
|
||||
username: string;
|
||||
email: string;
|
||||
avatar: string | null;
|
||||
isVip: boolean;
|
||||
vipExpireAt: string | null;
|
||||
vipLevel: number;
|
||||
}
|
||||
|
||||
export interface Scheme {
|
||||
id: string;
|
||||
title: string | null;
|
||||
weaponName: string | null;
|
||||
category: string | null;
|
||||
userId: string;
|
||||
description: string | null;
|
||||
viewsCount: number;
|
||||
downloadsCount: number;
|
||||
likesCount: number;
|
||||
favoritesCount: number;
|
||||
price: number;
|
||||
status: string;
|
||||
isOfficial: boolean;
|
||||
createdAt: string;
|
||||
user?: { username: string; avatar: string | null };
|
||||
}
|
||||
|
||||
export interface Category {
|
||||
code: string;
|
||||
name: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
export interface LoginRequest {
|
||||
username: string;
|
||||
password: string;
|
||||
installId?: string;
|
||||
deviceHash?: string;
|
||||
platform?: string;
|
||||
osVersion?: string;
|
||||
appVersion?: string;
|
||||
}
|
||||
|
||||
export interface LoginResponse {
|
||||
success: boolean;
|
||||
token?: string;
|
||||
user?: User;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface ApiResponse<T = any> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
message?: string;
|
||||
}
|
||||
40
tailwind.config.ts
Normal file
40
tailwind.config.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import type { Config } from 'tailwindcss';
|
||||
|
||||
const config: Config = {
|
||||
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
// New palette: Industrial/Tech style
|
||||
tactical: {
|
||||
bg: '#1a1a1a',
|
||||
surface: '#222222',
|
||||
border: '#333333',
|
||||
borderLight: '#555555',
|
||||
text: '#e0e0e0',
|
||||
textMuted: '#888888',
|
||||
textDim: '#555555',
|
||||
accent: '#ff4500',
|
||||
accentDim: '#cc3700',
|
||||
black: '#111111',
|
||||
dark: '#1e1e1e',
|
||||
gray: '#333333',
|
||||
green: '#ff4500',
|
||||
gold: '#ff4500',
|
||||
goldLight: '#ff6a33',
|
||||
warn: '#cc3300',
|
||||
army: '#ff4500',
|
||||
armyDark: '#1a0a00',
|
||||
},
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['Inter', 'Helvetica', 'sans-serif'],
|
||||
mono: ['JetBrains Mono', 'monospace'],
|
||||
tech: ['Inter', 'sans-serif'],
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
|
||||
export default config;
|
||||
25
tsconfig.json
Normal file
25
tsconfig.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
11
tsconfig.node.json
Normal file
11
tsconfig.node.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true
|
||||
},
|
||||
"include": ["vite.config.ts", "electron/**/*"]
|
||||
}
|
||||
1
tsconfig.node.tsbuildinfo
Normal file
1
tsconfig.node.tsbuildinfo
Normal file
@@ -0,0 +1 @@
|
||||
{"fileNames":[],"fileInfos":[],"root":[],"options":{"allowSyntheticDefaultImports":true,"composite":true,"module":99,"outDir":"./","skipLibCheck":true,"strict":true},"errors":true,"version":"5.9.3"}
|
||||
18
vite.config.ts
Normal file
18
vite.config.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import path from 'path';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
base: './',
|
||||
root: '.',
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true,
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user