support preview img,audio,video for FileDetail.vue
This commit is contained in:
@ -21,14 +21,26 @@ async function listFile(name: string, branch: string, path: string): Promise<Fil
|
|||||||
return axios.get(`${BASE_URI}/${name}:${branch}/file/list${path}`);
|
return axios.get(`${BASE_URI}/${name}:${branch}/file/list${path}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fileRaw(name: string, branch: string, path: string): Promise<ArrayBuffer> {
|
function fileRawURL(name: string, branch: string, path: string): string {
|
||||||
|
return `${API_HOST}${BASE_URI}/${name}:${branch}/file/raw${path}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fileRawByURL(rawURL: string): Promise<ArrayBuffer> {
|
||||||
return axios({
|
return axios({
|
||||||
url: `${BASE_URI}/${name}:${branch}/file/raw${path}`,
|
url: rawURL,
|
||||||
method: "GET",
|
method: "GET",
|
||||||
responseType: "arraybuffer"
|
responseType: "arraybuffer"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fileRaw(name: string, branch: string, path: string): Promise<ArrayBuffer> {
|
||||||
|
return fileRawByURL(fileRawURL(name, branch, path));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fileMimeType(name: string, branch: string, path: string): Promise<string> {
|
||||||
|
return axios.get(`${BASE_URI}/${name}:${branch}/file/mime${path}`);
|
||||||
|
}
|
||||||
|
|
||||||
function downloadArchive(name: string, branch: string) {
|
function downloadArchive(name: string, branch: string) {
|
||||||
window.open(`${API_HOST}${BASE_URI}/${name}:${branch}/archive`);
|
window.open(`${API_HOST}${BASE_URI}/${name}:${branch}/archive`);
|
||||||
}
|
}
|
||||||
@ -43,6 +55,9 @@ export default {
|
|||||||
pagePush,
|
pagePush,
|
||||||
listFile,
|
listFile,
|
||||||
fileRaw,
|
fileRaw,
|
||||||
|
fileRawURL,
|
||||||
|
fileRawByURL,
|
||||||
|
fileMimeType,
|
||||||
downloadArchive,
|
downloadArchive,
|
||||||
pageLog
|
pageLog
|
||||||
};
|
};
|
||||||
|
|||||||
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>
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { Model, Page, UserView } from "timi-web";
|
import { Model, Page, PrismjsType, UserView } from "timi-web";
|
||||||
|
|
||||||
export type Repository = {
|
export type Repository = {
|
||||||
id: number;
|
id: number;
|
||||||
@ -61,3 +61,30 @@ export type File = {
|
|||||||
|
|
||||||
children: boolean;
|
children: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum FileViewerType {
|
||||||
|
|
||||||
|
NOT_SUPPORT = "NOT_SUPPORT",
|
||||||
|
AUDIO = "AUDIO",
|
||||||
|
VIDEO = "VIDEO",
|
||||||
|
IMAGE = "IMAGE",
|
||||||
|
TEXT = "TEXT",
|
||||||
|
CODE = "CODE",
|
||||||
|
MARKDOWN = "MARKDOWN",
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OpenFile = {
|
||||||
|
|
||||||
|
path: string;
|
||||||
|
name: string;
|
||||||
|
raw?: ArrayBuffer;
|
||||||
|
rawURL: string;
|
||||||
|
rawFile: File;
|
||||||
|
data?: string;
|
||||||
|
removable: boolean;
|
||||||
|
isPreview: boolean;
|
||||||
|
activatedAt?: number;
|
||||||
|
mimeType?: string;
|
||||||
|
viewerType?: FileViewerType;
|
||||||
|
prismjsType?: PrismjsType;
|
||||||
|
}
|
||||||
|
|||||||
@ -59,61 +59,24 @@
|
|||||||
<t-button theme="success" @click="doCopyCloneAddr">复制</t-button>
|
<t-button theme="success" @click="doCopyCloneAddr">复制</t-button>
|
||||||
<t-button @click="RepositoryAPI.downloadArchive(repositoryStore.value.name, branch)">下载源码</t-button>
|
<t-button @click="RepositoryAPI.downloadArchive(repositoryStore.value.name, branch)">下载源码</t-button>
|
||||||
</div>
|
</div>
|
||||||
<t-tabs
|
<file-viewer
|
||||||
class="tabs"
|
v-if="branch"
|
||||||
theme="card"
|
ref="fileViewerRef"
|
||||||
v-model="tabActivated"
|
:repo-name="repositoryStore.value!.name"
|
||||||
:onRemove="onTabRemove"
|
:branch="branch"
|
||||||
>
|
|
||||||
<t-tab-panel
|
|
||||||
v-for="file in tabFiles"
|
|
||||||
:key="file.path"
|
|
||||||
:value="file.path"
|
|
||||||
:label="file.name"
|
|
||||||
:removable="file.removable"
|
|
||||||
@click="onClickOpenFile(file)"
|
|
||||||
>
|
|
||||||
<template #label>
|
|
||||||
<span
|
|
||||||
v-text="file.name"
|
|
||||||
@click="onClickOpenFile(file)"
|
|
||||||
></span>
|
|
||||||
</template>
|
|
||||||
</t-tab-panel>
|
|
||||||
</t-tabs>
|
|
||||||
<div v-if="tabActivatedFileViewer"
|
|
||||||
:class="`viewer ${viewerClass}`"
|
|
||||||
@click="onClickViewer"
|
|
||||||
>
|
|
||||||
<markdown-view
|
|
||||||
v-if="tabActivatedFile && tabActivatedFileViewer !== PrismjsViewer.TEXT"
|
|
||||||
class="content selectable"
|
|
||||||
:show-code-border="false"
|
|
||||||
max-height="auto"
|
|
||||||
:content="tabActivatedFile.data"
|
|
||||||
@click="onClickViewer"
|
|
||||||
/>
|
/>
|
||||||
<textarea
|
|
||||||
v-if="tabActivatedFile && tabActivatedFileViewer === PrismjsViewer.TEXT"
|
|
||||||
class="content selectable"
|
|
||||||
:value="tabActivatedFile.data"
|
|
||||||
readonly
|
|
||||||
></textarea>
|
|
||||||
</div>
|
|
||||||
<div v-else class="viewer not-support">
|
|
||||||
<div class="content">不支持预览</div>
|
|
||||||
</div>
|
|
||||||
</pane>
|
</pane>
|
||||||
</split-pane>
|
</split-pane>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { Icon, MarkdownView, Prismjs, PrismjsType, PrismjsViewer, Toolkit } from "timi-web";
|
import { Icon } from "timi-web";
|
||||||
import type { TreeInstanceFunctions, TreeProps } from "tdesign-vue-next";
|
import type { TreeInstanceFunctions, TreeProps } from "tdesign-vue-next";
|
||||||
import RepositoryAPI from "@/api/RepositoryAPI";
|
import RepositoryAPI from "@/api/RepositoryAPI";
|
||||||
import { File, FileType } from "@/types/Repository";
|
import { File, FileType } from "@/types/Repository";
|
||||||
import { useRepositoryStore } from "@/store/repository.ts";
|
import { useRepositoryStore } from "@/store/repository.ts";
|
||||||
import useClipboard from "vue-clipboard3";
|
import useClipboard from "vue-clipboard3";
|
||||||
import SplitPane, { Pane } from "@marsio/vue-split-pane";
|
import SplitPane, { Pane } from "@marsio/vue-split-pane";
|
||||||
|
import FileViewer from "@/components/FileViewer.vue";
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { toClipboard } = useClipboard();
|
const { toClipboard } = useClipboard();
|
||||||
@ -121,6 +84,14 @@ const repositoryStore = useRepositoryStore();
|
|||||||
|
|
||||||
// ---------- 分支 ----------
|
// ---------- 分支 ----------
|
||||||
const branch = ref();
|
const branch = ref();
|
||||||
|
|
||||||
|
watch(branch, async () => {
|
||||||
|
if (!branch.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await router.replace(`/${repositoryStore.value!.name}/${branch.value}`);
|
||||||
|
});
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (repositoryStore.value && repositoryStore.value.defaultBranch) {
|
if (repositoryStore.value && repositoryStore.value.defaultBranch) {
|
||||||
branch.value = repositoryStore.value.defaultBranch;
|
branch.value = repositoryStore.value.defaultBranch;
|
||||||
@ -147,7 +118,7 @@ watch(branch, async () => {
|
|||||||
const items = treeItems.value;
|
const items = treeItems.value;
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
if (item.path === "README.md") {
|
if (item.path === "README.md") {
|
||||||
await doOpenFile(tree.value.getItem(item.path).data as File);
|
doOpenFile(tree.value.getItem(item.path).data as File);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -193,119 +164,13 @@ async function doCopyCloneAddr() {
|
|||||||
|
|
||||||
// ---------- 打开文件 ----------
|
// ---------- 打开文件 ----------
|
||||||
|
|
||||||
type OpenFile = {
|
const fileViewerRef = ref<InstanceType<typeof FileViewer>>();
|
||||||
path: string;
|
function doOpenFile(file: File) {
|
||||||
name: string;
|
fileViewerRef.value!.openFile(file);
|
||||||
data?: string;
|
|
||||||
removable: boolean;
|
|
||||||
isPreview: boolean;
|
|
||||||
activatedAt?: number;
|
|
||||||
prismjsType?: PrismjsType;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const tabFiles = ref<OpenFile[]>([]);
|
|
||||||
const tabActivated = ref();
|
|
||||||
const tabActivatedFile = ref<OpenFile>();
|
|
||||||
const tabActivatedFileViewer = ref<PrismjsViewer>();
|
|
||||||
|
|
||||||
const viewerClass = computed(() => {
|
|
||||||
if (tabActivatedFileViewer.value) {
|
|
||||||
return Toolkit.className(tabActivatedFileViewer.value.toString().toLowerCase());
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
});
|
|
||||||
watch(branch, async () => {
|
|
||||||
if (!branch.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await router.replace(`/${repositoryStore.value!.name}/${branch.value}`);
|
|
||||||
tabFiles.value = [];
|
|
||||||
tabActivated.value = null;
|
|
||||||
});
|
|
||||||
watch(tabActivated, value => {
|
|
||||||
if (tabActivated.value) {
|
|
||||||
const targetFile = tabFiles.value.find(item => item.path === value);
|
|
||||||
if (targetFile) {
|
|
||||||
targetFile.activatedAt = new Date().getTime();
|
|
||||||
tabActivatedFile.value = targetFile;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tabActivatedFile.value = undefined;
|
|
||||||
});
|
|
||||||
watch(tabActivatedFile, () => {
|
|
||||||
if (tabActivatedFile.value && tabActivatedFile.value.prismjsType) {
|
|
||||||
const properties = Prismjs.getFileProperties(tabActivatedFile.value.prismjsType);
|
|
||||||
if (properties) {
|
|
||||||
tabActivatedFileViewer.value = properties.viewer;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tabActivatedFileViewer.value = undefined;
|
|
||||||
});
|
|
||||||
const doOpenFile = async (file: File) => {
|
|
||||||
if (file.type === FileType.FILE) {
|
|
||||||
let opened = tabFiles.value.find(item => item.path === file.path);
|
|
||||||
const previewIndex = tabFiles.value.findIndex(item => item.isPreview);
|
|
||||||
const isReadme = file.path === "README.md";
|
|
||||||
if (!opened) {
|
|
||||||
opened = {
|
|
||||||
path: file.path,
|
|
||||||
name: file.name,
|
|
||||||
isPreview: !isReadme,
|
|
||||||
removable: !isReadme,
|
|
||||||
prismjsType: PrismjsType.PlainText
|
|
||||||
};
|
|
||||||
if (previewIndex === -1) {
|
|
||||||
tabFiles.value.push(opened);
|
|
||||||
} else {
|
|
||||||
// 如果存在已打开预览,则替换该预览项
|
|
||||||
tabFiles.value.splice(previewIndex, 1, opened);
|
|
||||||
}
|
|
||||||
const raw = await RepositoryAPI.fileRaw(repositoryStore.value!.name, branch.value, `/${opened.path}`);
|
|
||||||
const decoder = new TextDecoder("utf-8");
|
|
||||||
opened.prismjsType = Prismjs.typeFromFileName(opened.name);
|
|
||||||
if (opened.prismjsType) {
|
|
||||||
const properties = Prismjs.getFileProperties(opened.prismjsType);
|
|
||||||
if (properties && properties.viewer === PrismjsViewer.CODE) {
|
|
||||||
opened.data = "```" + properties.prismjs + "\n" + decoder.decode(raw) + "\n```";
|
|
||||||
} else {
|
|
||||||
opened.data = decoder.decode(raw);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
opened.data = "该文件暂不支持预览";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
opened.activatedAt = new Date().getTime();
|
|
||||||
tabActivated.value = file.path;
|
|
||||||
tabActivatedFile.value = opened;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO 明确类型会构建失败
|
|
||||||
const onTreeActivated = (_value: any, context: { node: any }) => {
|
const onTreeActivated = (_value: any, context: { node: any }) => {
|
||||||
doOpenFile(context.node.data);
|
doOpenFile(context.node.data);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onTabRemove = (options: { index: number }) => {
|
|
||||||
tabFiles.value.splice(options.index, 1);
|
|
||||||
if (0 < tabFiles.value.length) {
|
|
||||||
tabActivated.value = tabFiles.value[tabFiles.value.length - 1].path;
|
|
||||||
} else {
|
|
||||||
tabActivated.value = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onClickOpenFile = (openFile: OpenFile) => {
|
|
||||||
openFile.isPreview = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const onClickViewer = async () => {
|
|
||||||
const activated = tabFiles.value.find(item => item.path === tabActivated.value);
|
|
||||||
if (activated) {
|
|
||||||
activated.isPreview = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
@ -412,72 +277,6 @@ const onClickViewer = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabs {
|
|
||||||
top: calc(50px + 49px + 30px);
|
|
||||||
position: sticky;
|
|
||||||
z-index: 9;
|
|
||||||
border-top: var(--tui-border);
|
|
||||||
border-bottom: var(--tui-border);
|
|
||||||
|
|
||||||
:deep(.t-tabs__nav-container.t-tabs__nav--card::after) {
|
|
||||||
background: var(--tui-light-gray);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.viewer {
|
|
||||||
height: 100%;
|
|
||||||
overflow: auto;
|
|
||||||
|
|
||||||
&.text {
|
|
||||||
|
|
||||||
.content {
|
|
||||||
width: calc(100% - 8px);
|
|
||||||
height: calc(100% - 4px);
|
|
||||||
border: none;
|
|
||||||
padding: 2px 4px;
|
|
||||||
font-size: 14px;
|
|
||||||
word-wrap: normal;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.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%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.markdown {
|
|
||||||
height: 100%;
|
|
||||||
padding: .5rem 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.not-support {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(pre[class*="language-"]) {
|
|
||||||
margin: 0;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user