-
Notifications
You must be signed in to change notification settings - Fork 39
Expand file tree
/
Copy pathFilePreviewPanel.razor
More file actions
427 lines (384 loc) · 15 KB
/
FilePreviewPanel.razor
File metadata and controls
427 lines (384 loc) · 15 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
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
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
@using System.Text.Json
@inject IJSRuntime JSRuntime
@* 文件预览面板 - 支持代码、图片、HTML、PDF 等多种文件类型 *@
<div class="@ContainerClass">
@if (IsLoading)
{
<div class="flex items-center justify-center h-full">
<div class="text-center">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900 mx-auto mb-2"></div>
<p class="text-sm text-gray-500">正在加载...</p>
</div>
</div>
}
else if (!string.IsNullOrEmpty(ErrorMessage))
{
<div class="flex items-center justify-center h-full">
<div class="text-center text-red-600">
<svg class="w-12 h-12 mx-auto mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<p class="font-semibold">@ErrorMessage</p>
</div>
</div>
}
else if (PreviewType == FilePreviewType.Image)
{
<div class="@(FullHeight ? "h-full" : "") flex items-center justify-center bg-gray-100 p-4">
<img src="@ImageUrl" alt="@FileName" class="max-w-full max-h-full object-contain shadow-lg rounded" />
</div>
}
else if (PreviewType == FilePreviewType.Html)
{
<iframe src="@HtmlUrl"
class="w-full @(FullHeight ? "h-full" : "h-96") border-0"
sandbox="allow-scripts allow-same-origin"
referrerpolicy="no-referrer"></iframe>
}
else if (PreviewType == FilePreviewType.Pdf)
{
<iframe src="@PdfDataUrl"
class="w-full @(FullHeight ? "h-full" : "h-96") border-0"
type="application/pdf"></iframe>
}
else if (PreviewType == FilePreviewType.Office)
{
@if (IsLoadingOffice)
{
<div class="flex items-center justify-center h-full bg-gray-50">
<div class="text-center">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto mb-4"></div>
<p class="text-gray-600 font-medium">正在加载预览...</p>
<p class="text-xs text-gray-400 mt-2">使用 Microsoft Office 在线预览</p>
</div>
</div>
}
else if (!string.IsNullOrEmpty(OfficePreviewError))
{
<div class="flex items-center justify-center h-full bg-gray-50 p-8">
<div class="text-center">
<div class="w-20 h-20 bg-amber-100 rounded-full flex items-center justify-center mb-4 mx-auto">
<svg class="w-10 h-10 text-amber-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>
</svg>
</div>
<h4 class="text-lg font-bold text-gray-800 mb-2">无法预览此文件</h4>
<p class="text-gray-600 text-center max-w-md whitespace-pre-line">@OfficePreviewError</p>
</div>
</div>
}
else
{
<div class="@(FullHeight ? "h-full" : "h-96") bg-gray-100">
<iframe src="@OfficePreviewUrl"
class="w-full h-full border-0"
sandbox="allow-scripts allow-same-origin allow-popups allow-forms"></iframe>
</div>
}
}
else if (PreviewType == FilePreviewType.Code)
{
<div class="@(FullHeight ? "h-full" : MaxHeight ?? "max-h-[calc(100vh-12rem)]") overflow-y-auto overflow-x-hidden bg-gray-900">
<pre class="!m-0 !p-4 !overflow-x-hidden" style="white-space: pre-wrap !important; word-wrap: break-word !important; word-break: break-word !important; overflow-wrap: anywhere !important;"><code id="@CodeElementId" class="language-@Language !text-sm @(ShowLineNumbers ? "line-numbers" : "")" style="white-space: pre-wrap !important; word-wrap: break-word !important; word-break: break-word !important; display: block !important;">@FileContent</code></pre>
</div>
}
else if (PreviewType == FilePreviewType.Text)
{
<div class="@(FullHeight ? "h-full" : MaxHeight ?? "max-h-[calc(100vh-12rem)]") overflow-auto bg-white">
<pre class="p-4 text-sm font-mono text-gray-800 whitespace-pre-wrap break-words overflow-wrap-anywhere">@FileContent</pre>
</div>
}
else
{
<div class="flex items-center justify-center h-full">
<div class="text-center text-gray-400">
<svg class="w-16 h-16 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z"></path>
</svg>
<p class="text-lg">@(string.IsNullOrEmpty(EmptyMessage) ? "选择文件以查看内容" : EmptyMessage)</p>
</div>
</div>
}
</div>
@code {
/// <summary>
/// 文件预览类型
/// </summary>
public enum FilePreviewType
{
None, // 未选择文件
Code, // 代码文件
Text, // 纯文本文件
Image, // 图片文件
Html, // HTML 文件
Pdf, // PDF 文件
Office, // Office 文档(Word/Excel/PowerPoint)
Unknown // 不支持预览
}
/// <summary>
/// 预览类型
/// </summary>
[Parameter] public FilePreviewType PreviewType { get; set; } = FilePreviewType.None;
/// <summary>
/// 文件名
/// </summary>
[Parameter] public string FileName { get; set; } = string.Empty;
/// <summary>
/// 文件内容(文本类型)
/// </summary>
[Parameter] public string FileContent { get; set; } = string.Empty;
/// <summary>
/// 编程语言(用于语法高亮)
/// </summary>
[Parameter] public string Language { get; set; } = "plaintext";
/// <summary>
/// 图片 URL
/// </summary>
[Parameter] public string ImageUrl { get; set; } = string.Empty;
/// <summary>
/// HTML 文件 URL
/// </summary>
[Parameter] public string HtmlUrl { get; set; } = string.Empty;
/// <summary>
/// PDF Data URL
/// </summary>
[Parameter] public string PdfDataUrl { get; set; } = string.Empty;
/// <summary>
/// Office 预览 URL
/// </summary>
[Parameter] public string OfficePreviewUrl { get; set; } = string.Empty;
/// <summary>
/// Office 预览错误信息
/// </summary>
[Parameter] public string OfficePreviewError { get; set; } = string.Empty;
/// <summary>
/// 是否正在加载 Office 预览
/// </summary>
[Parameter] public bool IsLoadingOffice { get; set; } = false;
/// <summary>
/// 是否显示行号
/// </summary>
[Parameter] public bool ShowLineNumbers { get; set; } = false;
/// <summary>
/// 代码元素 ID(用于语法高亮)
/// </summary>
[Parameter] public string CodeElementId { get; set; } = "file-preview-code";
/// <summary>
/// 是否正在加载
/// </summary>
[Parameter] public bool IsLoading { get; set; } = false;
/// <summary>
/// 错误消息
/// </summary>
[Parameter] public string ErrorMessage { get; set; } = string.Empty;
/// <summary>
/// 空状态消息
/// </summary>
[Parameter] public string EmptyMessage { get; set; } = string.Empty;
/// <summary>
/// 容器样式类
/// </summary>
[Parameter] public string ContainerClass { get; set; } = "w-full h-full";
/// <summary>
/// 最大高度
/// </summary>
[Parameter] public string? MaxHeight { get; set; }
/// <summary>
/// 是否使用全高度
/// </summary>
[Parameter] public bool FullHeight { get; set; } = false;
/// <summary>
/// 是否自动应用语法高亮
/// </summary>
[Parameter] public bool AutoApplyHighlight { get; set; } = true;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
// 如果是代码文件且开启了自动高亮,应用语法高亮
if (PreviewType == FilePreviewType.Code && AutoApplyHighlight && !string.IsNullOrEmpty(FileContent))
{
await Task.Delay(50);
try
{
await JSRuntime.InvokeVoidAsync("applyCodeHighlight");
}
catch (Exception ex)
{
Console.WriteLine($"应用语法高亮失败: {ex.Message}");
}
}
}
/// <summary>
/// 根据文件扩展名确定预览类型
/// </summary>
public static FilePreviewType GetPreviewTypeFromExtension(string extension)
{
extension = extension?.ToLower() ?? "";
// 图片文件
if (new[] { ".png", ".jpg", ".jpeg", ".gif", ".svg", ".ico", ".webp", ".bmp" }.Contains(extension))
{
return FilePreviewType.Image;
}
// HTML 文件
if (new[] { ".html", ".htm" }.Contains(extension))
{
return FilePreviewType.Html;
}
// PDF 文件
if (extension == ".pdf")
{
return FilePreviewType.Pdf;
}
// Office 文档
if (IsOfficeDocument(extension))
{
return FilePreviewType.Office;
}
// 代码文件
var language = GetLanguageFromExtension(extension);
if (language != "plaintext")
{
return FilePreviewType.Code;
}
// 纯文本文件
if (new[] { ".txt", ".log", ".md", ".gitignore", ".env" }.Contains(extension))
{
return FilePreviewType.Text;
}
return FilePreviewType.Unknown;
}
/// <summary>
/// 根据文件扩展名获取编程语言
/// </summary>
public static string GetLanguageFromExtension(string extension)
{
return extension?.ToLower() switch
{
".html" or ".htm" => "html",
".css" => "css",
".js" => "javascript",
".ts" => "typescript",
".jsx" => "jsx",
".tsx" => "tsx",
".json" => "json",
".xml" => "xml",
".cs" => "csharp",
".java" => "java",
".py" => "python",
".rb" => "ruby",
".php" => "php",
".go" => "go",
".rs" => "rust",
".sql" => "sql",
".sh" or ".bash" => "bash",
".ps1" => "powershell",
".yaml" or ".yml" => "yaml",
".md" or ".markdown" => "markdown",
".vue" => "markup",
".razor" => "cshtml",
".cpp" or ".cc" or ".cxx" => "cpp",
".c" => "c",
".h" or ".hpp" => "cpp",
".swift" => "swift",
".kt" or ".kts" => "kotlin",
".dart" => "dart",
".r" => "r",
".scala" => "scala",
".dockerfile" => "docker",
".csproj" or ".sln" or ".props" or ".targets" => "xml",
".config" or ".ini" => "ini",
".toml" => "toml",
".gradle" => "gradle",
".properties" => "properties",
_ => "plaintext"
};
}
/// <summary>
/// 格式化 JSON 内容
/// </summary>
public static string FormatJsonContent(string jsonContent)
{
if (string.IsNullOrEmpty(jsonContent)) return jsonContent;
try
{
using var jsonDoc = JsonDocument.Parse(jsonContent);
return JsonSerializer.Serialize(jsonDoc, new JsonSerializerOptions
{
WriteIndented = true,
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
});
}
catch
{
// 如果解析失败,返回原内容
return jsonContent;
}
}
/// <summary>
/// 判断是否为 Office 文档
/// </summary>
public static bool IsOfficeDocument(string extension)
{
var officeExtensions = new[]
{
// Word
".doc", ".docx", ".docm", ".dotx", ".dotm", ".odt",
// Excel
".xls", ".xlsx", ".xlsm", ".xlsb", ".xltx", ".xltm", ".csv", ".ods",
// PowerPoint
".ppt", ".pptx", ".pptm", ".potx", ".potm", ".ppsx", ".ppsm", ".odp"
};
return officeExtensions.Contains(extension?.ToLower() ?? "");
}
/// <summary>
/// 获取 Office 文档类型名称
/// </summary>
public static string GetOfficeTypeName(string extension)
{
return extension?.ToLower() switch
{
".doc" or ".docx" => "Word 文档",
".docm" => "Word 宏文档",
".dotx" or ".dotm" => "Word 模板",
".odt" => "OpenDocument 文本",
".xls" or ".xlsx" => "Excel 电子表格",
".xlsm" => "Excel 宏工作簿",
".xlsb" => "Excel 二进制工作簿",
".xltx" or ".xltm" => "Excel 模板",
".csv" => "CSV 数据文件",
".ods" => "OpenDocument 电子表格",
".ppt" or ".pptx" => "PowerPoint 演示文稿",
".pptm" => "PowerPoint 宏演示文稿",
".potx" or ".potm" => "PowerPoint 模板",
".ppsx" or ".ppsm" => "PowerPoint 放映",
".odp" => "OpenDocument 演示文稿",
_ => "Office 文档"
};
}
/// <summary>
/// 生成 Office 在线预览 URL
/// </summary>
/// <param name="fileUrl">文件的公开访问 URL</param>
/// <param name="baseUrl">应用基础 URL(用于检测是否为本地环境)</param>
/// <returns>预览 URL 或错误信息</returns>
public static (string previewUrl, string errorMessage) GenerateOfficePreviewUrl(string fileUrl, string baseUrl)
{
try
{
// 检查是否为 localhost/本地环境
var uri = new Uri(baseUrl);
var isLocalhost = uri.Host == "localhost" || uri.Host == "127.0.0.1" || uri.Host.StartsWith("192.168.");
if (isLocalhost)
{
return ("", "Office在线预览需要公网可访问的URL。\n\n当前为本地开发环境,请下载文件后使用本地Office应用打开。\n\n如需在线预览,请将应用部署到公网服务器。");
}
// 构建 Office Online 预览 URL
var encodedFileUrl = Uri.EscapeDataString(fileUrl);
var previewUrl = $"https://view.officeapps.live.com/op/view.aspx?src={encodedFileUrl}";
return (previewUrl, "");
}
catch (Exception ex)
{
return ("", $"初始化预览失败: {ex.Message}");
}
}
}