p2p-explorer-web/src/pages/file/item/fileReader.vue
2025-01-02 11:20:43 +08:00

466 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="file-reader" v-if="file">
<div class="reader-header">
<h3>{{ file.name }}</h3>
<div class="header-actions">
<button
v-if="file.isRemote && loadedFile.type !== ''"
@click="downloadFile"
>
{{ fileTypeCheck == "unsupported-size" ? "远程传输" : "浏览器下载" }}
</button>
<button v-if="canEdit" @click="toggleEdit">
{{ isEditing ? "保存" : "编辑" }}
</button>
<button @click="$emit('close')">关闭</button>
</div>
</div>
<div class="reader-content">
<!-- 图片预览 -->
<div v-if="fileTypeCheck === 'image'" class="image-viewer">
<img :src="fileUrl" :alt="file.name" />
</div>
<!-- 视频预览 -->
<div v-else-if="fileTypeCheck === 'video'" class="video-viewer">
<video controls>
<source :src="fileUrl" :type="mimeType" />
您的浏览器不支持视频播放
</video>
</div>
<!-- 代码预览 -->
<div v-else-if="fileTypeCheck === 'code'" class="code-viewer">
<div class="code-header">
<select v-model="selectedLanguage">
<option value="auto">自动检测</option>
<option value="javascript">JavaScript</option>
<option value="typescript">TypeScript</option>
<option value="html">HTML</option>
<option value="css">CSS</option>
<option value="python">Python</option>
<option value="java">Java</option>
<option value="cpp">C++</option>
<option value="go">Go</option>
<option value="rust">Rust</option>
<option value="lua">Lua</option>
<option value="sql">SQL</option>
<option value="shell">Shell</option>
<option value="json">JSON</option>
<option value="other">其他</option>
</select>
</div>
<div v-if="isEditing" class="editor">
<textarea
v-model="editingContent"
:style="{ height: editorHeight + 'px', minHeight: '100%' }"
@input="adjustEditorHeight"
></textarea>
</div>
<pre v-else><code v-html="highlightedCode"></code></pre>
</div>
<!-- 文本预览 -->
<div
v-else-if="
fileTypeCheck === 'text' || fileTypeCheck === 'unsupported-type'
"
class="text-viewer"
>
<div v-if="isEditing" class="editor">
<textarea
v-model="editingContent"
:style="{ height: editorHeight + 'px' }"
@input="adjustEditorHeight"
></textarea>
</div>
<pre v-else>{{ fileContent }}</pre>
</div>
<!-- 不支持的文件类型 -->
<div v-else class="unsupported">
{{
fileTypeCheck === "unsupported-size"
? "文件过大,无法预览,请传输后查看"
: "不支持预览该类型的文件"
}}
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, computed } from "vue";
import {
fileMgrInstance,
type FileData,
type FileInfo,
} from "../utils/fileMgr";
import hljs from "highlight.js";
import "highlight.js/styles/github.css";
import { Modal } from "ant-design-vue";
import { localCurrentFile, remoteCurrentFile } from "../utils/common";
const props = defineProps<{
file: FileInfo;
}>();
const emit = defineEmits<{
(e: "close"): void;
}>();
const fileUrl = ref("");
const fileContent = ref("");
const mimeType = ref("");
const selectedLanguage = ref("auto");
const isEditing = ref(false);
const editingContent = ref("");
const editorHeight = ref(300);
// 下载文件
const downloadFile = async () => {
if (fileTypeCheck.value == "unsupported-size") {
await props.file.getFile(
false,
localCurrentFile.value.path.replace(
(
await fileMgrInstance.getRootFile()
).path,
""
),
remoteCurrentFile.value.path
);
localCurrentFile.value.loadLocalDirectory();
return;
}
Modal.confirm({
title: "下载文件",
content: `确定要下载${props.file.name}吗?`,
onOk: () => {
const blob = new Blob([loadedFile.value.buffer], {
type: loadedFile.value.type || "application/octet-stream",
});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = props.file.name;
a.click();
URL.revokeObjectURL(url);
},
});
};
// 是否可以编辑
const canEdit = computed(() => {
return fileTypeCheck.value === "text" || fileTypeCheck.value === "code";
});
// 文件类型判断
const fileTypeCheck = computed(() => {
//如果文件大于30M则不预览
if (props.file.size > 30 * 1024 * 1024) {
return "unsupported-size";
}
const ext = props.file.name.split(".").pop()?.toLowerCase() || "";
// 图片类型
if (["jpg", "jpeg", "png", "gif", "webp", "svg"].includes(ext)) {
return "image";
}
// 视频类型
if (["mp4", "webm", "ogg"].includes(ext)) {
return "video";
}
// 代码类型
if (
[
"js",
"ts",
"json",
"html",
"css",
"vue",
"jsx",
"tsx",
"py",
"java",
"cpp",
"c",
"go",
"rs",
"lua",
"sql",
"shell",
].includes(ext)
) {
return "code";
}
// 文本类型
if (["txt", "md", "log"].includes(ext)) {
return "text";
}
return "unsupported-type";
});
// 代码高亮处理
const highlightedCode = computed(() => {
if (!fileContent.value) return "";
try {
if (selectedLanguage.value && selectedLanguage.value !== "auto") {
// 使用指定的语言
const result = hljs.highlight(fileContent.value, {
language: selectedLanguage.value,
});
return result.value;
} else {
// 自动检测语言
const result = hljs.highlightAuto(fileContent.value);
selectedLanguage.value = result.language || "auto";
return result.value;
}
} catch (err) {
console.error("代码高亮失败:", err);
return fileContent.value;
}
});
// 切换编辑模式
const toggleEdit = async () => {
if (isEditing.value) {
// 保存文件
try {
const encoder = new TextEncoder();
const buffer = encoder.encode(editingContent.value);
await props.file.saveFile(buffer);
fileContent.value = editingContent.value;
isEditing.value = false;
} catch (err) {
console.error("保存文件失败:", err);
alert("保存文件失败: " + (err as Error).message);
}
} else {
// 进入编辑模式
editingContent.value = fileContent.value;
isEditing.value = true;
adjustEditorHeight();
}
};
// 自动调整编辑器高度
const adjustEditorHeight = () => {
const textarea = document.querySelector(
".editor textarea"
) as HTMLTextAreaElement;
if (textarea) {
textarea.style.height = "auto";
textarea.style.height = textarea.scrollHeight + "px";
editorHeight.value = textarea.scrollHeight;
}
};
const loadedFile = ref<{
type: string;
buffer: ArrayBuffer;
name: string;
size: number;
lastModified: number;
}>({
type: "",
buffer: undefined,
name: "",
size: 0,
lastModified: 0,
});
// 加载文件内容
const loadFile = async () => {
try {
const file: FileData = (await props.file.getFile(true)) as FileData;
loadedFile.value = file;
if (file.buffer == null) {
file.buffer = props.file.previewCache;
}
if (fileTypeCheck.value === "image" || fileTypeCheck.value === "video") {
fileUrl.value = URL.createObjectURL(new Blob([file.buffer]));
mimeType.value = file.type;
} else if (
fileTypeCheck.value === "code" ||
fileTypeCheck.value === "text"
) {
fileContent.value = new TextDecoder().decode(file.buffer);
} else {
fileContent.value = new TextDecoder().decode(file.buffer);
}
} catch (err) {
console.error("加载文件失败:", err);
}
};
onMounted(() => {
loadFile();
});
// 组件卸载时清理资源
onUnmounted(() => {
if (fileUrl.value) {
URL.revokeObjectURL(fileUrl.value);
props.file.clearPreviewCache();
}
});
</script>
<style scoped>
.file-reader {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.98);
z-index: 1000;
display: flex;
flex-direction: column;
padding: 20px;
}
.reader-header {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 20px;
border-bottom: 1px solid #eee;
}
.reader-content {
flex: 1;
overflow: auto;
padding: 20px 0;
}
.image-viewer {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
.image-viewer img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
.video-viewer {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
.video-viewer video {
max-width: 100%;
max-height: 100%;
}
.code-viewer,
.text-viewer {
height: 100%;
overflow: auto;
}
.code-viewer pre,
.text-viewer pre {
margin: 0;
padding: 20px;
background: #f5f5f5;
border-radius: 4px;
white-space: pre-wrap;
word-wrap: break-word;
}
.unsupported {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
color: #666;
}
button {
padding: 6px 12px;
border: 1px solid #ddd;
border-radius: 4px;
background: white;
cursor: pointer;
}
button:hover {
background: #f5f5f5;
}
.code-header {
padding: 10px;
background: #f8f8f8;
border-bottom: 1px solid #eee;
}
.code-header select {
padding: 4px 8px;
border: 1px solid #ddd;
border-radius: 4px;
background: white;
}
.code-viewer pre {
margin: 0;
padding: 20px;
background: #f8f8f8;
border-radius: 4px;
overflow-x: auto;
}
.code-viewer code {
font-family: "Fira Code", "Consolas", monospace;
font-size: 14px;
line-height: 1.5;
}
/* 取消 scoped 样式对 highlight.js 的影响 */
:deep(.hljs) {
background: transparent;
padding: 0;
}
.header-actions {
display: flex;
gap: 10px;
}
.editor {
height: 100%;
padding: 20px;
background: #f8f8f8;
border-radius: 4px;
}
.editor textarea {
width: 100%;
min-height: 300px;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-family: "Fira Code", "Consolas", monospace;
font-size: 14px;
line-height: 1.5;
resize: none;
background: white;
}
.editor textarea:focus {
outline: none;
border-color: #4a9eff;
}
</style>