192 lines
5.7 KiB
TypeScript
192 lines
5.7 KiB
TypeScript
// ============================================================================
|
||
// File Parser Utilities
|
||
// ============================================================================
|
||
|
||
import * as XLSX from "xlsx";
|
||
import Papa from "papaparse";
|
||
|
||
export interface ParsedChannel {
|
||
telegram_id?: number;
|
||
title: string;
|
||
username?: string;
|
||
description?: string;
|
||
subscribers_count?: number;
|
||
row: number; // Номер строки для отчетов об ошибках
|
||
}
|
||
|
||
export interface ParseResult {
|
||
channels: ParsedChannel[];
|
||
errors: string[];
|
||
}
|
||
|
||
/**
|
||
* Парсит Excel файл (xlsx)
|
||
*/
|
||
export const parseExcelFile = async (file: File): Promise<ParseResult> => {
|
||
const errors: string[] = [];
|
||
const channels: ParsedChannel[] = [];
|
||
|
||
try {
|
||
const arrayBuffer = await file.arrayBuffer();
|
||
const workbook = XLSX.read(arrayBuffer, { type: "array" });
|
||
const sheetName = workbook.SheetNames[0];
|
||
const worksheet = workbook.Sheets[sheetName];
|
||
const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
|
||
|
||
// Пропускаем заголовок (первая строка)
|
||
const rows = jsonData.slice(1) as any[][];
|
||
|
||
rows.forEach((row, index) => {
|
||
const rowNumber = index + 2; // +2 потому что пропустили заголовок и индекс с 0
|
||
|
||
// Пропускаем пустые строки
|
||
if (!row || row.length === 0 || !row[0]) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const channel: ParsedChannel = {
|
||
row: rowNumber,
|
||
title: String(row[0] || "").trim(),
|
||
username: row[1] ? String(row[1]).trim().replace("@", "") : undefined,
|
||
telegram_id: row[2] ? parseInt(String(row[2])) : undefined,
|
||
subscribers_count: row[3] ? parseInt(String(row[3])) : undefined,
|
||
description: row[4] ? String(row[4]).trim() : undefined,
|
||
};
|
||
|
||
if (!channel.title) {
|
||
errors.push(`Строка ${rowNumber}: отсутствует название канала`);
|
||
return;
|
||
}
|
||
|
||
channels.push(channel);
|
||
} catch (err) {
|
||
errors.push(
|
||
`Строка ${rowNumber}: ошибка парсинга - ${
|
||
err instanceof Error ? err.message : "неизвестная ошибка"
|
||
}`
|
||
);
|
||
}
|
||
});
|
||
} catch (err) {
|
||
errors.push(
|
||
`Ошибка чтения файла: ${
|
||
err instanceof Error ? err.message : "неизвестная ошибка"
|
||
}`
|
||
);
|
||
}
|
||
|
||
return { channels, errors };
|
||
};
|
||
|
||
/**
|
||
* Парсит CSV файл
|
||
*/
|
||
export const parseCsvFile = async (file: File): Promise<ParseResult> => {
|
||
return new Promise((resolve) => {
|
||
const errors: string[] = [];
|
||
const channels: ParsedChannel[] = [];
|
||
|
||
Papa.parse(file, {
|
||
header: false,
|
||
skipEmptyLines: true,
|
||
complete: (results) => {
|
||
const rows = results.data as string[][];
|
||
|
||
// Пропускаем заголовок
|
||
rows.slice(1).forEach((row, index) => {
|
||
const rowNumber = index + 2;
|
||
|
||
if (!row || row.length === 0 || !row[0]) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const channel: ParsedChannel = {
|
||
row: rowNumber,
|
||
title: (row[0] || "").trim(),
|
||
username: row[1] ? row[1].trim().replace("@", "") : undefined,
|
||
telegram_id: row[2] ? parseInt(row[2]) : undefined,
|
||
subscribers_count: row[3] ? parseInt(row[3]) : undefined,
|
||
description: row[4] ? row[4].trim() : undefined,
|
||
};
|
||
|
||
if (!channel.title) {
|
||
errors.push(`Строка ${rowNumber}: отсутствует название канала`);
|
||
return;
|
||
}
|
||
|
||
channels.push(channel);
|
||
} catch (err) {
|
||
errors.push(
|
||
`Строка ${rowNumber}: ошибка парсинга - ${
|
||
err instanceof Error ? err.message : "неизвестная ошибка"
|
||
}`
|
||
);
|
||
}
|
||
});
|
||
|
||
resolve({ channels, errors });
|
||
},
|
||
error: (err) => {
|
||
errors.push(`Ошибка парсинга CSV: ${err.message}`);
|
||
resolve({ channels: [], errors });
|
||
},
|
||
});
|
||
});
|
||
};
|
||
|
||
/**
|
||
* Универсальная функция парсинга файла
|
||
*/
|
||
export const parseChannelsFile = async (file: File): Promise<ParseResult> => {
|
||
const extension = file.name.split(".").pop()?.toLowerCase();
|
||
|
||
if (extension === "xlsx" || extension === "xls") {
|
||
return parseExcelFile(file);
|
||
} else if (extension === "csv") {
|
||
return parseCsvFile(file);
|
||
} else {
|
||
return {
|
||
channels: [],
|
||
errors: [
|
||
"Неподдерживаемый формат файла. Поддерживаются только .xlsx, .xls и .csv",
|
||
],
|
||
};
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Скачать шаблон для импорта
|
||
*/
|
||
export const downloadTemplate = (format: "xlsx" | "csv") => {
|
||
const headers = [
|
||
"Название канала*",
|
||
"Username (без @)",
|
||
"Telegram ID",
|
||
"Подписчиков",
|
||
"Описание",
|
||
];
|
||
const example = [
|
||
"Пример канала",
|
||
"example_channel",
|
||
"-1001234567890",
|
||
"10000",
|
||
"Описание канала",
|
||
];
|
||
|
||
if (format === "xlsx") {
|
||
const ws = XLSX.utils.aoa_to_sheet([headers, example]);
|
||
const wb = XLSX.utils.book_new();
|
||
XLSX.utils.book_append_sheet(wb, ws, "Channels");
|
||
XLSX.writeFile(wb, "channels_template.xlsx");
|
||
} else {
|
||
const csv = Papa.unparse([headers, example]);
|
||
const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
|
||
const link = document.createElement("a");
|
||
link.href = URL.createObjectURL(blob);
|
||
link.download = "channels_template.csv";
|
||
link.click();
|
||
}
|
||
};
|