2025-06-17 22:37:37 +08:00
|
|
|
|
<!-- 文件上传 -->
|
|
|
|
|
|
<script setup lang="ts">
|
2025-12-13 18:09:12 +08:00
|
|
|
|
import type { FileItem } from '@/stores/modules/files';
|
2025-06-17 22:37:37 +08:00
|
|
|
|
import { useFileDialog } from '@vueuse/core';
|
|
|
|
|
|
import { ElMessage } from 'element-plus';
|
2025-12-13 23:08:11 +08:00
|
|
|
|
import mammoth from 'mammoth';
|
|
|
|
|
|
import * as pdfjsLib from 'pdfjs-dist';
|
|
|
|
|
|
import * as XLSX from 'xlsx';
|
2025-06-17 22:37:37 +08:00
|
|
|
|
import Popover from '@/components/Popover/index.vue';
|
|
|
|
|
|
import SvgIcon from '@/components/SvgIcon/index.vue';
|
|
|
|
|
|
import { useFilesStore } from '@/stores/modules/files';
|
|
|
|
|
|
|
2025-12-13 23:08:11 +08:00
|
|
|
|
// 配置 PDF.js worker - 使用稳定的 CDN
|
|
|
|
|
|
pdfjsLib.GlobalWorkerOptions.workerSrc = `https://cdn.jsdelivr.net/npm/pdfjs-dist@${pdfjsLib.version}/build/pdf.worker.min.mjs`;
|
|
|
|
|
|
|
2025-06-17 22:37:37 +08:00
|
|
|
|
const filesStore = useFilesStore();
|
|
|
|
|
|
|
2025-12-13 18:09:12 +08:00
|
|
|
|
// 文件大小限制 3MB
|
|
|
|
|
|
const MAX_FILE_SIZE = 3 * 1024 * 1024;
|
|
|
|
|
|
|
2025-12-13 23:08:11 +08:00
|
|
|
|
// 单个文件内容长度限制
|
|
|
|
|
|
const MAX_TEXT_FILE_LENGTH = 50000; // 文本文件最大字符数
|
|
|
|
|
|
const MAX_WORD_LENGTH = 30000; // Word 文档最大字符数
|
|
|
|
|
|
const MAX_EXCEL_ROWS = 100; // Excel 最大行数
|
|
|
|
|
|
const MAX_PDF_PAGES = 10; // PDF 最大页数
|
|
|
|
|
|
|
|
|
|
|
|
// 整个消息总长度限制(所有文件内容加起来,预估 token 安全限制)
|
|
|
|
|
|
// 272000 tokens * 0.55 安全系数 ≈ 150000 字符
|
|
|
|
|
|
const MAX_TOTAL_CONTENT_LENGTH = 150000;
|
2025-06-17 22:37:37 +08:00
|
|
|
|
|
|
|
|
|
|
const { reset, open, onChange } = useFileDialog({
|
2025-12-13 23:08:11 +08:00
|
|
|
|
// 支持图片、文档、文本文件等
|
|
|
|
|
|
accept: 'image/*,.txt,.log,.csv,.tsv,.md,.markdown,.json,.xml,.yaml,.yml,.toml,.ini,.conf,.config,.properties,.prop,.env,'
|
|
|
|
|
|
+ '.js,.jsx,.ts,.tsx,.vue,.html,.htm,.css,.scss,.sass,.less,.styl,'
|
|
|
|
|
|
+ '.java,.c,.cpp,.h,.hpp,.cs,.py,.rb,.go,.rs,.swift,.kt,.php,.sh,.bash,.zsh,.fish,.bat,.cmd,.ps1,'
|
|
|
|
|
|
+ '.sql,.graphql,.proto,.thrift,'
|
|
|
|
|
|
+ '.dockerfile,.gitignore,.gitattributes,.editorconfig,.npmrc,.nvmrc,'
|
|
|
|
|
|
+ '.sln,.csproj,.vbproj,.fsproj,.props,.targets,'
|
|
|
|
|
|
+ '.xlsx,.xls,.csv,.docx,.pdf',
|
|
|
|
|
|
directory: false,
|
|
|
|
|
|
multiple: true,
|
2025-06-17 22:37:37 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2025-12-13 23:08:11 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 压缩图片
|
|
|
|
|
|
* @param {File} file - 原始图片文件
|
|
|
|
|
|
* @param {number} maxWidth - 最大宽度,默认 1024px
|
|
|
|
|
|
* @param {number} maxHeight - 最大高度,默认 1024px
|
|
|
|
|
|
* @param {number} quality - 压缩质量,0-1之间,默认 0.8
|
|
|
|
|
|
* @returns {Promise<Blob>} 压缩后的图片 Blob
|
|
|
|
|
|
*/
|
2025-12-13 18:09:12 +08:00
|
|
|
|
function compressImage(file: File, maxWidth = 1024, maxHeight = 1024, quality = 0.8): Promise<Blob> {
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
|
const reader = new FileReader();
|
|
|
|
|
|
reader.onload = (e) => {
|
|
|
|
|
|
const img = new Image();
|
|
|
|
|
|
img.onload = () => {
|
|
|
|
|
|
const canvas = document.createElement('canvas');
|
|
|
|
|
|
let width = img.width;
|
|
|
|
|
|
let height = img.height;
|
|
|
|
|
|
|
|
|
|
|
|
// 计算缩放比例
|
|
|
|
|
|
if (width > maxWidth || height > maxHeight) {
|
|
|
|
|
|
const ratio = Math.min(maxWidth / width, maxHeight / height);
|
|
|
|
|
|
width = width * ratio;
|
|
|
|
|
|
height = height * ratio;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
canvas.width = width;
|
|
|
|
|
|
canvas.height = height;
|
|
|
|
|
|
|
|
|
|
|
|
const ctx = canvas.getContext('2d')!;
|
|
|
|
|
|
ctx.drawImage(img, 0, 0, width, height);
|
|
|
|
|
|
|
|
|
|
|
|
// 转换为 Blob
|
|
|
|
|
|
canvas.toBlob(
|
|
|
|
|
|
(blob) => {
|
|
|
|
|
|
if (blob) {
|
|
|
|
|
|
resolve(blob);
|
2025-12-13 23:08:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
else {
|
2025-12-13 18:09:12 +08:00
|
|
|
|
reject(new Error('压缩失败'));
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
file.type,
|
2025-12-13 23:08:11 +08:00
|
|
|
|
quality,
|
2025-12-13 18:09:12 +08:00
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
img.onerror = reject;
|
|
|
|
|
|
img.src = e.target?.result as string;
|
|
|
|
|
|
};
|
|
|
|
|
|
reader.onerror = reject;
|
|
|
|
|
|
reader.readAsDataURL(file);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-13 23:08:11 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 将 Blob 转换为 base64 格式
|
|
|
|
|
|
* @param {Blob} blob - 要转换的 Blob 对象
|
|
|
|
|
|
* @returns {Promise<string>} base64 编码的字符串(包含 data:xxx;base64, 前缀)
|
|
|
|
|
|
*/
|
2025-12-13 18:09:12 +08:00
|
|
|
|
function blobToBase64(blob: Blob): Promise<string> {
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
|
const reader = new FileReader();
|
|
|
|
|
|
reader.onload = () => {
|
|
|
|
|
|
resolve(reader.result as string);
|
|
|
|
|
|
};
|
|
|
|
|
|
reader.onerror = reject;
|
|
|
|
|
|
reader.readAsDataURL(blob);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-13 23:08:11 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 读取文本文件内容
|
|
|
|
|
|
* @param {File} file - 文本文件
|
|
|
|
|
|
* @returns {Promise<string>} 文件内容字符串
|
|
|
|
|
|
*/
|
|
|
|
|
|
function readTextFile(file: File): Promise<string> {
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
|
const reader = new FileReader();
|
|
|
|
|
|
reader.onload = () => {
|
|
|
|
|
|
resolve(reader.result as string);
|
|
|
|
|
|
};
|
|
|
|
|
|
reader.onerror = reject;
|
|
|
|
|
|
reader.readAsText(file, 'UTF-8');
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 判断是否为文本文件
|
|
|
|
|
|
* 通过 MIME 类型或文件扩展名判断
|
|
|
|
|
|
* @param {File} file - 要判断的文件
|
|
|
|
|
|
* @returns {boolean} 是否为文本文件
|
|
|
|
|
|
*/
|
|
|
|
|
|
function isTextFile(file: File): boolean {
|
|
|
|
|
|
// 通过 MIME type 判断
|
|
|
|
|
|
if (file.type.startsWith('text/')) {
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 通过扩展名判断(更全面的列表)
|
|
|
|
|
|
const textExtensions = [
|
|
|
|
|
|
// 通用文本
|
|
|
|
|
|
'txt',
|
|
|
|
|
|
'log',
|
|
|
|
|
|
'md',
|
|
|
|
|
|
'markdown',
|
|
|
|
|
|
'rtf',
|
|
|
|
|
|
// 配置文件
|
|
|
|
|
|
'json',
|
|
|
|
|
|
'xml',
|
|
|
|
|
|
'yaml',
|
|
|
|
|
|
'yml',
|
|
|
|
|
|
'toml',
|
|
|
|
|
|
'ini',
|
|
|
|
|
|
'conf',
|
|
|
|
|
|
'config',
|
|
|
|
|
|
'properties',
|
|
|
|
|
|
'prop',
|
|
|
|
|
|
'env',
|
|
|
|
|
|
// 前端
|
|
|
|
|
|
'js',
|
|
|
|
|
|
'jsx',
|
|
|
|
|
|
'ts',
|
|
|
|
|
|
'tsx',
|
|
|
|
|
|
'vue',
|
|
|
|
|
|
'html',
|
|
|
|
|
|
'htm',
|
|
|
|
|
|
'css',
|
|
|
|
|
|
'scss',
|
|
|
|
|
|
'sass',
|
|
|
|
|
|
'less',
|
|
|
|
|
|
'styl',
|
|
|
|
|
|
// 编程语言
|
|
|
|
|
|
'java',
|
|
|
|
|
|
'c',
|
|
|
|
|
|
'cpp',
|
|
|
|
|
|
'h',
|
|
|
|
|
|
'hpp',
|
|
|
|
|
|
'cs',
|
|
|
|
|
|
'py',
|
|
|
|
|
|
'rb',
|
|
|
|
|
|
'go',
|
|
|
|
|
|
'rs',
|
|
|
|
|
|
'swift',
|
|
|
|
|
|
'kt',
|
|
|
|
|
|
'php',
|
|
|
|
|
|
// 脚本
|
|
|
|
|
|
'sh',
|
|
|
|
|
|
'bash',
|
|
|
|
|
|
'zsh',
|
|
|
|
|
|
'fish',
|
|
|
|
|
|
'bat',
|
|
|
|
|
|
'cmd',
|
|
|
|
|
|
'ps1',
|
|
|
|
|
|
// 数据库/API
|
|
|
|
|
|
'sql',
|
|
|
|
|
|
'graphql',
|
|
|
|
|
|
'proto',
|
|
|
|
|
|
'thrift',
|
|
|
|
|
|
// 版本控制/工具
|
|
|
|
|
|
'dockerfile',
|
|
|
|
|
|
'gitignore',
|
|
|
|
|
|
'gitattributes',
|
|
|
|
|
|
'editorconfig',
|
|
|
|
|
|
'npmrc',
|
|
|
|
|
|
'nvmrc',
|
|
|
|
|
|
// .NET 项目文件
|
|
|
|
|
|
'sln',
|
|
|
|
|
|
'csproj',
|
|
|
|
|
|
'vbproj',
|
|
|
|
|
|
'fsproj',
|
|
|
|
|
|
'props',
|
|
|
|
|
|
'targets',
|
|
|
|
|
|
// 数据文件
|
|
|
|
|
|
'csv',
|
|
|
|
|
|
'tsv',
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
const ext = file.name.split('.').pop()?.toLowerCase();
|
|
|
|
|
|
return ext ? textExtensions.includes(ext) : false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 解析 Excel 文件,提取前 N 行数据转为 CSV 格式
|
|
|
|
|
|
* @param {File} file - Excel 文件 (.xlsx, .xls)
|
|
|
|
|
|
* @returns {Promise<{content: string, totalRows: number, extractedRows: number}>}
|
|
|
|
|
|
* - content: CSV 格式的文本内容
|
|
|
|
|
|
* - totalRows: 文件总行数
|
|
|
|
|
|
* - extractedRows: 实际提取的行数(受 MAX_EXCEL_ROWS 限制)
|
|
|
|
|
|
*/
|
|
|
|
|
|
async function parseExcel(file: File): Promise<{ content: string; totalRows: number; extractedRows: number }> {
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
|
const reader = new FileReader();
|
|
|
|
|
|
reader.onload = (e) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const data = new Uint8Array(e.target?.result as ArrayBuffer);
|
|
|
|
|
|
const workbook = XLSX.read(data, { type: 'array' });
|
|
|
|
|
|
|
|
|
|
|
|
let result = '';
|
|
|
|
|
|
let totalRows = 0;
|
|
|
|
|
|
let extractedRows = 0;
|
|
|
|
|
|
|
|
|
|
|
|
workbook.SheetNames.forEach((sheetName, index) => {
|
|
|
|
|
|
const worksheet = workbook.Sheets[sheetName];
|
|
|
|
|
|
|
|
|
|
|
|
// 获取工作表的范围
|
|
|
|
|
|
const range = XLSX.utils.decode_range(worksheet['!ref'] || 'A1');
|
|
|
|
|
|
const sheetTotalRows = range.e.r - range.s.r + 1;
|
|
|
|
|
|
totalRows += sheetTotalRows;
|
|
|
|
|
|
|
|
|
|
|
|
// 限制行数
|
|
|
|
|
|
const rowsToExtract = Math.min(sheetTotalRows, MAX_EXCEL_ROWS);
|
|
|
|
|
|
extractedRows += rowsToExtract;
|
|
|
|
|
|
|
|
|
|
|
|
// 创建新的范围,只包含前 N 行
|
|
|
|
|
|
const limitedRange = {
|
|
|
|
|
|
s: { r: range.s.r, c: range.s.c },
|
|
|
|
|
|
e: { r: range.s.r + rowsToExtract - 1, c: range.e.c },
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 提取限制范围内的数据
|
|
|
|
|
|
const limitedData: any[][] = [];
|
|
|
|
|
|
for (let row = limitedRange.s.r; row <= limitedRange.e.r; row++) {
|
|
|
|
|
|
const rowData: any[] = [];
|
|
|
|
|
|
for (let col = limitedRange.s.c; col <= limitedRange.e.c; col++) {
|
|
|
|
|
|
const cellAddress = XLSX.utils.encode_cell({ r: row, c: col });
|
|
|
|
|
|
const cell = worksheet[cellAddress];
|
|
|
|
|
|
rowData.push(cell ? cell.v : '');
|
|
|
|
|
|
}
|
|
|
|
|
|
limitedData.push(rowData);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 转换为 CSV
|
|
|
|
|
|
const csvData = limitedData.map(row => row.join(',')).join('\n');
|
|
|
|
|
|
|
|
|
|
|
|
if (workbook.SheetNames.length > 1) {
|
|
|
|
|
|
result += `=== Sheet: ${sheetName} ===\n`;
|
|
|
|
|
|
}
|
|
|
|
|
|
result += csvData;
|
|
|
|
|
|
if (index < workbook.SheetNames.length - 1) {
|
|
|
|
|
|
result += '\n\n';
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
resolve({ content: result, totalRows, extractedRows });
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (error) {
|
|
|
|
|
|
reject(error);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
reader.onerror = reject;
|
|
|
|
|
|
reader.readAsArrayBuffer(file);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 解析 Word 文档,提取纯文本内容
|
|
|
|
|
|
* @param {File} file - Word 文档 (.docx)
|
|
|
|
|
|
* @returns {Promise<{content: string, totalLength: number, extracted: boolean}>}
|
|
|
|
|
|
* - content: 提取的文本内容
|
|
|
|
|
|
* - totalLength: 原始文本总长度
|
|
|
|
|
|
* - extracted: 是否被截断(超过 MAX_WORD_LENGTH)
|
|
|
|
|
|
*/
|
|
|
|
|
|
async function parseWord(file: File): Promise<{ content: string; totalLength: number; extracted: boolean }> {
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
|
const reader = new FileReader();
|
|
|
|
|
|
reader.onload = async (e) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const arrayBuffer = e.target?.result as ArrayBuffer;
|
|
|
|
|
|
const result = await mammoth.extractRawText({ arrayBuffer });
|
|
|
|
|
|
const fullText = result.value;
|
|
|
|
|
|
const totalLength = fullText.length;
|
|
|
|
|
|
|
|
|
|
|
|
if (totalLength > MAX_WORD_LENGTH) {
|
|
|
|
|
|
const truncated = fullText.substring(0, MAX_WORD_LENGTH);
|
|
|
|
|
|
resolve({ content: truncated, totalLength, extracted: true });
|
|
|
|
|
|
}
|
|
|
|
|
|
else {
|
|
|
|
|
|
resolve({ content: fullText, totalLength, extracted: false });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (error) {
|
|
|
|
|
|
reject(error);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
reader.onerror = reject;
|
|
|
|
|
|
reader.readAsArrayBuffer(file);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 解析 PDF 文件,提取前 N 页的文本内容
|
|
|
|
|
|
* @param {File} file - PDF 文件
|
|
|
|
|
|
* @returns {Promise<{content: string, totalPages: number, extractedPages: number}>}
|
|
|
|
|
|
* - content: 提取的文本内容
|
|
|
|
|
|
* - totalPages: 文件总页数
|
|
|
|
|
|
* - extractedPages: 实际提取的页数(受 MAX_PDF_PAGES 限制)
|
|
|
|
|
|
*/
|
|
|
|
|
|
async function parsePDF(file: File): Promise<{ content: string; totalPages: number; extractedPages: number }> {
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
|
const reader = new FileReader();
|
|
|
|
|
|
reader.onload = async (e) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const typedArray = new Uint8Array(e.target?.result as ArrayBuffer);
|
|
|
|
|
|
const pdf = await pdfjsLib.getDocument(typedArray).promise;
|
|
|
|
|
|
const totalPages = pdf.numPages;
|
|
|
|
|
|
const pagesToExtract = Math.min(totalPages, MAX_PDF_PAGES);
|
|
|
|
|
|
|
|
|
|
|
|
let fullText = '';
|
|
|
|
|
|
for (let i = 1; i <= pagesToExtract; i++) {
|
|
|
|
|
|
const page = await pdf.getPage(i);
|
|
|
|
|
|
const textContent = await page.getTextContent();
|
|
|
|
|
|
const pageText = textContent.items.map((item: any) => item.str).join(' ');
|
|
|
|
|
|
fullText += `${pageText}\n`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
resolve({ content: fullText, totalPages, extractedPages: pagesToExtract });
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (error) {
|
|
|
|
|
|
reject(error);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
reader.onerror = reject;
|
|
|
|
|
|
reader.readAsArrayBuffer(file);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取文件扩展名
|
|
|
|
|
|
* @param {string} filename - 文件名
|
|
|
|
|
|
* @returns {string} 小写的扩展名,无点号
|
|
|
|
|
|
*/
|
|
|
|
|
|
function getFileExtension(filename: string): string {
|
|
|
|
|
|
return filename.split('.').pop()?.toLowerCase() || '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-13 18:09:12 +08:00
|
|
|
|
onChange(async (files) => {
|
2025-06-17 22:37:37 +08:00
|
|
|
|
if (!files)
|
|
|
|
|
|
return;
|
2025-12-13 18:09:12 +08:00
|
|
|
|
|
|
|
|
|
|
const arr = [] as FileItem[];
|
2025-12-13 23:08:11 +08:00
|
|
|
|
let totalContentLength = 0; // 跟踪总内容长度
|
|
|
|
|
|
|
|
|
|
|
|
// 先计算已有文件的总内容长度
|
|
|
|
|
|
filesStore.filesList.forEach((f) => {
|
|
|
|
|
|
if (f.fileType === 'text' && f.fileContent) {
|
|
|
|
|
|
totalContentLength += f.fileContent.length;
|
|
|
|
|
|
}
|
|
|
|
|
|
// 图片 base64 也计入(虽然转 token 时不同,但也要计算)
|
|
|
|
|
|
if (f.fileType === 'image' && f.base64) {
|
|
|
|
|
|
// base64 转 token 比例约 1:1.5,这里保守估计
|
|
|
|
|
|
totalContentLength += Math.floor(f.base64.length * 0.5);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2025-12-13 18:09:12 +08:00
|
|
|
|
|
2025-06-17 22:37:37 +08:00
|
|
|
|
for (let i = 0; i < files!.length; i++) {
|
|
|
|
|
|
const file = files![i];
|
2025-12-13 18:09:12 +08:00
|
|
|
|
|
|
|
|
|
|
// 验证文件大小
|
|
|
|
|
|
if (file.size > MAX_FILE_SIZE) {
|
|
|
|
|
|
ElMessage.error(`文件 ${file.name} 超过 3MB 限制,已跳过`);
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-13 23:08:11 +08:00
|
|
|
|
const ext = getFileExtension(file.name);
|
2025-12-13 18:09:12 +08:00
|
|
|
|
const isImage = file.type.startsWith('image/');
|
2025-12-13 23:08:11 +08:00
|
|
|
|
const isExcel = ['xlsx', 'xls'].includes(ext);
|
|
|
|
|
|
const isWord = ext === 'docx';
|
|
|
|
|
|
const isPDF = ext === 'pdf';
|
|
|
|
|
|
const isText = isTextFile(file);
|
2025-12-13 18:09:12 +08:00
|
|
|
|
|
2025-12-13 23:08:11 +08:00
|
|
|
|
// 处理图片文件
|
2025-12-13 18:09:12 +08:00
|
|
|
|
if (isImage) {
|
|
|
|
|
|
try {
|
2025-12-14 17:25:20 +08:00
|
|
|
|
// 多级压缩策略:逐步降低质量和分辨率
|
|
|
|
|
|
const compressionLevels = [
|
|
|
|
|
|
{ maxWidth: 800, maxHeight: 800, quality: 0.6 },
|
|
|
|
|
|
{ maxWidth: 600, maxHeight: 600, quality: 0.5 },
|
|
|
|
|
|
{ maxWidth: 400, maxHeight: 400, quality: 0.4 },
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
let compressedBlob: Blob | null = null;
|
|
|
|
|
|
let base64 = '';
|
|
|
|
|
|
let compressionLevel = 0;
|
|
|
|
|
|
|
|
|
|
|
|
// 尝试不同级别的压缩
|
|
|
|
|
|
for (const level of compressionLevels) {
|
|
|
|
|
|
compressionLevel++;
|
|
|
|
|
|
compressedBlob = await compressImage(file, level.maxWidth, level.maxHeight, level.quality);
|
|
|
|
|
|
base64 = await blobToBase64(compressedBlob);
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否满足总长度限制
|
|
|
|
|
|
const estimatedLength = Math.floor(base64.length * 0.5);
|
|
|
|
|
|
if (totalContentLength + estimatedLength <= MAX_TOTAL_CONTENT_LENGTH) {
|
|
|
|
|
|
// 满足限制,使用当前压缩级别
|
|
|
|
|
|
totalContentLength += estimatedLength;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果是最后一级压缩仍然超限,则跳过
|
|
|
|
|
|
if (compressionLevel === compressionLevels.length) {
|
|
|
|
|
|
const fileSizeMB = (file.size / 1024 / 1024).toFixed(2);
|
|
|
|
|
|
ElMessage.error(`${file.name} (${fileSizeMB}MB) 即使压缩后仍超过总内容限制,已跳过`);
|
|
|
|
|
|
compressedBlob = null;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
2025-12-13 23:08:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-14 17:25:20 +08:00
|
|
|
|
// 如果压缩失败,跳过此文件
|
|
|
|
|
|
if (!compressedBlob) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
2025-12-13 18:09:12 +08:00
|
|
|
|
|
|
|
|
|
|
// 计算压缩比例
|
|
|
|
|
|
const originalSize = (file.size / 1024).toFixed(2);
|
|
|
|
|
|
const compressedSize = (compressedBlob.size / 1024).toFixed(2);
|
2025-12-14 17:25:20 +08:00
|
|
|
|
console.log(`图片压缩: ${file.name} - 原始: ${originalSize}KB, 压缩后: ${compressedSize}KB (级别${compressionLevel})`);
|
2025-12-13 23:08:11 +08:00
|
|
|
|
|
|
|
|
|
|
arr.push({
|
|
|
|
|
|
uid: crypto.randomUUID(),
|
|
|
|
|
|
name: file.name,
|
|
|
|
|
|
fileSize: file.size,
|
|
|
|
|
|
file,
|
|
|
|
|
|
maxWidth: '200px',
|
|
|
|
|
|
showDelIcon: true,
|
|
|
|
|
|
imgPreview: true,
|
|
|
|
|
|
imgVariant: 'square',
|
|
|
|
|
|
url: base64, // 使用压缩后的 base64 作为预览地址
|
|
|
|
|
|
isUploaded: true,
|
|
|
|
|
|
base64,
|
|
|
|
|
|
fileType: 'image',
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (error) {
|
2025-12-13 18:09:12 +08:00
|
|
|
|
console.error('压缩图片失败:', error);
|
|
|
|
|
|
ElMessage.error(`${file.name} 压缩失败`);
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-12-13 23:08:11 +08:00
|
|
|
|
// 处理 Excel 文件
|
|
|
|
|
|
else if (isExcel) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const result = await parseExcel(file);
|
|
|
|
|
|
|
2025-12-14 17:25:20 +08:00
|
|
|
|
// 动态裁剪内容以适应剩余空间
|
|
|
|
|
|
let finalContent = result.content;
|
|
|
|
|
|
let wasTruncated = result.totalRows > MAX_EXCEL_ROWS;
|
|
|
|
|
|
|
|
|
|
|
|
// 如果超过总内容限制,裁剪内容
|
|
|
|
|
|
const remainingSpace = MAX_TOTAL_CONTENT_LENGTH - totalContentLength;
|
|
|
|
|
|
if (result.content.length > remainingSpace && remainingSpace > 1000) {
|
|
|
|
|
|
// 至少保留1000字符才有意义
|
|
|
|
|
|
finalContent = result.content.substring(0, remainingSpace);
|
|
|
|
|
|
wasTruncated = true;
|
|
|
|
|
|
} else if (remainingSpace <= 1000) {
|
|
|
|
|
|
const fileSizeKB = (file.size / 1024).toFixed(2);
|
|
|
|
|
|
ElMessage.error(`${file.name} (${fileSizeKB}KB) 会超过总内容限制,已跳过`);
|
2025-12-13 23:08:11 +08:00
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-14 17:25:20 +08:00
|
|
|
|
totalContentLength += finalContent.length;
|
2025-12-13 23:08:11 +08:00
|
|
|
|
|
|
|
|
|
|
arr.push({
|
|
|
|
|
|
uid: crypto.randomUUID(),
|
|
|
|
|
|
name: file.name,
|
|
|
|
|
|
fileSize: file.size,
|
|
|
|
|
|
file,
|
|
|
|
|
|
maxWidth: '200px',
|
|
|
|
|
|
showDelIcon: true,
|
|
|
|
|
|
imgPreview: false,
|
|
|
|
|
|
isUploaded: true,
|
2025-12-14 17:25:20 +08:00
|
|
|
|
fileContent: finalContent,
|
2025-12-13 23:08:11 +08:00
|
|
|
|
fileType: 'text',
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 提示信息
|
2025-12-14 17:25:20 +08:00
|
|
|
|
if (wasTruncated) {
|
|
|
|
|
|
const fileSizeKB = (file.size / 1024).toFixed(2);
|
|
|
|
|
|
ElMessage.warning(`${file.name} (${fileSizeKB}KB) 内容过大,已自动截取部分内容`);
|
2025-12-13 23:08:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-14 17:25:20 +08:00
|
|
|
|
console.log(`Excel 解析: ${file.name} - 大小: ${(file.size / 1024).toFixed(2)}KB, 总行数: ${result.totalRows}, 已提取: ${result.extractedRows} 行, 内容长度: ${finalContent.length} 字符`);
|
2025-12-13 23:08:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
catch (error) {
|
|
|
|
|
|
console.error('解析 Excel 失败:', error);
|
|
|
|
|
|
ElMessage.error(`${file.name} 解析失败`);
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// 处理 Word 文档
|
|
|
|
|
|
else if (isWord) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const result = await parseWord(file);
|
|
|
|
|
|
|
2025-12-14 17:25:20 +08:00
|
|
|
|
// 动态裁剪内容以适应剩余空间
|
|
|
|
|
|
let finalContent = result.content;
|
|
|
|
|
|
let wasTruncated = result.extracted;
|
|
|
|
|
|
|
|
|
|
|
|
// 如果超过总内容限制,裁剪内容
|
|
|
|
|
|
const remainingSpace = MAX_TOTAL_CONTENT_LENGTH - totalContentLength;
|
|
|
|
|
|
if (result.content.length > remainingSpace && remainingSpace > 1000) {
|
|
|
|
|
|
finalContent = result.content.substring(0, remainingSpace);
|
|
|
|
|
|
wasTruncated = true;
|
|
|
|
|
|
} else if (remainingSpace <= 1000) {
|
|
|
|
|
|
const fileSizeKB = (file.size / 1024).toFixed(2);
|
|
|
|
|
|
ElMessage.error(`${file.name} (${fileSizeKB}KB) 会超过总内容限制,已跳过`);
|
2025-12-13 23:08:11 +08:00
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-14 17:25:20 +08:00
|
|
|
|
totalContentLength += finalContent.length;
|
2025-12-13 18:09:12 +08:00
|
|
|
|
|
2025-12-13 23:08:11 +08:00
|
|
|
|
arr.push({
|
|
|
|
|
|
uid: crypto.randomUUID(),
|
|
|
|
|
|
name: file.name,
|
|
|
|
|
|
fileSize: file.size,
|
|
|
|
|
|
file,
|
|
|
|
|
|
maxWidth: '200px',
|
|
|
|
|
|
showDelIcon: true,
|
|
|
|
|
|
imgPreview: false,
|
|
|
|
|
|
isUploaded: true,
|
2025-12-14 17:25:20 +08:00
|
|
|
|
fileContent: finalContent,
|
2025-12-13 23:08:11 +08:00
|
|
|
|
fileType: 'text',
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 提示信息
|
2025-12-14 17:25:20 +08:00
|
|
|
|
if (wasTruncated) {
|
|
|
|
|
|
const fileSizeKB = (file.size / 1024).toFixed(2);
|
|
|
|
|
|
ElMessage.warning(`${file.name} (${fileSizeKB}KB) 内容过大,已自动截取部分内容`);
|
2025-12-13 23:08:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-14 17:25:20 +08:00
|
|
|
|
console.log(`Word 解析: ${file.name} - 大小: ${(file.size / 1024).toFixed(2)}KB, 总长度: ${result.totalLength}, 已提取: ${finalContent.length} 字符`);
|
2025-12-13 23:08:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
catch (error) {
|
|
|
|
|
|
console.error('解析 Word 失败:', error);
|
|
|
|
|
|
ElMessage.error(`${file.name} 解析失败`);
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// 处理 PDF 文件
|
|
|
|
|
|
else if (isPDF) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const result = await parsePDF(file);
|
|
|
|
|
|
|
2025-12-14 17:25:20 +08:00
|
|
|
|
// 动态裁剪内容以适应剩余空间
|
|
|
|
|
|
let finalContent = result.content;
|
|
|
|
|
|
let wasTruncated = result.totalPages > MAX_PDF_PAGES;
|
|
|
|
|
|
|
|
|
|
|
|
// 如果超过总内容限制,裁剪内容
|
|
|
|
|
|
const remainingSpace = MAX_TOTAL_CONTENT_LENGTH - totalContentLength;
|
|
|
|
|
|
if (result.content.length > remainingSpace && remainingSpace > 1000) {
|
|
|
|
|
|
finalContent = result.content.substring(0, remainingSpace);
|
|
|
|
|
|
wasTruncated = true;
|
|
|
|
|
|
} else if (remainingSpace <= 1000) {
|
|
|
|
|
|
const fileSizeKB = (file.size / 1024).toFixed(2);
|
|
|
|
|
|
ElMessage.error(`${file.name} (${fileSizeKB}KB) 会超过总内容限制,已跳过`);
|
2025-12-13 23:08:11 +08:00
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-14 17:25:20 +08:00
|
|
|
|
totalContentLength += finalContent.length;
|
2025-12-13 23:08:11 +08:00
|
|
|
|
|
|
|
|
|
|
arr.push({
|
|
|
|
|
|
uid: crypto.randomUUID(),
|
|
|
|
|
|
name: file.name,
|
|
|
|
|
|
fileSize: file.size,
|
|
|
|
|
|
file,
|
|
|
|
|
|
maxWidth: '200px',
|
|
|
|
|
|
showDelIcon: true,
|
|
|
|
|
|
imgPreview: false,
|
|
|
|
|
|
isUploaded: true,
|
2025-12-14 17:25:20 +08:00
|
|
|
|
fileContent: finalContent,
|
2025-12-13 23:08:11 +08:00
|
|
|
|
fileType: 'text',
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 提示信息
|
2025-12-14 17:25:20 +08:00
|
|
|
|
if (wasTruncated) {
|
|
|
|
|
|
const fileSizeKB = (file.size / 1024).toFixed(2);
|
|
|
|
|
|
ElMessage.warning(`${file.name} (${fileSizeKB}KB) 内容过大,已自动截取部分内容`);
|
2025-12-13 23:08:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-14 17:25:20 +08:00
|
|
|
|
console.log(`PDF 解析: ${file.name} - 大小: ${(file.size / 1024).toFixed(2)}KB, 总页数: ${result.totalPages}, 已提取: ${result.extractedPages} 页, 内容长度: ${finalContent.length} 字符`);
|
2025-12-13 23:08:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
catch (error) {
|
|
|
|
|
|
console.error('解析 PDF 失败:', error);
|
|
|
|
|
|
ElMessage.error(`${file.name} 解析失败`);
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// 处理文本文件
|
|
|
|
|
|
else if (isText) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 读取文本文件内容
|
|
|
|
|
|
const content = await readTextFile(file);
|
|
|
|
|
|
|
|
|
|
|
|
// 限制单个文本文件长度
|
|
|
|
|
|
let finalContent = content;
|
|
|
|
|
|
let truncated = false;
|
|
|
|
|
|
if (content.length > MAX_TEXT_FILE_LENGTH) {
|
|
|
|
|
|
finalContent = content.substring(0, MAX_TEXT_FILE_LENGTH);
|
|
|
|
|
|
truncated = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-14 17:25:20 +08:00
|
|
|
|
// 动态裁剪内容以适应剩余空间
|
|
|
|
|
|
const remainingSpace = MAX_TOTAL_CONTENT_LENGTH - totalContentLength;
|
|
|
|
|
|
if (finalContent.length > remainingSpace && remainingSpace > 1000) {
|
|
|
|
|
|
finalContent = finalContent.substring(0, remainingSpace);
|
|
|
|
|
|
truncated = true;
|
|
|
|
|
|
} else if (remainingSpace <= 1000) {
|
|
|
|
|
|
const fileSizeKB = (file.size / 1024).toFixed(2);
|
|
|
|
|
|
ElMessage.error(`${file.name} (${fileSizeKB}KB) 会超过总内容限制,已跳过`);
|
2025-12-13 23:08:11 +08:00
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
totalContentLength += finalContent.length;
|
|
|
|
|
|
|
|
|
|
|
|
arr.push({
|
|
|
|
|
|
uid: crypto.randomUUID(),
|
|
|
|
|
|
name: file.name,
|
|
|
|
|
|
fileSize: file.size,
|
|
|
|
|
|
file,
|
|
|
|
|
|
maxWidth: '200px',
|
|
|
|
|
|
showDelIcon: true,
|
|
|
|
|
|
imgPreview: false,
|
|
|
|
|
|
isUploaded: true,
|
|
|
|
|
|
fileContent: finalContent,
|
|
|
|
|
|
fileType: 'text',
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 提示信息
|
|
|
|
|
|
if (truncated) {
|
2025-12-14 17:25:20 +08:00
|
|
|
|
const fileSizeKB = (file.size / 1024).toFixed(2);
|
|
|
|
|
|
ElMessage.warning(`${file.name} (${fileSizeKB}KB) 内容过大,已自动截取部分内容`);
|
2025-12-13 23:08:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`文本文件读取: ${file.name} - 大小: ${(file.size / 1024).toFixed(2)}KB, 内容长度: ${content.length} 字符`);
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (error) {
|
|
|
|
|
|
console.error('读取文件失败:', error);
|
|
|
|
|
|
ElMessage.error(`${file.name} 读取失败`);
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// 不支持的文件类型
|
|
|
|
|
|
else {
|
|
|
|
|
|
ElMessage.warning(`${file.name} 不是支持的文件类型`);
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
2025-06-17 22:37:37 +08:00
|
|
|
|
}
|
2025-12-13 18:09:12 +08:00
|
|
|
|
|
|
|
|
|
|
if (arr.length > 0) {
|
|
|
|
|
|
filesStore.setFilesList([...filesStore.filesList, ...arr]);
|
2025-12-14 17:25:20 +08:00
|
|
|
|
ElMessage.success(`已添加 ${arr.length} 个文件`);
|
2025-12-13 18:09:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-17 22:37:37 +08:00
|
|
|
|
// 重置文件选择器
|
|
|
|
|
|
nextTick(() => reset());
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-12-13 23:08:11 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 打开文件选择对话框
|
|
|
|
|
|
*/
|
2025-06-17 22:37:37 +08:00
|
|
|
|
function handleUploadFiles() {
|
|
|
|
|
|
open();
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
|
<div class="files-select">
|
2025-12-13 23:08:11 +08:00
|
|
|
|
<!-- 直接点击上传,添加 tooltip 提示 -->
|
|
|
|
|
|
<el-tooltip
|
|
|
|
|
|
content="上传文件或图片(支持 Excel、Word、PDF、代码文件等,最大3MB)"
|
|
|
|
|
|
placement="top"
|
2025-06-17 22:37:37 +08:00
|
|
|
|
>
|
2025-12-13 23:08:11 +08:00
|
|
|
|
<div
|
|
|
|
|
|
class="flex items-center gap-4px p-10px rounded-10px cursor-pointer font-size-14px border-1px border-[rgba(0,0,0,0.08)] border-solid hover:bg-[rgba(0,0,0,.04)]"
|
|
|
|
|
|
@click="handleUploadFiles"
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-icon>
|
|
|
|
|
|
<Paperclip />
|
|
|
|
|
|
</el-icon>
|
2025-06-17 22:37:37 +08:00
|
|
|
|
</div>
|
2025-12-13 23:08:11 +08:00
|
|
|
|
</el-tooltip>
|
2025-06-17 22:37:37 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped lang="scss"></style>
|