色板选择系统

This commit is contained in:
zihanjian
2025-06-25 16:24:07 +08:00
parent d401494093
commit 3e88cfcd81
3 changed files with 343 additions and 448 deletions

View File

@@ -1,430 +0,0 @@
'use client';
import React, { useState, useRef, ChangeEvent, DragEvent, useMemo, useCallback } from 'react';
import Script from 'next/script';
import InstallPWA from '../components/InstallPWA';
import CanvasContainer from '../components/CanvasContainer';
// 导入像素化工具和类型
import {
PixelationMode,
calculatePixelGrid,
PaletteColor,
MappedPixel,
hexToRgb,
} from '../utils/pixelation';
import {
colorSystemOptions,
getColorKeyByHex,
getMardToHexMapping,
sortColorsByHue,
ColorSystem
} from '../utils/colorSystemUtils';
// 添加自定义动画样式
const floatAnimation = `
@keyframes float {
0% { transform: translateY(0px); }
50% { transform: translateY(-5px); }
100% { transform: translateY(0px); }
}
.animate-float {
animation: float 3s ease-in-out infinite;
}
`;
// 获取完整色板
const mardToHexMapping = getMardToHexMapping();
const fullBeadPalette: PaletteColor[] = Object.entries(mardToHexMapping)
.map(([, hex]) => {
const rgb = hexToRgb(hex);
if (!rgb) return null;
return { key: hex, hex, rgb };
})
.filter((item): item is PaletteColor => item !== null);
export default function Home() {
// 基础状态
const [originalImageSrc, setOriginalImageSrc] = useState<string | null>(null);
const [granularity, setGranularity] = useState<number>(50);
const [pixelationMode, setPixelationMode] = useState<PixelationMode>(PixelationMode.Dominant);
const [selectedColorSystem, setSelectedColorSystem] = useState<ColorSystem>('MARD');
// 色板相关
const [activeBeadPalette] = useState<PaletteColor[]>(fullBeadPalette);
// 像素数据
const [mappedPixelData, setMappedPixelData] = useState<MappedPixel[][] | null>(null);
const [gridDimensions, setGridDimensions] = useState<{ N: number; M: number } | null>(null);
const [colorCounts, setColorCounts] = useState<{ [key: string]: { count: number; color: string } } | null>(null);
const [totalBeadCount, setTotalBeadCount] = useState<number>(0);
// UI状态
const [isDonationModalOpen, setIsDonationModalOpen] = useState<boolean>(false);
const [isProcessing, setIsProcessing] = useState<boolean>(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const mainRef = useRef<HTMLDivElement>(null);
// 处理文件选择
const handleFileSelect = (e: ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
setOriginalImageSrc(e.target?.result as string);
};
reader.readAsDataURL(file);
}
};
// 处理拖拽
const handleDragOver = (e: DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
};
const handleDrop = (e: DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
const file = e.dataTransfer.files[0];
if (file && file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = (e) => {
setOriginalImageSrc(e.target?.result as string);
};
reader.readAsDataURL(file);
}
};
// 生成像素画
const generatePixelArt = useCallback(() => {
if (!originalImageSrc) return;
setIsProcessing(true);
const img = new Image();
img.onload = () => {
// 创建临时 canvas 获取图像数据
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (!ctx) {
setIsProcessing(false);
return;
}
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0);
// 计算网格尺寸
const N = Math.round(img.height / granularity);
const M = Math.round(img.width / granularity);
// 获取备用颜色(第一个颜色)
const fallbackColor = activeBeadPalette[0] || { key: '#000000', hex: '#000000', rgb: { r: 0, g: 0, b: 0 } };
// 调用新的 API
const mappedPixelData = calculatePixelGrid(
ctx,
img.width,
img.height,
N,
M,
activeBeadPalette,
pixelationMode,
fallbackColor
);
// 计算颜色统计
const colorCounts: { [key: string]: { count: number; color: string } } = {};
let totalBeadCount = 0;
mappedPixelData.forEach(row => {
row.forEach(pixel => {
if (pixel.key && pixel.key !== 'transparent' && !pixel.isExternal) {
if (!colorCounts[pixel.key]) {
colorCounts[pixel.key] = { count: 0, color: pixel.color };
}
colorCounts[pixel.key].count++;
totalBeadCount++;
}
});
});
setMappedPixelData(mappedPixelData);
setGridDimensions({ N, M });
setColorCounts(colorCounts);
setTotalBeadCount(totalBeadCount);
setIsProcessing(false);
};
img.src = originalImageSrc;
}, [originalImageSrc, granularity, activeBeadPalette, pixelationMode]);
// 处理像素网格更新
const handlePixelGridUpdate = useCallback((newGrid: MappedPixel[][]) => {
setMappedPixelData(newGrid);
// 重新计算颜色统计
const counts: { [key: string]: { count: number; color: string } } = {};
let total = 0;
newGrid.forEach(row => {
row.forEach(pixel => {
if (pixel.key && pixel.key !== 'transparent' && !pixel.isExternal) {
if (!counts[pixel.key]) {
counts[pixel.key] = { count: 0, color: pixel.color };
}
counts[pixel.key].count++;
total++;
}
});
});
setColorCounts(counts);
setTotalBeadCount(total);
}, []);
// 计算当前色板
const currentColorPalette = useMemo(() => {
if (!mappedPixelData) return [];
const uniqueColors = new Set<string>();
mappedPixelData.forEach(row => {
row.forEach(pixel => {
if (pixel.key && pixel.key !== 'transparent' && !pixel.isExternal) {
uniqueColors.add(pixel.key);
}
});
});
const colorArray = Array.from(uniqueColors).map(key => {
const colorInfo = colorCounts?.[key];
return {
key,
hex: colorInfo?.color || key,
color: colorInfo?.color || key,
name: getColorKeyByHex(key, selectedColorSystem) || key
};
});
return sortColorsByHue(colorArray);
}, [mappedPixelData, colorCounts, selectedColorSystem]);
return (
<>
<style dangerouslySetInnerHTML={{ __html: floatAnimation }} />
<InstallPWA />
<Script
async
src="//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js"
strategy="lazyOnload"
/>
<main ref={mainRef} className="relative min-h-screen flex flex-col bg-gradient-to-br from-gray-900 via-black to-gray-900">
{/* 背景装饰 */}
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<div className="absolute -top-40 -right-40 w-80 h-80 bg-purple-500 rounded-full mix-blend-multiply filter blur-xl opacity-20 animate-float"></div>
<div className="absolute -bottom-40 -left-40 w-80 h-80 bg-blue-500 rounded-full mix-blend-multiply filter blur-xl opacity-20 animate-float" style={{ animationDelay: '2s' }}></div>
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-96 h-96 bg-green-500 rounded-full mix-blend-multiply filter blur-xl opacity-10 animate-float" style={{ animationDelay: '4s' }}></div>
</div>
{/* 头部 */}
<header className="relative z-10 bg-black/30 backdrop-blur-xl border-b border-white/10">
<div className="container mx-auto px-4 py-6">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<h1 className="text-3xl font-bold bg-gradient-to-r from-purple-400 via-pink-400 to-blue-400 bg-clip-text text-transparent">
稿
</h1>
<span className="text-sm text-gray-400"></span>
</div>
<button
onClick={() => setIsDonationModalOpen(true)}
className="px-4 py-2 bg-gradient-to-r from-yellow-400 to-orange-400 text-black rounded-lg hover:from-yellow-500 hover:to-orange-500 transition-all duration-200 transform hover:scale-105"
>
</button>
</div>
</div>
</header>
{/* 主内容区 */}
<div className="flex-1 container mx-auto px-4 py-8">
{!mappedPixelData ? (
// 上传区域
<div className="flex flex-col items-center justify-center min-h-[60vh]">
<div
onDragOver={handleDragOver}
onDrop={handleDrop}
onClick={() => fileInputRef.current?.click()}
className="relative w-full max-w-2xl p-12 bg-white/5 backdrop-blur-lg rounded-2xl border-2 border-dashed border-white/20 hover:border-white/40 transition-all duration-300 cursor-pointer group"
>
<input
ref={fileInputRef}
type="file"
accept="image/*"
onChange={handleFileSelect}
className="hidden"
/>
<div className="text-center">
<svg className="w-24 h-24 mx-auto mb-4 text-white/40 group-hover:text-white/60 transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
</svg>
<p className="text-xl text-white/80 mb-2"></p>
<p className="text-sm text-white/50"> JPGPNGGIF </p>
</div>
</div>
{originalImageSrc && (
<div className="mt-8 space-y-6 w-full max-w-2xl">
{/* 参数设置 */}
<div className="bg-white/5 backdrop-blur-lg rounded-xl p-6 space-y-4">
<div>
<label className="block text-sm font-medium text-white/80 mb-2">
({granularity})
</label>
<input
type="range"
min="10"
max="200"
value={granularity}
onChange={(e) => {
setGranularity(Number(e.target.value));
}}
className="w-full"
/>
</div>
<div>
<label className="block text-sm font-medium text-white/80 mb-2">
</label>
<select
value={pixelationMode}
onChange={(e) => setPixelationMode(e.target.value as unknown as PixelationMode)}
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white"
>
<option value={PixelationMode.Average}></option>
<option value={PixelationMode.Dominant}></option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-white/80 mb-2">
</label>
<select
value={selectedColorSystem}
onChange={(e) => setSelectedColorSystem(e.target.value as ColorSystem)}
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white"
>
{colorSystemOptions.map(option => (
<option key={option.key} value={option.key}>
{option.name}
</option>
))}
</select>
</div>
</div>
<button
onClick={generatePixelArt}
disabled={isProcessing}
className="w-full py-4 bg-gradient-to-r from-purple-500 to-blue-500 text-white rounded-xl hover:from-purple-600 hover:to-blue-600 transition-all duration-200 transform hover:scale-[1.02] disabled:opacity-50 disabled:cursor-not-allowed"
>
{isProcessing ? '生成中...' : '生成像素画'}
</button>
</div>
)}
</div>
) : (
// 画布容器
<div className="h-[calc(100vh-200px)] relative">
<CanvasContainer
pixelGrid={mappedPixelData}
cellSize={10}
onPixelGridUpdate={handlePixelGridUpdate}
colorPalette={currentColorPalette}
selectedColorSystem={selectedColorSystem}
/>
{/* 浮动操作按钮 */}
<div className="absolute top-4 left-4 flex gap-2">
<button
onClick={() => {
setMappedPixelData(null);
setColorCounts(null);
setTotalBeadCount(0);
}}
className="px-4 py-2 bg-white/90 dark:bg-gray-800/90 text-gray-800 dark:text-white rounded-lg shadow-lg hover:bg-white dark:hover:bg-gray-800 transition-all duration-200"
title="重新开始"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</button>
<select
value={selectedColorSystem}
onChange={(e) => setSelectedColorSystem(e.target.value as ColorSystem)}
className="px-3 py-2 bg-white/90 dark:bg-gray-800/90 text-gray-800 dark:text-white rounded-lg shadow-lg"
>
{colorSystemOptions.map(option => (
<option key={option.key} value={option.key}>
{option.name}
</option>
))}
</select>
</div>
{/* 统计信息 */}
<div className="absolute bottom-4 left-4 bg-white/90 dark:bg-gray-800/90 rounded-lg shadow-lg p-4">
<div className="text-sm space-y-1">
<div className="font-medium"></div>
<div className="text-gray-600 dark:text-gray-300">
: {gridDimensions?.M} × {gridDimensions?.N}
</div>
<div className="text-gray-600 dark:text-gray-300">
: {totalBeadCount}
</div>
<div className="text-gray-600 dark:text-gray-300">
: {currentColorPalette.length}
</div>
</div>
</div>
</div>
)}
</div>
{/* 打赏模态框 */}
{isDonationModalOpen && (
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4">
<div className="bg-white dark:bg-gray-800 rounded-2xl p-6 max-w-md w-full">
<h3 className="text-xl font-bold mb-4"></h3>
<p className="text-gray-600 dark:text-gray-300 mb-4">
</p>
<div className="flex justify-center mb-4">
{/* 这里可以添加二维码图片 */}
<div className="w-48 h-48 bg-gray-200 dark:bg-gray-700 rounded-lg flex items-center justify-center">
<span className="text-gray-500"></span>
</div>
</div>
<button
onClick={() => setIsDonationModalOpen(false)}
className="w-full py-2 bg-gray-200 dark:bg-gray-700 rounded-lg hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors"
>
</button>
</div>
</div>
)}
</main>
</>
);
}

