新增CSV导入导出功能,支持导入CSV数据并生成合成图像,同时增加导出CSV hex数据选项,优化文件处理逻辑,提升用户交互体验。
This commit is contained in:
166
src/app/page.tsx
166
src/app/page.tsx
@@ -18,7 +18,7 @@ import {
|
||||
// 导入新的类型和组件
|
||||
import { GridDownloadOptions } from '../types/downloadTypes';
|
||||
import DownloadSettingsModal, { gridLineColorOptions } from '../components/DownloadSettingsModal';
|
||||
import { downloadImage } from '../utils/imageDownloader';
|
||||
import { downloadImage, importCsvData } from '../utils/imageDownloader';
|
||||
|
||||
import {
|
||||
colorSystemOptions,
|
||||
@@ -138,7 +138,8 @@ export default function Home() {
|
||||
gridInterval: 10,
|
||||
showCoordinates: true,
|
||||
gridLineColor: gridLineColorOptions[0].value,
|
||||
includeStats: true // 默认包含统计信息
|
||||
includeStats: true, // 默认包含统计信息
|
||||
exportCsv: false // 默认不导出CSV
|
||||
});
|
||||
|
||||
// 新增:高亮相关状态
|
||||
@@ -147,6 +148,9 @@ export default function Home() {
|
||||
// 新增:完整色板切换状态
|
||||
const [showFullPalette, setShowFullPalette] = useState<boolean>(false);
|
||||
|
||||
// 添加标记来区分是否是CSV导入的图像
|
||||
const [isFromCsvImport, setIsFromCsvImport] = useState<boolean>(false);
|
||||
|
||||
// 新增:颜色替换相关状态
|
||||
const [colorReplaceState, setColorReplaceState] = useState<{
|
||||
isActive: boolean;
|
||||
@@ -434,12 +438,13 @@ export default function Home() {
|
||||
// 更严格的文件类型检查
|
||||
const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png'];
|
||||
const fileType = file.type.toLowerCase();
|
||||
const fileName = file.name.toLowerCase();
|
||||
|
||||
if (allowedTypes.includes(fileType) || file.type.startsWith('image/')) {
|
||||
if (allowedTypes.includes(fileType) || file.type.startsWith('image/') || fileName.endsWith('.csv')) {
|
||||
setExcludedColorKeys(new Set()); // ++ 重置排除列表 ++
|
||||
processFile(file);
|
||||
} else {
|
||||
alert(`不支持的文件类型: ${file.type || '未知'}。请拖放 JPG 或 PNG 格式的图片文件。`);
|
||||
alert(`不支持的文件类型: ${file.type || '未知'}。请拖放 JPG、PNG 格式的图片文件,或 CSV 数据文件。`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -453,32 +458,131 @@ export default function Home() {
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
const processFile = (file: File) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const result = e.target?.result as string;
|
||||
setOriginalImageSrc(result);
|
||||
setMappedPixelData(null);
|
||||
setGridDimensions(null);
|
||||
setColorCounts(null);
|
||||
setTotalBeadCount(0);
|
||||
setInitialGridColorKeys(new Set()); // ++ 重置初始键 ++
|
||||
// ++ 重置横轴格子数量为默认值 ++
|
||||
const defaultGranularity = 100;
|
||||
setGranularity(defaultGranularity);
|
||||
setGranularityInput(defaultGranularity.toString());
|
||||
setRemapTrigger(prev => prev + 1); // Trigger full remap for new image
|
||||
};
|
||||
reader.onerror = () => {
|
||||
console.error("文件读取失败");
|
||||
alert("无法读取文件。");
|
||||
setInitialGridColorKeys(new Set()); // ++ 重置初始键 ++
|
||||
// 根据mappedPixelData生成合成的originalImageSrc
|
||||
const generateSyntheticImageFromPixelData = (pixelData: MappedPixel[][], dimensions: { N: number; M: number }): string => {
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
if (!ctx) {
|
||||
console.error('无法创建canvas上下文');
|
||||
return '';
|
||||
}
|
||||
|
||||
// 设置画布尺寸,每个像素用8x8像素来表示以确保清晰度
|
||||
const pixelSize = 8;
|
||||
canvas.width = dimensions.N * pixelSize;
|
||||
canvas.height = dimensions.M * pixelSize;
|
||||
|
||||
// 绘制每个像素
|
||||
pixelData.forEach((row, rowIndex) => {
|
||||
row.forEach((cell, colIndex) => {
|
||||
if (cell) {
|
||||
// 使用颜色,外部单元格用白色
|
||||
const color = cell.isExternal ? '#FFFFFF' : cell.color;
|
||||
ctx.fillStyle = color;
|
||||
ctx.fillRect(
|
||||
colIndex * pixelSize,
|
||||
rowIndex * pixelSize,
|
||||
pixelSize,
|
||||
pixelSize
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 转换为dataURL
|
||||
return canvas.toDataURL('image/png');
|
||||
};
|
||||
|
||||
const processFile = (file: File) => {
|
||||
// 检查文件类型
|
||||
const fileExtension = file.name.split('.').pop()?.toLowerCase();
|
||||
|
||||
if (fileExtension === 'csv') {
|
||||
// 处理CSV文件
|
||||
console.log('正在导入CSV文件...');
|
||||
importCsvData(file)
|
||||
.then(({ mappedPixelData, gridDimensions }) => {
|
||||
console.log(`成功导入CSV文件: ${gridDimensions.N}x${gridDimensions.M}`);
|
||||
|
||||
// 设置导入的数据
|
||||
setMappedPixelData(mappedPixelData);
|
||||
setGridDimensions(gridDimensions);
|
||||
setOriginalImageSrc(null); // CSV导入时没有原始图片
|
||||
|
||||
// 计算颜色统计
|
||||
const colorCountsMap: { [key: string]: { count: number; color: string } } = {};
|
||||
let totalCount = 0;
|
||||
|
||||
mappedPixelData.forEach(row => {
|
||||
row.forEach(cell => {
|
||||
if (cell && !cell.isExternal) {
|
||||
const colorKey = cell.color.toUpperCase();
|
||||
if (colorCountsMap[colorKey]) {
|
||||
colorCountsMap[colorKey].count++;
|
||||
} else {
|
||||
colorCountsMap[colorKey] = {
|
||||
count: 1,
|
||||
color: cell.color
|
||||
};
|
||||
}
|
||||
totalCount++;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
setColorCounts(colorCountsMap);
|
||||
setTotalBeadCount(totalCount);
|
||||
setInitialGridColorKeys(new Set(Object.keys(colorCountsMap)));
|
||||
|
||||
// 根据mappedPixelData生成合成的originalImageSrc
|
||||
const syntheticImageSrc = generateSyntheticImageFromPixelData(mappedPixelData, gridDimensions);
|
||||
|
||||
setOriginalImageSrc(syntheticImageSrc);
|
||||
|
||||
// 重置状态
|
||||
setIsManualColoringMode(false);
|
||||
setSelectedColor(null);
|
||||
setIsEraseMode(false);
|
||||
|
||||
// 设置格子数量为导入的尺寸,避免重新映射时尺寸被修改
|
||||
setGranularity(gridDimensions.N);
|
||||
setGranularityInput(gridDimensions.N.toString());
|
||||
|
||||
alert(`成功导入CSV文件!图纸尺寸:${gridDimensions.N}x${gridDimensions.M},共使用${Object.keys(colorCountsMap).length}种颜色。`);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('CSV导入失败:', error);
|
||||
alert(`CSV导入失败:${error.message}`);
|
||||
});
|
||||
} else {
|
||||
// 处理图片文件
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const result = e.target?.result as string;
|
||||
setOriginalImageSrc(result);
|
||||
setMappedPixelData(null);
|
||||
setGridDimensions(null);
|
||||
setColorCounts(null);
|
||||
setTotalBeadCount(0);
|
||||
setInitialGridColorKeys(new Set()); // ++ 重置初始键 ++
|
||||
// ++ 重置横轴格子数量为默认值 ++
|
||||
const defaultGranularity = 100;
|
||||
setGranularity(defaultGranularity);
|
||||
setGranularityInput(defaultGranularity.toString());
|
||||
setRemapTrigger(prev => prev + 1); // Trigger full remap for new image
|
||||
};
|
||||
reader.onerror = () => {
|
||||
console.error("文件读取失败");
|
||||
alert("无法读取文件。");
|
||||
setInitialGridColorKeys(new Set()); // ++ 重置初始键 ++
|
||||
}
|
||||
reader.readAsDataURL(file);
|
||||
// ++ Reset manual coloring mode when a new file is processed ++
|
||||
setIsManualColoringMode(false);
|
||||
setSelectedColor(null);
|
||||
setIsEraseMode(false);
|
||||
}
|
||||
reader.readAsDataURL(file);
|
||||
// ++ Reset manual coloring mode when a new file is processed ++
|
||||
setIsManualColoringMode(false);
|
||||
setSelectedColor(null);
|
||||
setIsEraseMode(false);
|
||||
};
|
||||
|
||||
// 处理一键擦除模式切换
|
||||
@@ -1734,7 +1838,7 @@ export default function Home() {
|
||||
{/* Text color */}
|
||||
<p className="text-xs sm:text-sm text-gray-500 dark:text-gray-400">拖放图片到此处,或<span className="font-medium text-blue-600 dark:text-blue-400">点击选择文件</span></p>
|
||||
{/* Text color */}
|
||||
<p className="text-xs text-gray-400 dark:text-gray-500 mt-1">支持 JPG, PNG 格式</p>
|
||||
<p className="text-xs text-gray-400 dark:text-gray-500 mt-1">支持 JPG, PNG 图片格式,或 CSV 数据文件</p>
|
||||
</div>
|
||||
|
||||
{/* Apply dark mode styles to the Tip Box */}
|
||||
@@ -1751,7 +1855,7 @@ export default function Home() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<input type="file" accept="image/jpeg, image/png" onChange={handleFileChange} ref={fileInputRef} className="hidden" />
|
||||
<input type="file" accept="image/jpeg, image/png, .csv" onChange={handleFileChange} ref={fileInputRef} className="hidden" />
|
||||
|
||||
{/* Controls and Output Area */}
|
||||
{originalImageSrc && (
|
||||
|
||||
@@ -167,6 +167,27 @@ const DownloadSettingsModal: React.FC<DownloadSettingsModalProps> = ({
|
||||
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* 新增: 导出CSV hex数据选项 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-col">
|
||||
<label className="flex items-center text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
同时导出CSV数据
|
||||
</label>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
导出hex颜色值的CSV文件,可用于重新导入
|
||||
</span>
|
||||
</div>
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="sr-only peer"
|
||||
checked={tempOptions.exportCsv}
|
||||
onChange={(e) => handleOptionChange('exportCsv', e.target.checked)}
|
||||
/>
|
||||
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end mt-6 space-x-3">
|
||||
|
||||
@@ -5,4 +5,5 @@ export type GridDownloadOptions = {
|
||||
showCoordinates: boolean;
|
||||
gridLineColor: string;
|
||||
includeStats: boolean;
|
||||
exportCsv: boolean; // 新增:是否同时导出CSV hex数据
|
||||
};
|
||||
@@ -46,6 +46,160 @@ function sortColorKeys(a: string, b: string): number {
|
||||
return a.localeCompare(b);
|
||||
}
|
||||
|
||||
// 导出CSV hex数据的函数
|
||||
export function exportCsvData({
|
||||
mappedPixelData,
|
||||
gridDimensions,
|
||||
selectedColorSystem
|
||||
}: {
|
||||
mappedPixelData: MappedPixel[][] | null;
|
||||
gridDimensions: { N: number; M: number } | null;
|
||||
selectedColorSystem: ColorSystem;
|
||||
}): void {
|
||||
if (!mappedPixelData || !gridDimensions) {
|
||||
console.error("导出失败: 映射数据或尺寸无效。");
|
||||
alert("无法导出CSV,数据未生成或无效。");
|
||||
return;
|
||||
}
|
||||
|
||||
const { N, M } = gridDimensions;
|
||||
|
||||
// 生成CSV内容,每行代表图纸的一行
|
||||
const csvLines: string[] = [];
|
||||
|
||||
for (let row = 0; row < M; row++) {
|
||||
const rowData: string[] = [];
|
||||
for (let col = 0; col < N; col++) {
|
||||
const cellData = mappedPixelData[row][col];
|
||||
if (cellData && !cellData.isExternal) {
|
||||
// 内部单元格,记录hex颜色值
|
||||
rowData.push(cellData.color);
|
||||
} else {
|
||||
// 外部单元格或空白,使用特殊标记
|
||||
rowData.push('TRANSPARENT');
|
||||
}
|
||||
}
|
||||
csvLines.push(rowData.join(','));
|
||||
}
|
||||
|
||||
// 创建CSV内容
|
||||
const csvContent = csvLines.join('\n');
|
||||
|
||||
// 创建并下载CSV文件
|
||||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||
const link = document.createElement('a');
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
link.setAttribute('href', url);
|
||||
link.setAttribute('download', `bead-pattern-${N}x${M}-${selectedColorSystem}.csv`);
|
||||
link.style.visibility = 'hidden';
|
||||
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
// 释放URL对象
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
console.log("CSV数据导出完成");
|
||||
}
|
||||
|
||||
// 导入CSV hex数据的函数
|
||||
export function importCsvData(file: File): Promise<{
|
||||
mappedPixelData: MappedPixel[][];
|
||||
gridDimensions: { N: number; M: number };
|
||||
}> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = (e) => {
|
||||
try {
|
||||
const text = e.target?.result as string;
|
||||
if (!text) {
|
||||
reject(new Error('无法读取文件内容'));
|
||||
return;
|
||||
}
|
||||
|
||||
// 解析CSV内容
|
||||
const lines = text.trim().split('\n');
|
||||
const M = lines.length; // 行数
|
||||
|
||||
if (M === 0) {
|
||||
reject(new Error('CSV文件为空'));
|
||||
return;
|
||||
}
|
||||
|
||||
// 解析第一行获取列数
|
||||
const firstRowData = lines[0].split(',');
|
||||
const N = firstRowData.length; // 列数
|
||||
|
||||
if (N === 0) {
|
||||
reject(new Error('CSV文件格式无效'));
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建映射数据
|
||||
const mappedPixelData: MappedPixel[][] = [];
|
||||
|
||||
for (let row = 0; row < M; row++) {
|
||||
const rowData = lines[row].split(',');
|
||||
const mappedRow: MappedPixel[] = [];
|
||||
|
||||
// 确保每行都有正确的列数
|
||||
if (rowData.length !== N) {
|
||||
reject(new Error(`第${row + 1}行的列数不匹配,期望${N}列,实际${rowData.length}列`));
|
||||
return;
|
||||
}
|
||||
|
||||
for (let col = 0; col < N; col++) {
|
||||
const cellValue = rowData[col].trim();
|
||||
|
||||
if (cellValue === 'TRANSPARENT' || cellValue === '') {
|
||||
// 外部/透明单元格
|
||||
mappedRow.push({
|
||||
key: 'TRANSPARENT',
|
||||
color: '#FFFFFF',
|
||||
isExternal: true
|
||||
});
|
||||
} else {
|
||||
// 验证hex颜色格式
|
||||
const hexPattern = /^#[0-9A-Fa-f]{6}$/;
|
||||
if (!hexPattern.test(cellValue)) {
|
||||
reject(new Error(`第${row + 1}行第${col + 1}列的颜色值无效:${cellValue}`));
|
||||
return;
|
||||
}
|
||||
|
||||
// 内部单元格
|
||||
mappedRow.push({
|
||||
key: cellValue.toUpperCase(),
|
||||
color: cellValue.toUpperCase(),
|
||||
isExternal: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
mappedPixelData.push(mappedRow);
|
||||
}
|
||||
|
||||
// 返回解析结果
|
||||
resolve({
|
||||
mappedPixelData,
|
||||
gridDimensions: { N, M }
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
reject(new Error(`解析CSV文件失败:${error}`));
|
||||
}
|
||||
};
|
||||
|
||||
reader.onerror = () => {
|
||||
reject(new Error('读取文件失败'));
|
||||
};
|
||||
|
||||
reader.readAsText(file, 'utf-8');
|
||||
});
|
||||
}
|
||||
|
||||
// 下载图片的主函数
|
||||
export async function downloadImage({
|
||||
mappedPixelData,
|
||||
@@ -666,6 +820,15 @@ export async function downloadImage({
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
console.log("Grid image download initiated.");
|
||||
|
||||
// 如果启用了CSV导出,同时导出CSV文件
|
||||
if (options.exportCsv) {
|
||||
exportCsvData({
|
||||
mappedPixelData,
|
||||
gridDimensions,
|
||||
selectedColorSystem
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("下载图纸失败:", e);
|
||||
alert("无法生成图纸下载链接。");
|
||||
|
||||
Reference in New Issue
Block a user