feat: 实现 Mini Game AI 工具平台

基于 Next.js 15、React 19 和 TypeScript 构建面向小游戏开发者的 AI 赋能工具平台。

主要功能:
- 首页:包含 Hero、功能展示、优势介绍、定价和 CTA 区域
- 三大核心工具:视频转序列帧、图片压缩、音频压缩
- 响应式布局:包含顶部导航、页脚和侧边栏
- 文件上传:支持拖拽上传,使用 react-dropzone
- 进度追踪:实时显示上传和处理进度
- 可配置工具:每个工具都支持自定义参数配置
- 结果预览:支持下载处理后的文件
- 4K 优化:针对大屏幕优化的响应式设计
- API 路由:文件上传和处理的模拟实现

技术栈:
- Next.js 15 (App Router)
- React 19
- TypeScript (严格模式)
- Tailwind CSS(自定义 4K 断点)
- shadcn/ui 组件库
- Framer Motion 动画
- Zustand 状态管理

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-20 22:26:55 +08:00
parent 9529a684a1
commit a7449bf49b
40 changed files with 10963 additions and 0 deletions

View File

@@ -0,0 +1,261 @@
"use client";
import { useState, useCallback } from "react";
import { motion } from "framer-motion";
import { Music, Volume2 } from "lucide-react";
import { FileUploader } from "@/components/tools/FileUploader";
import { ProgressBar } from "@/components/tools/ProgressBar";
import { ResultPreview } from "@/components/tools/ResultPreview";
import { ConfigPanel, type ConfigOption } from "@/components/tools/ConfigPanel";
import { Button } from "@/components/ui/button";
import { useUploadStore } from "@/store/uploadStore";
import { generateId } from "@/lib/utils";
import type { UploadedFile, ProcessedFile, AudioCompressConfig } from "@/types";
const audioAccept = {
"audio/*": [".mp3", ".wav", ".ogg", ".aac", ".flac", ".m4a"],
};
const defaultConfig: AudioCompressConfig = {
bitrate: 128,
format: "mp3",
sampleRate: 44100,
channels: 2,
};
const configOptions: ConfigOption[] = [
{
id: "bitrate",
type: "select",
label: "Bitrate",
description: "Higher bitrate = better quality, larger file",
value: defaultConfig.bitrate,
options: [
{ label: "64 kbps", value: 64 },
{ label: "128 kbps", value: 128 },
{ label: "192 kbps", value: 192 },
{ label: "256 kbps", value: 256 },
{ label: "320 kbps", value: 320 },
],
},
{
id: "format",
type: "select",
label: "Output Format",
description: "Target audio format",
value: defaultConfig.format,
options: [
{ label: "MP3", value: "mp3" },
{ label: "AAC", value: "aac" },
{ label: "OGG", value: "ogg" },
{ label: "FLAC", value: "flac" },
],
},
{
id: "sampleRate",
type: "select",
label: "Sample Rate",
description: "Audio sample rate in Hz",
value: defaultConfig.sampleRate,
options: [
{ label: "44.1 kHz", value: 44100 },
{ label: "48 kHz", value: 48000 },
],
},
{
id: "channels",
type: "radio",
label: "Channels",
description: "Audio channels",
value: defaultConfig.channels,
options: [
{ label: "Stereo (2 channels)", value: 2 },
{ label: "Mono (1 channel)", value: 1 },
],
},
];
export default function AudioCompressPage() {
const { files, addFile, removeFile, clearFiles, processingStatus, setProcessingStatus } =
useUploadStore();
const [config, setConfig] = useState<AudioCompressConfig>(defaultConfig);
const [processedFiles, setProcessedFiles] = useState<ProcessedFile[]>([]);
const handleFilesDrop = useCallback(
(acceptedFiles: File[]) => {
const newFiles: UploadedFile[] = acceptedFiles.map((file) => ({
id: generateId(),
file,
name: file.name,
size: file.size,
type: file.type,
uploadedAt: new Date(),
}));
newFiles.forEach((file) => addFile(file));
},
[addFile]
);
const handleConfigChange = (id: string, value: any) => {
setConfig((prev) => ({ ...prev, [id]: value }));
};
const handleResetConfig = () => {
setConfig(defaultConfig);
};
const handleProcess = async () => {
if (files.length === 0) return;
setProcessingStatus({
status: "uploading",
progress: 0,
message: "Uploading audio...",
});
try {
// Simulate upload
for (let i = 0; i <= 100; i += 10) {
await new Promise((resolve) => setTimeout(resolve, 50));
setProcessingStatus({
status: "uploading",
progress: i,
message: `Uploading... ${i}%`,
});
}
setProcessingStatus({
status: "processing",
progress: 0,
message: "Compressing audio...",
});
// Simulate processing
for (let i = 0; i <= 100; i += 5) {
await new Promise((resolve) => setTimeout(resolve, 150));
setProcessingStatus({
status: "processing",
progress: i,
message: `Compressing... ${i}%`,
});
}
// Simulate completion
const results: ProcessedFile[] = files.map((file) => ({
id: generateId(),
originalFile: file,
processedUrl: "#",
metadata: {
format: config.format,
bitrate: config.bitrate,
sampleRate: config.sampleRate,
compressionRatio: Math.floor(Math.random() * 50) + 50, // Simulated 50-100%
},
createdAt: new Date(),
}));
setProcessedFiles(results);
clearFiles();
setProcessingStatus({
status: "completed",
progress: 100,
message: "Compression complete!",
});
} catch (error) {
setProcessingStatus({
status: "failed",
progress: 0,
message: "Compression failed",
error: error instanceof Error ? error.message : "Unknown error",
});
}
};
const handleDownload = (fileId: string) => {
console.log("Downloading file:", fileId);
};
const canProcess = files.length > 0 && processingStatus.status !== "processing";
return (
<div className="p-6">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
>
<div className="mb-8">
<div className="flex items-center gap-3">
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-primary/10">
<Music className="h-6 w-6 text-primary" />
</div>
<div>
<h1 className="text-3xl font-bold">Audio Compression</h1>
<p className="text-muted-foreground">
Compress and convert audio files with quality control
</p>
</div>
</div>
</div>
<div className="grid gap-6 lg:grid-cols-2">
<div className="space-y-6">
<FileUploader
files={files}
onFilesDrop={handleFilesDrop}
onRemoveFile={removeFile}
accept={audioAccept}
maxSize={100 * 1024 * 1024} // 100MB
maxFiles={10}
disabled={processingStatus.status === "processing"}
/>
<ConfigPanel
title="Audio Settings"
description="Configure compression parameters"
options={configOptions.map((opt) => ({
...opt,
value: config[opt.id as keyof AudioCompressConfig],
}))}
onChange={handleConfigChange}
onReset={handleResetConfig}
/>
{canProcess && (
<Button onClick={handleProcess} size="lg" className="w-full">
<Volume2 className="mr-2 h-4 w-4" />
Compress Audio
</Button>
)}
</div>
<div className="space-y-6">
{processingStatus.status !== "idle" && (
<ProgressBar progress={processingStatus} />
)}
{processedFiles.length > 0 && (
<ResultPreview results={processedFiles} onDownload={handleDownload} />
)}
<div className="rounded-lg border border-border/40 bg-card/50 p-6">
<h3 className="mb-3 font-semibold">Supported Formats</h3>
<div className="grid grid-cols-2 gap-3 text-sm text-muted-foreground">
<div>
<p className="font-medium text-foreground">Input</p>
<p>MP3, WAV, OGG, AAC, FLAC, M4A</p>
</div>
<div>
<p className="font-medium text-foreground">Output</p>
<p>MP3, AAC, OGG, FLAC</p>
</div>
</div>
</div>
</div>
</div>
</motion.div>
</div>
);
}