View File

@@ -29,7 +29,8 @@ import CelebrationAnimation from '../../components/CelebrationAnimation';
import CompletionCard from '../../components/CompletionCard';
import PreviewToolbar from '../../components/PreviewToolbar';
import EditToolbar from '../../components/EditToolbar';
import { getColorKeyByHex, ColorSystem, getMardToHexMapping } from '../../utils/colorSystemUtils';
import ColorSystemPanel from '../../components/ColorSystemPanel';
import { getColorKeyByHex, ColorSystem, getMardToHexMapping, getAllHexValues } from '../../utils/colorSystemUtils';
// 定义编辑模式类型
type EditMode = 'focus' | 'preview' | 'edit';
@@ -74,15 +75,6 @@ interface FocusModeState {
editMode: EditMode;
}
// 获取完整色板
const mardToHexMapping = getMardToHexMapping();
const fullBeadPalette: PaletteColor[] = Object.entries(mardToHexMapping)
.map(([, hex]) => {
const rgb = hexToRgb(hex);
if (!rgb) return null;
return { key: hex, hex, rgb };
})
.filter((item): item is PaletteColor => item !== null);
export default function FocusMode() {
const router = useRouter();
@@ -133,6 +125,12 @@ export default function FocusMode() {
const [selectedColor, setSelectedColor] = useState<string>('');
const [isProcessing, setIsProcessing] = useState<boolean>(false);
// 色号系统和点击格子颜色信息的状态
const [selectedColorSystem, setSelectedColorSystem] = useState<ColorSystem>('MARD');
const [clickedCellColor, setClickedCellColor] = useState<{ hex: string; key: string } | null>(null);
const [showColorSystemPanel, setShowColorSystemPanel] = useState(false);
const [customPalette, setCustomPalette] = useState<Set<string>>(new Set(getAllHexValues()));
// 编辑模式状态
const [editTool, setEditTool] = useState<'select' | 'wand'>('select');
const [selectedCells, setSelectedCells] = useState<Set<string>>(new Set());
@@ -146,6 +144,26 @@ export default function FocusMode() {
const canUndo = historyIndex > 0;
const canRedo = historyIndex < history.length - 1;
// 在客户端加载保存的设置
useEffect(() => {
// 加载色号系统
const savedColorSystem = localStorage.getItem('selectedColorSystem');
if (savedColorSystem) {
setSelectedColorSystem(savedColorSystem as ColorSystem);
}
// 加载自定义色板
const savedPalette = localStorage.getItem('customPalette');
if (savedPalette) {
try {
const palette = new Set<string>(JSON.parse(savedPalette));
setCustomPalette(palette);
} catch (e) {
console.error('Failed to load custom palette:', e);
}
}
}, []);
// 计时器管理
useEffect(() => {
let interval: NodeJS.Timeout | null = null;
@@ -195,8 +213,17 @@ export default function FocusMode() {
const aspectRatio = img.height / img.width;
const M = Math.round(N * aspectRatio); // M是纵向高度按比例计算
// 根据自定义色板构建可用颜色
const activeBeadPalette: PaletteColor[] = Array.from(customPalette)
.map(hex => {
const rgb = hexToRgb(hex);
if (!rgb) return null;
return { key: hex, hex, rgb };
})
.filter((color): color is PaletteColor => color !== null);
// 获取备用颜色
const fallbackColor = fullBeadPalette[0] || { key: '#000000', hex: '#000000', rgb: { r: 0, g: 0, b: 0 } };
const fallbackColor = activeBeadPalette[0] || { key: '#000000', hex: '#000000', rgb: { r: 0, g: 0, b: 0 } };
// 计算像素网格
const pixelData = calculatePixelGrid(
@@ -205,7 +232,7 @@ export default function FocusMode() {
img.height,
N,
M,
fullBeadPalette,
activeBeadPalette,
PixelationMode.Dominant,
fallbackColor
);
@@ -402,6 +429,15 @@ export default function FocusMode() {
if (!mappedPixelData) return;
const cellColor = mappedPixelData[row][col].color;
const pixel = mappedPixelData[row][col];
// 更新点击的格子颜色信息(所有模式下都执行)
if (!pixel.isExternal && pixel.key !== 'transparent') {
const colorKey = getColorKeyByHex(cellColor, selectedColorSystem);
setClickedCellColor({ hex: cellColor, key: colorKey });
} else {
setClickedCellColor(null);
}
// 专心模式:标记区域
if (focusState.editMode === 'focus' && cellColor === focusState.currentColor) {
@@ -529,7 +565,7 @@ export default function FocusMode() {
setSelectedCells(newSelection);
}
}
}, [mappedPixelData, focusState, editTool, isSelecting]);
}, [mappedPixelData, focusState, editTool, isSelecting, selectedColorSystem]);
// 处理颜色切换
const handleColorChange = useCallback((color: string) => {
@@ -672,8 +708,17 @@ export default function FocusMode() {
const aspectRatio = img.height / img.width;
const M = Math.round(N * aspectRatio); // M是纵向高度
// 根据自定义色板构建可用颜色
const activeBeadPalette: PaletteColor[] = Array.from(customPalette)
.map(hex => {
const rgb = hexToRgb(hex);
if (!rgb) return null;
return { key: hex, hex, rgb };
})
.filter((color): color is PaletteColor => color !== null);
// 获取备用颜色
const fallbackColor = fullBeadPalette[0] || { key: '#000000', hex: '#000000', rgb: { r: 0, g: 0, b: 0 } };
const fallbackColor = activeBeadPalette[0] || { key: '#000000', hex: '#000000', rgb: { r: 0, g: 0, b: 0 } };
// 计算像素网格
const pixelData = calculatePixelGrid(
@@ -682,7 +727,7 @@ export default function FocusMode() {
img.height,
N,
M,
fullBeadPalette,
activeBeadPalette,
pixelationMode,
fallbackColor
);
@@ -888,7 +933,7 @@ export default function FocusMode() {
// 更新颜色列表
const colors = Object.entries(counts).map(([, colorData]) => {
const displayKey = getColorKeyByHex(colorData.color, 'MARD');
const displayKey = getColorKeyByHex(colorData.color, selectedColorSystem);
return {
color: colorData.color,
name: displayKey,
@@ -906,7 +951,7 @@ export default function FocusMode() {
setIsProcessing(false);
};
img.src = imageSrc;
}, [gridWidth, pixelationMode, colorMergeThreshold, removeBackground, selectedColor]);
}, [gridWidth, pixelationMode, colorMergeThreshold, removeBackground, selectedColor, selectedColorSystem, customPalette]);
// 编辑模式:保存历史记录
const saveToHistory = useCallback(() => {
@@ -1036,6 +1081,16 @@ export default function FocusMode() {
regeneratePixelArt();
}
}, [removeBackground]); // 只监听 removeBackground 的变化
// 当色号系统改变时,更新所有颜色的名称
useEffect(() => {
if (availableColors.length > 0 && !isProcessing) {
setAvailableColors(prev => prev.map(color => ({
...color,
name: getColorKeyByHex(color.color, selectedColorSystem)
})));
}
}, [selectedColorSystem]);
if (!mappedPixelData || !gridDimensions) {
return (
@@ -1070,6 +1125,14 @@ export default function FocusMode() {
{/* TODO: 添加 Logo */}
</div>
{/* 色板系统选择 */}
<button
onClick={() => setShowColorSystemPanel(true)}
className="text-sm text-blue-600 hover:text-blue-700 font-medium mr-2"
>
{selectedColorSystem}
</button>
<button
onClick={() => setFocusState(prev => ({ ...prev, showSettingsPanel: true }))}
className="p-2 -mr-2 text-gray-600 active:bg-gray-100 rounded-lg"
@@ -1081,12 +1144,24 @@ export default function FocusMode() {
</header>
{/* 状态信息栏 - 显示颜色数量和像素尺寸 */}
<div className="bg-gray-50 border-b border-gray-200 px-4 py-1.5">
<div className="bg-gray-50 border-b border-gray-200 px-4 py-1.5 flex items-center justify-between">
<div className="text-sm text-gray-700 font-mono tracking-wide">
<span className="font-medium">{availableColors.length}</span>
<span className="mx-3 text-gray-400">|</span>
<span className="font-medium">{mappedPixelData ? `${mappedPixelData[0]?.length || 0}×${mappedPixelData.length}` : '0×0'}</span>
</div>
{/* 点击格子的颜色信息 */}
{clickedCellColor && (
<div className="flex items-center gap-2 px-3 py-1 bg-gray-100 rounded-lg">
<div
className="w-5 h-5 rounded border border-gray-300"
style={{ backgroundColor: clickedCellColor.hex }}
/>
<span className="text-sm font-medium text-gray-700">
{clickedCellColor.key}
</span>
</div>
)}
</div>
{/* 当前颜色状态栏 - 仅在专心模式显示 */}
@@ -1243,6 +1318,17 @@ export default function FocusMode() {
/>
)}
{/* 色板系统面板 */}
{showColorSystemPanel && (
<ColorSystemPanel
selectedColorSystem={selectedColorSystem}
customPalette={customPalette}
onColorSystemChange={setSelectedColorSystem}
onCustomPaletteChange={setCustomPalette}
onClose={() => setShowColorSystemPanel(false)}
/>
)}
{/* 庆祝动画 */}
<CelebrationAnimation
isVisible={focusState.showCelebration}

View File

@@ -0,0 +1,239 @@
import React, { useState, useEffect } from 'react';
import { ColorSystem, colorSystemOptions, getAllHexValues, getColorKeyByHex } from '../utils/colorSystemUtils';
interface ColorSystemPanelProps {
selectedColorSystem: ColorSystem;
customPalette: Set<string>; // hex values
onColorSystemChange: (system: ColorSystem) => void;
onCustomPaletteChange: (palette: Set<string>) => void;
onClose: () => void;
}
const ColorSystemPanel: React.FC<ColorSystemPanelProps> = ({
selectedColorSystem,
customPalette,
onColorSystemChange,
onCustomPaletteChange,
onClose
}) => {
const [activeTab, setActiveTab] = useState<'system' | 'custom'>('system');
const [searchTerm, setSearchTerm] = useState('');
const [tempCustomPalette, setTempCustomPalette] = useState<Set<string>>(new Set(customPalette));
// 获取所有可用颜色
const allColors = getAllHexValues();
// 过滤颜色
const filteredColors = allColors.filter(hex => {
const colorKey = getColorKeyByHex(hex, selectedColorSystem);
return colorKey.toLowerCase().includes(searchTerm.toLowerCase()) ||
hex.toLowerCase().includes(searchTerm.toLowerCase());
});
// 保存自定义色板
const handleSaveCustomPalette = () => {
onCustomPaletteChange(tempCustomPalette);
localStorage.setItem('customPalette', JSON.stringify(Array.from(tempCustomPalette)));
};
// 加载保存的自定义色板
useEffect(() => {
const saved = localStorage.getItem('customPalette');
if (saved) {
try {
const palette = new Set<string>(JSON.parse(saved));
setTempCustomPalette(palette);
onCustomPaletteChange(palette);
} catch (e) {
console.error('Failed to load custom palette:', e);
}
}
}, [onCustomPaletteChange]);
// 切换颜色选择
const toggleColor = (hex: string) => {
const newPalette = new Set(tempCustomPalette);
if (newPalette.has(hex)) {
newPalette.delete(hex);
} else {
newPalette.add(hex);
}
setTempCustomPalette(newPalette);
};
// 全选/全不选
const handleSelectAll = () => {
if (tempCustomPalette.size === allColors.length) {
setTempCustomPalette(new Set());
} else {
setTempCustomPalette(new Set(allColors));
}
};
return (
<div className="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4">
<div className="w-full max-w-4xl bg-white rounded-2xl max-h-[90vh] flex flex-col shadow-xl">
{/* 标题栏 */}
<div className="flex items-center justify-between p-4 border-b">
<h2 className="text-lg font-semibold"></h2>
<button
onClick={onClose}
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{/* 标签页 */}
<div className="flex border-b">
<button
onClick={() => setActiveTab('system')}
className={`flex-1 py-3 px-4 font-medium transition-colors ${
activeTab === 'system'
? 'text-blue-600 border-b-2 border-blue-600'
: 'text-gray-600 hover:text-gray-800'
}`}
>
</button>
<button
onClick={() => setActiveTab('custom')}
className={`flex-1 py-3 px-4 font-medium transition-colors ${
activeTab === 'custom'
? 'text-blue-600 border-b-2 border-blue-600'
: 'text-gray-600 hover:text-gray-800'
}`}
>
</button>
</div>
<div className="flex-1 overflow-hidden flex flex-col">
{activeTab === 'system' ? (
/* 色号系统选择 */
<div className="p-4">
<h3 className="text-sm font-medium text-gray-700 mb-3"></h3>
<div className="space-y-2">
{colorSystemOptions.map(option => (
<label
key={option.key}
className="flex items-center p-3 border rounded-lg cursor-pointer hover:bg-gray-50 transition-colors"
>
<input
type="radio"
name="colorSystem"
value={option.key}
checked={selectedColorSystem === option.key}
onChange={() => {
onColorSystemChange(option.key as ColorSystem);
localStorage.setItem('selectedColorSystem', option.key);
}}
className="mr-3"
/>
<span className="font-medium">{option.name}</span>
</label>
))}
</div>
<div className="mt-4 p-3 bg-blue-50 rounded-lg">
<p className="text-sm text-blue-800">
<span className="font-medium">{selectedColorSystem}</span>
</p>
<p className="text-xs text-blue-600 mt-1">
使
</p>
</div>
</div>
) : (
/* 自定义色板 */
<div className="flex-1 flex flex-col">
{/* 搜索和操作栏 */}
<div className="p-4 space-y-3">
<div className="relative">
<input
type="text"
placeholder="搜索颜色编号..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-10 pr-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<svg
className="absolute left-3 top-2.5 h-5 w-5 text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
<div className="flex items-center justify-between">
<div className="text-sm text-gray-600">
{tempCustomPalette.size} / {allColors.length}
</div>
<button
onClick={handleSelectAll}
className="text-sm text-blue-600 hover:text-blue-700 font-medium"
>
{tempCustomPalette.size === allColors.length ? '取消全选' : '全选'}
</button>
</div>
</div>
{/* 颜色网格 */}
<div className="flex-1 overflow-y-auto px-4 pb-4">
<div className="grid grid-cols-4 sm:grid-cols-6 md:grid-cols-8 lg:grid-cols-10 gap-2">
{filteredColors.map(hex => {
const colorKey = getColorKeyByHex(hex, selectedColorSystem);
const isSelected = tempCustomPalette.has(hex);
return (
<button
key={hex}
onClick={() => toggleColor(hex)}
className={`relative p-2 rounded-lg border-2 transition-all ${
isSelected
? 'border-blue-500 bg-blue-50'
: 'border-gray-200 hover:border-gray-300'
}`}
title={`${colorKey} - ${hex}`}
>
<div
className="w-full aspect-square rounded mb-1"
style={{ backgroundColor: hex }}
/>
<div className="text-xs text-center font-mono">
{colorKey}
</div>
{isSelected && (
<div className="absolute top-1 right-1 w-4 h-4 bg-blue-500 rounded-full flex items-center justify-center">
<svg className="w-3 h-3 text-white" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
</div>
)}
</button>
);
})}
</div>
</div>
{/* 保存按钮 */}
<div className="p-4 border-t bg-gray-50">
<button
onClick={handleSaveCustomPalette}
className="w-full py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-medium"
>
</button>
</div>
</div>
)}
</div>
</div>
</div>
);
};
export default ColorSystemPanel;