gaoluyang
2026-06-24 c0cb161bb52ce0fbdce5c66ec391d107c75e2452
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
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
/**
 * 根据支持的文件类型生成 accept 属性值
 *
 * @param supportedFileTypes 支持的文件类型数组,如 ['PDF', 'DOC', 'DOCX']
 * @returns 用于文件上传组件 accept 属性的字符串
 */
export function generateAcceptedFileTypes(
  supportedFileTypes: string[],
): string {
  const allowedExtensions = supportedFileTypes.map((ext) => ext.toLowerCase());
  const mimeTypes: string[] = [];
 
  // 添加常见的 MIME 类型映射
  if (allowedExtensions.includes('txt')) {
    mimeTypes.push('text/plain');
  }
  if (allowedExtensions.includes('pdf')) {
    mimeTypes.push('application/pdf');
  }
  if (allowedExtensions.includes('html') || allowedExtensions.includes('htm')) {
    mimeTypes.push('text/html');
  }
  if (allowedExtensions.includes('csv')) {
    mimeTypes.push('text/csv');
  }
  if (allowedExtensions.includes('xlsx') || allowedExtensions.includes('xls')) {
    mimeTypes.push(
      'application/vnd.ms-excel',
      'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
    );
  }
  if (allowedExtensions.includes('docx') || allowedExtensions.includes('doc')) {
    mimeTypes.push(
      'application/msword',
      'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
    );
  }
  if (allowedExtensions.includes('pptx') || allowedExtensions.includes('ppt')) {
    mimeTypes.push(
      'application/vnd.ms-powerpoint',
      'application/vnd.openxmlformats-officedocument.presentationml.presentation',
    );
  }
  if (allowedExtensions.includes('xml')) {
    mimeTypes.push('application/xml', 'text/xml');
  }
  if (
    allowedExtensions.includes('md') ||
    allowedExtensions.includes('markdown')
  ) {
    mimeTypes.push('text/markdown');
  }
  if (allowedExtensions.includes('epub')) {
    mimeTypes.push('application/epub+zip');
  }
  if (allowedExtensions.includes('eml')) {
    mimeTypes.push('message/rfc822');
  }
  if (allowedExtensions.includes('msg')) {
    mimeTypes.push('application/vnd.ms-outlook');
  }
 
  // 添加文件扩展名
  const extensions = allowedExtensions.map((ext) => `.${ext}`);
 
  return [...mimeTypes, ...extensions].join(',');
}
 
/**
 * 从 URL 中提取文件名
 *
 * @param url 文件 URL
 * @returns 文件名,如果无法提取则返回 'unknown'
 */
export function getFileNameFromUrl(url: null | string | undefined): string {
  // 处理空值
  if (!url) {
    return 'unknown';
  }
 
  try {
    const urlObj = new URL(url);
    const pathname = urlObj.pathname;
    const fileName = pathname.split('/').pop() || 'unknown';
    return decodeURIComponent(fileName);
  } catch {
    // 如果 URL 解析失败,尝试从字符串中提取
    const parts = url.split('/');
    return parts[parts.length - 1] || 'unknown';
  }
}
 
/**
 * 默认图片类型
 */
export const defaultImageAccepts = [
  'bmp',
  'gif',
  'jpeg',
  'jpg',
  'png',
  'svg',
  'webp',
];
 
/**
 * 图片类 MIME 子类型到扩展名的别名映射;未列出的子类型按字面量与扩展名比较
 */
const IMAGE_MIME_SUBTYPE_ALIASES: Record<string, string[]> = {
  apng: ['apng', 'png'],
  jpeg: ['jpeg', 'jpg'],
  pjpeg: ['jpeg', 'jpg'],
  'svg+xml': ['svg'],
  tiff: ['tif', 'tiff'],
  'x-icon': ['ico'],
};
 
/**
 * 判断 MIME 子类型是否与文件扩展名匹配;image/* 限定为已知图片扩展名集合
 */
function matchMimeSubtype(subtype: string, ext: string): boolean {
  if (subtype === '*') {
    return defaultImageAccepts.includes(ext);
  }
  const aliases = IMAGE_MIME_SUBTYPE_ALIASES[subtype];
  if (aliases) {
    return aliases.includes(ext);
  }
  return subtype === ext;
}
 
