审查视图

app/utils/fileUtil.ts 5.1 KB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155
import { getExcelData } from "./fileExport/export2Excel";
import { getWordData } from "./fileExport/word";
import { getPdfData } from "./fileExport/toPdf";

/**
 * 提取文件的扩展名
 * @param filename 原始文件名
 * @returns 文件扩展名(例如 ".jpg")
 */
function getFileExtension(filename: string): string {
  const lastDotIndex = filename.lastIndexOf(".");
  return lastDotIndex === -1 ? "" : filename.substring(lastDotIndex);
}

/**
 * 清理文件名,替换非字母数字字符为下划线并截断长度
 * @param filename 待处理的文件名(不含扩展名)
 * @param maxLength 文件名最大长度,默认50
 * @returns 清理后的文件名
 */
function cleanFileName(filename: string, maxLength: number = 50): string {
  const cleaned = filename
    .replace(/[^a-zA-Z0-9]/g, "_") // 非字母数字替换为下划线
    .replace(/_+/g, "_") // 合并连续下划线
    .replace(/^_+|_+$/g, "") // 去除首尾下划线
    .substring(0, maxLength); // 截断到指定长度
  return cleaned;
}

/**
 * 生成安全的随机字符串
 * @param length 字符串长度,默认8
 * @returns 随机字符串
 */
function generateRandomString(length: number = 8): string {
  const characters =
    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
  let result = "";

  // 优先使用加密安全的随机数生成器
  if (typeof crypto !== "undefined" && crypto.getRandomValues) {
    const values = new Uint8Array(length);
    crypto.getRandomValues(values);
    for (let i = 0; i < length; i++) {
      result += characters[values[i] % characters.length];
    }
  } else {
    // 降级方案:使用 Math.random()
    for (let i = 0; i < length; i++) {
      result += characters[Math.floor(Math.random() * characters.length)];
    }
  }
  return result;
}

/**
 * 生成唯一文件名
 * @param originalFileName 原始文件名
 * @returns 格式为 [清理后的名称]_[时间戳]_[随机字符串].[扩展名]
 */
export function generateUniqueFileName(originalFileName: string): string {
  const ext = getFileExtension(originalFileName);
  const nameWithoutExt = originalFileName.slice(0, -ext.length);
  const cleanedName = cleanFileName(nameWithoutExt);
  const timestamp = Date.now().toString();
  const randomStr = generateRandomString(8);

  // 组合最终文件名
  const uniquePart = `${timestamp}_${randomStr}`;
  const newName = cleanedName ? `${cleanedName}_${uniquePart}` : uniquePart;
  return `${newName}${ext}`;
}

export async function getFileByUrl(url: string): Promise<File> {
  try {
    // 1. 发起网络请求获取图片数据
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(`HTTP error! Status: ${response.status}`);
    }
    // 2. 提取文件名(处理URL中的特殊字符)
    const parsedUrl = new URL(url);
    let filename = "image.png";
    // 移除文件名中的查询参数和哈希片段(如 "image.jpg?param=1#section" → "image.jpg")
    filename = filename.split(/[?#]/)[0];
    // 3. 获取 MIME 类型(优先使用响应头类型)
    const mimeType = response.headers.get("Content-Type") || "";
    // 4. 转换为 Blob 并生成 File 对象
    const blob = await response.blob();
    return new File([blob], filename, {
      type: mimeType || blob.type, // 双重保障类型
      lastModified: Date.now(),
    });
  } catch (error) {
    throw new Error("Failed to download image");
  }
}

export async function processChatFile(): Promise<{
  data: string;
  name: string;
}> {
  return new Promise((resolve, reject) => {
    // 创建隐藏的文件输入元素
    const fileInput = document.createElement("input");
    fileInput.type = "file";
    fileInput.accept = ".xlsx, .xls, .pdf, .docx, .doc";
    fileInput.multiple = false;
    fileInput.style.display = "none";

    // 文件类型与处理函数映射
    const fileHandlers: Record<string, (file: File) => Promise<any>> = {
      ".xlsx": getExcelData,
      ".xls": getExcelData,
      ".doc": getWordData,
      ".docx": getWordData,
      ".pdf": getPdfData,
    };

    fileInput.onchange = async (event: Event) => {
      try {
        const input = event.target as HTMLInputElement;
        const file = input.files?.[0];
        if (!file) throw new Error("No file selected");

        // 获取文件扩展名并校验类型
        const fileExt = file.name
          .toLowerCase()
          .slice(file.name.lastIndexOf("."));
        if (!Object.keys(fileHandlers).includes(fileExt)) {
          throw new Error("Unsupported file type");
        }

        // 处理文件内容
        const handler = fileHandlers[fileExt];
        const filedata = await handler(file);

        // 格式化返回数据
        const data = `'''filedata
                                ${file.name}
                                ${JSON.stringify(filedata)}
                                '''filedata`;
        resolve({ data: data, name: file.name });
      } catch (error) {
        reject(error instanceof Error ? error : new Error(String(error)));
      } finally {
        fileInput.remove();
      }
    };

    // 触发文件选择
    document.body.appendChild(fileInput);
    fileInput.click();
  });
}