将合图处理从服务端迁移到浏览器端,使用 Web Worker 实现高性能打包算法,新增三栏布局界面和精灵动画预览功能 - 新增 atlasStore 状态管理,实现文件、配置、结果的统一管理 - 新增 atlas-packer 打包算法库(MaxRects/Shelf),支持浏览器端快速合图 - 新增 atlas-worker Web Worker,实现异步打包处理避免阻塞 UI - 新增三栏布局组件:FileListPanel、CanvasPreview、AtlasConfigPanel - 新增 AnimationPreviewDialog 支持精灵动画帧预览和帧率控制 - 优化所有工具页面的响应式布局和交互体验 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
141 lines
4.4 KiB
TypeScript
141 lines
4.4 KiB
TypeScript
"use client";
|
|
|
|
import { Label } from "@/components/ui/label";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Slider } from "@/components/ui/slider";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { cn } from "@/lib/utils";
|
|
import { useSafeTranslation } from "@/lib/i18n";
|
|
|
|
export type ConfigValue = string | number | boolean | undefined;
|
|
|
|
export interface ConfigOption {
|
|
id: string;
|
|
type: "slider" | "select" | "toggle" | "radio";
|
|
label: string;
|
|
description?: string;
|
|
value: ConfigValue;
|
|
options?: { label: string; value: ConfigValue }[];
|
|
min?: number;
|
|
max?: number;
|
|
step?: number;
|
|
suffix?: string;
|
|
icon?: React.ReactNode;
|
|
}
|
|
|
|
interface ConfigPanelProps {
|
|
title: string;
|
|
description?: string;
|
|
options: ConfigOption[];
|
|
onChange: (id: string, value: ConfigValue) => void;
|
|
onReset?: () => void;
|
|
className?: string;
|
|
}
|
|
|
|
export function ConfigPanel({
|
|
title,
|
|
description,
|
|
options,
|
|
onChange,
|
|
onReset,
|
|
className,
|
|
}: ConfigPanelProps) {
|
|
const { t } = useSafeTranslation();
|
|
|
|
return (
|
|
<Card className={className}>
|
|
<CardHeader>
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<CardTitle className="text-lg">{title}</CardTitle>
|
|
{description && (
|
|
<p className="mt-1 text-sm text-muted-foreground">{description}</p>
|
|
)}
|
|
</div>
|
|
{onReset && (
|
|
<Button variant="ghost" size="sm" onClick={onReset}>
|
|
{t("common.reset")}
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="space-y-6">
|
|
{options.map((option) => (
|
|
<div key={option.id} className="space-y-3">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
{option.icon && <div className="text-muted-foreground">{option.icon}</div>}
|
|
<Label htmlFor={option.id} className="font-medium">
|
|
{option.label}
|
|
</Label>
|
|
</div>
|
|
{option.type === "slider" && (
|
|
<Badge variant="secondary">
|
|
{option.value}
|
|
{option.suffix}
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
|
|
{option.description && (
|
|
<p className="text-xs text-muted-foreground">{option.description}</p>
|
|
)}
|
|
|
|
{option.type === "slider" && (
|
|
<Slider
|
|
id={option.id}
|
|
min={option.min ?? 0}
|
|
max={option.max ?? 100}
|
|
step={option.step ?? 1}
|
|
value={[typeof option.value === "number" ? option.value : 0]}
|
|
onValueChange={(values: number[]) => onChange(option.id, values[0])}
|
|
className="mt-2"
|
|
/>
|
|
)}
|
|
|
|
{option.type === "select" && option.options && (
|
|
<div className="flex flex-wrap gap-2">
|
|
{option.options.map((opt) => (
|
|
<Button
|
|
key={String(opt.value)}
|
|
variant={option.value === opt.value ? "default" : "outline"}
|
|
size="sm"
|
|
onClick={() => onChange(option.id, opt.value)}
|
|
>
|
|
{opt.label}
|
|
</Button>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{option.type === "radio" && option.options && (
|
|
<div className="space-y-2">
|
|
{option.options.map((opt) => (
|
|
<label
|
|
key={String(opt.value)}
|
|
className={cn(
|
|
"flex cursor-pointer items-center gap-3 rounded-lg border p-3 transition-colors hover:bg-accent",
|
|
option.value === opt.value && "border-primary bg-primary/10"
|
|
)}
|
|
>
|
|
<input
|
|
type="radio"
|
|
name={option.id}
|
|
value={String(opt.value)}
|
|
checked={option.value === opt.value}
|
|
onChange={() => onChange(option.id, opt.value)}
|
|
className="h-4 w-4"
|
|
/>
|
|
<span className="text-sm">{opt.label}</span>
|
|
</label>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|