/**
 * 判断文件是否为图片
 *
 * @param filename 文件名
 * @param accepts 支持的文件类型,兼容 MIME(如 image/png)、.ext(如 .png)与纯后缀(如 png)
 * @returns 是否为图片
 */
export function isImage(
  filename: null | string | undefined,
  accepts: string[] = defaultImageAccepts,
): boolean {
  if (!filename || accepts.length === 0) {
    return false;
  }
  const ext = filename.split('.').pop()?.toLowerCase() || '';
  if (!ext) {
    return false;
  }
  return accepts.some((accept) => {
    const lower = accept.toLowerCase();
    // MIME 类型,例如 image/png ;image/* 仅放行已知图片扩展
    if (lower.includes('/')) {
      const subtype = lower.split('/').pop() || '';
      return matchMimeSubtype(subtype, ext);
    }
    // 以点号开头的扩展名,例如 .png
    if (lower.startsWith('.')) {
      return lower.slice(1) === ext;
    }
    // 纯后缀
    return lower === ext;
  });
}
 
/**
 * 判断文件是否为指定类型
 *
 * @param file 文件
 * @param accepts 支持的文件类型
 * @returns 是否为指定类型
 */
export function checkFileType(file: File, accepts: string[]) {
  if (!accepts || accepts.length === 0) {
    return true;
  }
  const newTypes = accepts.join('|');
  const reg = new RegExp(`${String.raw`\.(` + newTypes})$`, 'i');
  return reg.test(file.name);
}
 
/**
 * 格式化文件大小
 *
 * @param bytes 文件大小(字节)
 * @returns 格式化后的文件大小字符串
 */
export function formatFileSize(bytes: number, digits = 2): string {
  if (bytes === 0) {
    return '0 B';
  }
  const k = 1024;
  const unitArr = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
  const index = Math.floor(Math.log(bytes) / Math.log(k));
  return `${Number.parseFloat((bytes / k ** index).toFixed(digits))} ${unitArr[index]}`;
}
 
/**
 * 获取文件图标(Lucide Icons)
 *
 * @param filename 文件名
 * @returns Lucide 图标名称
 */
export function getFileIcon(filename: null | string | undefined): string {
  if (!filename) {
    return 'lucide:file';
  }
  const ext = filename.split('.').pop()?.toLowerCase() || '';
  if (isImage(ext)) {
    return 'lucide:image';
  }
  if (['pdf'].includes(ext)) {
    return 'lucide:file-text';
  }
  if (['doc', 'docx'].includes(ext)) {
    return 'lucide:file-text';
  }
  if (['xls', 'xlsx'].includes(ext)) {
    return 'lucide:file-spreadsheet';
  }
  if (['ppt', 'pptx'].includes(ext)) {
    return 'lucide:presentation';
  }
  if (['aac', 'm4a', 'mp3', 'wav'].includes(ext)) {
    return 'lucide:music';
  }
  if (['avi', 'mov', 'mp4', 'wmv'].includes(ext)) {
    return 'lucide:video';
  }
  return 'lucide:file';
}
 
/**
 * 获取文件类型样式类(Tailwind CSS 渐变色)
 *
 * @param filename 文件名
 * @returns Tailwind CSS 渐变类名
 */
export function getFileTypeClass(filename: null | string | undefined): string {
  if (!filename) {
    return 'from-gray-500 to-gray-700';
  }
  const ext = filename.split('.').pop()?.toLowerCase() || '';
  if (isImage(ext)) {
    return 'from-yellow-400 to-orange-500';
  }
  if (['pdf'].includes(ext)) {
    return 'from-red-500 to-red-700';
  }
  if (['doc', 'docx'].includes(ext)) {
    return 'from-blue-600 to-blue-800';
  }
  if (['xls', 'xlsx'].includes(ext)) {
    return 'from-green-600 to-green-800';
  }
  if (['ppt', 'pptx'].includes(ext)) {
    return 'from-orange-600 to-orange-800';
  }
  if (['aac', 'm4a', 'mp3', 'wav'].includes(ext)) {
    return 'from-purple-500 to-purple-700';
  }
  if (['avi', 'mov', 'mp4', 'wmv'].includes(ext)) {
    return 'from-red-500 to-red-700';
  }
  return 'from-gray-500 to-gray-700';
}