support preview img,audio,video for FileDetail.vue
This commit is contained in:
362
src/components/FileViewer.vue
Normal file
362
src/components/FileViewer.vue
Normal file
@ -0,0 +1,362 @@
|
||||
<template>
|
||||
<div class="file-viewer diselect">
|
||||
<t-tabs
|
||||
class="tabs"
|
||||
theme="card"
|
||||
v-model="filePath"
|
||||
:onRemove="onTabRemove"
|
||||
>
|
||||
<t-tab-panel
|
||||
v-for="file in files"
|
||||
:key="file.path"
|
||||
:value="file.path"
|
||||
:label="file.name"
|
||||
:removable="file.removable"
|
||||
@click="file.isPreview = false"
|
||||
>
|
||||
<template #label>
|
||||
<span
|
||||
v-text="file.name"
|
||||
@click="file.isPreview = false"
|
||||
></span>
|
||||
</template>
|
||||
</t-tab-panel>
|
||||
</t-tabs>
|
||||
<div v-if="file" :class="`viewer ${clazz}`" @click="file.isPreview = false">
|
||||
<loading class="loading" :showOn="isLoading" />
|
||||
<div
|
||||
v-if="file.viewerType === FileViewerType.NOT_SUPPORT"
|
||||
class="not-support"
|
||||
>
|
||||
<p class="tips">此文件不支持预览</p>
|
||||
<t-button
|
||||
v-if="file.rawFile.size < IOSize.MB"
|
||||
@click="previewAsText"
|
||||
>尝试以文本文件解析预览</t-button>
|
||||
</div>
|
||||
<markdown-view
|
||||
v-if="file.viewerType === FileViewerType.MARKDOWN || file.viewerType === FileViewerType.CODE"
|
||||
class="content selectable"
|
||||
:show-code-border="false"
|
||||
max-height="auto"
|
||||
:content="file.data"
|
||||
/>
|
||||
<div
|
||||
v-if="file.viewerType === FileViewerType.TEXT"
|
||||
class="container selectable"
|
||||
>
|
||||
<div class="value" v-text="file.data"></div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<component
|
||||
:is="mediaComponent"
|
||||
class="value"
|
||||
v-if="mediaComponent && file.rawURL"
|
||||
:src="file.rawURL"
|
||||
:controls="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { IOSize, Loading, MarkdownView, Prismjs, PrismjsViewer, Toolkit } from "timi-web";
|
||||
import { File, FileType, FileViewerType, OpenFile } from "@/types/Repository.ts";
|
||||
import RepositoryAPI from "@/api/RepositoryAPI.ts";
|
||||
|
||||
defineOptions({
|
||||
name: "FileViewer"
|
||||
});
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
repoName: string;
|
||||
branch: string;
|
||||
}>(), {
|
||||
});
|
||||
const { repoName, branch } = toRefs(props);
|
||||
|
||||
const isLoading = ref<boolean>(false);
|
||||
const files = ref<OpenFile[]>([]);
|
||||
const file = ref<OpenFile>();
|
||||
const filePath = ref<string>();
|
||||
watch(file, file => filePath.value = file?.path);
|
||||
|
||||
const clazz = computed(() => {
|
||||
if (file.value && file.value.viewerType) {
|
||||
return Toolkit.className(file.value.viewerType.toString().toLowerCase());
|
||||
}
|
||||
return "";
|
||||
});
|
||||
|
||||
// ---------- 切换分支 ----------
|
||||
|
||||
watch(branch, () => {
|
||||
files.value = [];
|
||||
file.value = undefined;
|
||||
});
|
||||
|
||||
// ---------- 打开文件 ----------
|
||||
|
||||
function openFile(treeFile: File) {
|
||||
if (treeFile.type === FileType.FILE) {
|
||||
// 已打开该文件
|
||||
let opened = files.value.find(item => item.path === treeFile.path);
|
||||
const isReadme = treeFile.path === "README.md";
|
||||
if (!opened) {
|
||||
opened = {
|
||||
path: treeFile.path,
|
||||
name: treeFile.name,
|
||||
rawURL: RepositoryAPI.fileRawURL(repoName.value, branch.value, `/${treeFile.path}`),
|
||||
rawFile: treeFile,
|
||||
isPreview: !isReadme,
|
||||
removable: !isReadme
|
||||
} as OpenFile;
|
||||
// 如果存在已打开预览,则替换该预览项
|
||||
const previewIndex = files.value.findIndex(item => item.isPreview);
|
||||
if (previewIndex === -1) {
|
||||
files.value.push(opened);
|
||||
} else {
|
||||
files.value.splice(previewIndex, 1, opened);
|
||||
}
|
||||
}
|
||||
opened.activatedAt = new Date().getTime();
|
||||
file.value = opened;
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
openFile
|
||||
});
|
||||
|
||||
// ---------- 切换文件 ----------
|
||||
|
||||
// 从 tab 切换文件
|
||||
watch(filePath, value => {
|
||||
if (filePath.value) {
|
||||
const targetFile = files.value.find(item => item.path === value);
|
||||
if (targetFile) {
|
||||
targetFile.activatedAt = new Date().getTime();
|
||||
file.value = targetFile;
|
||||
return;
|
||||
}
|
||||
}
|
||||
file.value = undefined;
|
||||
});
|
||||
|
||||
watch(file, async () => {
|
||||
if (file.value) {
|
||||
if (!file.value.mimeType) {
|
||||
isLoading.value = true;
|
||||
// 获取 Mime-Type
|
||||
file.value.mimeType = await RepositoryAPI.fileMimeType(repoName.value, branch.value, `/${file.value.path}`);
|
||||
}
|
||||
file.value.prismjsType = file.value.prismjsType ?? Prismjs.typeFromFileName(file.value.name);
|
||||
if (file.value.prismjsType) {
|
||||
if (!file.value.raw) {
|
||||
isLoading.value = true;
|
||||
file.value.raw = await RepositoryAPI.fileRawByURL(file.value.rawURL);
|
||||
}
|
||||
// 解码为文本
|
||||
const properties = Prismjs.getFileProperties(file.value.prismjsType);
|
||||
if (properties) {
|
||||
if (!file.value.data) {
|
||||
isLoading.value = true;
|
||||
const data = new TextDecoder("utf-8").decode(file.value.raw);
|
||||
if (properties.viewer === PrismjsViewer.CODE) {
|
||||
file.value.data = Toolkit.format("```${type}\n${data}\n```", {
|
||||
type: properties.prismjs,
|
||||
data
|
||||
});
|
||||
} else {
|
||||
file.value.data = data;
|
||||
}
|
||||
}
|
||||
if (properties.viewer === PrismjsViewer.MARKDOWN) {
|
||||
file.value.viewerType = FileViewerType.MARKDOWN;
|
||||
} else if (properties.viewer === PrismjsViewer.CODE) {
|
||||
file.value.viewerType = FileViewerType.CODE;
|
||||
} else {
|
||||
file.value.viewerType = FileViewerType.TEXT;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!file.value.viewerType) {
|
||||
if (file.value.mimeType) {
|
||||
switch (true) {
|
||||
case file.value.mimeType.startsWith("image/"):
|
||||
file.value.viewerType = FileViewerType.IMAGE;
|
||||
break;
|
||||
case file.value.mimeType.startsWith("audio/"):
|
||||
file.value.viewerType = FileViewerType.AUDIO;
|
||||
break;
|
||||
case file.value.mimeType.startsWith("video/"):
|
||||
file.value.viewerType = FileViewerType.VIDEO;
|
||||
break;
|
||||
case file.value.mimeType.startsWith("text/"):
|
||||
file.value.viewerType = FileViewerType.TEXT;
|
||||
if (!file.value.data) {
|
||||
if (!file.value.raw) {
|
||||
isLoading.value = true;
|
||||
file.value.raw = await RepositoryAPI.fileRawByURL(file.value.rawURL);
|
||||
}
|
||||
file.value.data = new TextDecoder("utf-8").decode(file.value.raw);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
file.value.viewerType = FileViewerType.NOT_SUPPORT;
|
||||
}
|
||||
} else {
|
||||
file.value.viewerType = FileViewerType.NOT_SUPPORT;
|
||||
}
|
||||
}
|
||||
}
|
||||
isLoading.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
// ---------- 关闭文件 ----------
|
||||
|
||||
const onTabRemove = (options: { index: number }) => {
|
||||
files.value.splice(options.index, 1);
|
||||
if (0 < files.value.length) {
|
||||
file.value = files.value[files.value.length - 1];
|
||||
} else {
|
||||
file.value = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
// ---------- 尝试以文本文件解码预览 ----------
|
||||
|
||||
async function previewAsText() {
|
||||
if (file.value) {
|
||||
// 解码为文本
|
||||
if (!file.value.data) {
|
||||
if (!file.value.raw) {
|
||||
file.value.raw = await RepositoryAPI.fileRawByURL(file.value.rawURL);
|
||||
}
|
||||
file.value.data = new TextDecoder("utf-8").decode(file.value.raw);
|
||||
}
|
||||
file.value.viewerType = FileViewerType.TEXT;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 媒体文件 ----------
|
||||
|
||||
const mediaComponent = computed(() => {
|
||||
if (file.value) {
|
||||
switch (file.value.viewerType) {
|
||||
case FileViewerType.IMAGE:
|
||||
return "img";
|
||||
case FileViewerType.VIDEO:
|
||||
return FileViewerType.VIDEO.toString().toLowerCase();
|
||||
case FileViewerType.AUDIO:
|
||||
return FileViewerType.AUDIO.toString().toLowerCase();
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.file-viewer {
|
||||
height: 100%;
|
||||
|
||||
.tabs {
|
||||
top: calc(50px + 49px + 30px);
|
||||
position: sticky;
|
||||
z-index: 9;
|
||||
border-top: var(--tui-border);
|
||||
border-bottom: var(--tui-border);
|
||||
}
|
||||
|
||||
.viewer {
|
||||
height: calc(100% - 33px);
|
||||
|
||||
&.text {
|
||||
|
||||
.container {
|
||||
width: calc(100% - 8px);
|
||||
min-height: calc(100% - 4px);
|
||||
padding: 2px 4px;
|
||||
overflow: auto;
|
||||
|
||||
.value {
|
||||
font-size: 14px;
|
||||
white-space: pre;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.code {
|
||||
|
||||
:deep(.tui-markdown-view) {
|
||||
height: 100%;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 3em;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
background: rgba(242, 242, 242, .9);
|
||||
border-right: 1px solid #999;
|
||||
}
|
||||
|
||||
pre[class^="language-"] {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(pre[class*="language-"]) {
|
||||
margin: 0;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.markdown {
|
||||
height: 100%;
|
||||
padding: .5rem 1rem;
|
||||
}
|
||||
|
||||
&.image,
|
||||
&.audio,
|
||||
&.video {
|
||||
|
||||
.container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
audio {
|
||||
width: 40rem;
|
||||
margin: 0 2rem;
|
||||
}
|
||||
|
||||
video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: 60vh;
|
||||
object-fit: contain;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.value {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.loading {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.not-support {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user