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:
Chen Gu
2026-05-09 00:31:09 +08:00
commit 5bd314deb2
80 changed files with 12217 additions and 0 deletions

13
src/App.tsx Normal file
View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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}`} />;
}

View 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
View 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
View 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
View 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;
}

View 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
View 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
View 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
View 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
View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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;
}

View 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;
}

View 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
View 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 });
},
}));

View 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' }),
}));

View 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
View 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
View 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
View 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
View 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
View 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;
}