mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-01-19 08:19:15 +08:00
feat: 文件管理支持预览常见文件格式 (#5629)
#### What this PR does / why we need it? Refs #5431 #### Summary of your change 支持预览 pdf/word/excel/image/video/audio #### Please indicate you've done the following: - [ ] Made sure tests are passing and test coverage is added if needed. - [ ] Made sure commit message follow the rule of [Conventional Commits specification](https://www.conventionalcommits.org/). - [ ] Considered the docs impact and opened a new docs issue or PR with docs changes if needed.
This commit is contained in:
parent
6fd9dc53e2
commit
b5b710f8b2
@ -28,6 +28,9 @@
|
|||||||
"@codemirror/theme-one-dark": "^6.1.2",
|
"@codemirror/theme-one-dark": "^6.1.2",
|
||||||
"@element-plus/icons-vue": "^1.1.4",
|
"@element-plus/icons-vue": "^1.1.4",
|
||||||
"@highlightjs/vue-plugin": "^2.1.0",
|
"@highlightjs/vue-plugin": "^2.1.0",
|
||||||
|
"@vue-office/docx": "^1.6.2",
|
||||||
|
"@vue-office/excel": "^1.7.8",
|
||||||
|
"@vue-office/pdf": "^2.0.2",
|
||||||
"@vueuse/core": "^8.9.4",
|
"@vueuse/core": "^8.9.4",
|
||||||
"@xterm/addon-fit": "^0.10.0",
|
"@xterm/addon-fit": "^0.10.0",
|
||||||
"@xterm/xterm": "^5.5.0",
|
"@xterm/xterm": "^5.5.0",
|
||||||
@ -49,6 +52,7 @@
|
|||||||
"vue": "^3.4.27",
|
"vue": "^3.4.27",
|
||||||
"vue-clipboard3": "^2.0.0",
|
"vue-clipboard3": "^2.0.0",
|
||||||
"vue-codemirror": "^6.1.1",
|
"vue-codemirror": "^6.1.1",
|
||||||
|
"vue-demi": "^0.14.6",
|
||||||
"vue-i18n": "^9.13.1",
|
"vue-i18n": "^9.13.1",
|
||||||
"vue-router": "^4.3.3"
|
"vue-router": "^4.3.3"
|
||||||
},
|
},
|
||||||
|
@ -63,6 +63,7 @@ const message = {
|
|||||||
hideSome: 'Hide Some',
|
hideSome: 'Hide Some',
|
||||||
agree: 'Agree',
|
agree: 'Agree',
|
||||||
notAgree: 'Not Agree',
|
notAgree: 'Not Agree',
|
||||||
|
preview: 'Preview',
|
||||||
},
|
},
|
||||||
search: {
|
search: {
|
||||||
timeStart: 'Time start',
|
timeStart: 'Time start',
|
||||||
|
@ -62,6 +62,7 @@ const message = {
|
|||||||
hideSome: '隱藏部分',
|
hideSome: '隱藏部分',
|
||||||
agree: '同意',
|
agree: '同意',
|
||||||
notAgree: '不同意',
|
notAgree: '不同意',
|
||||||
|
preview: '預覽',
|
||||||
},
|
},
|
||||||
search: {
|
search: {
|
||||||
timeStart: '開始時間',
|
timeStart: '開始時間',
|
||||||
|
@ -62,6 +62,7 @@ const message = {
|
|||||||
hideSome: '隐藏部分',
|
hideSome: '隐藏部分',
|
||||||
agree: '同意',
|
agree: '同意',
|
||||||
notAgree: '不同意',
|
notAgree: '不同意',
|
||||||
|
preview: '预览',
|
||||||
},
|
},
|
||||||
search: {
|
search: {
|
||||||
timeStart: '开始时间',
|
timeStart: '开始时间',
|
||||||
|
@ -196,7 +196,10 @@ let icons = new Map([
|
|||||||
['.tar.bz2', 'p-file-zip'],
|
['.tar.bz2', 'p-file-zip'],
|
||||||
['.tar', 'p-file-zip'],
|
['.tar', 'p-file-zip'],
|
||||||
['.tar.gz', 'p-file-zip'],
|
['.tar.gz', 'p-file-zip'],
|
||||||
['.tar.xz', 'p-file-zip'],
|
['.war', 'p-file-zip'],
|
||||||
|
['.tgz', 'p-file-zip'],
|
||||||
|
['.7z', 'p-file-zip'],
|
||||||
|
['.rar', 'p-file-zip'],
|
||||||
['.mp3', 'p-file-mp3'],
|
['.mp3', 'p-file-mp3'],
|
||||||
['.svg', 'p-file-svg'],
|
['.svg', 'p-file-svg'],
|
||||||
['.txt', 'p-file-txt'],
|
['.txt', 'p-file-txt'],
|
||||||
@ -204,9 +207,32 @@ let icons = new Map([
|
|||||||
['.word', 'p-file-word'],
|
['.word', 'p-file-word'],
|
||||||
['.ppt', 'p-file-ppt'],
|
['.ppt', 'p-file-ppt'],
|
||||||
['.jpg', 'p-file-jpg'],
|
['.jpg', 'p-file-jpg'],
|
||||||
|
['.jpeg', 'p-file-jpg'],
|
||||||
|
['.png', 'p-file-png'],
|
||||||
['.xlsx', 'p-file-excel'],
|
['.xlsx', 'p-file-excel'],
|
||||||
['.doc', 'p-file-word'],
|
['.doc', 'p-file-word'],
|
||||||
|
['.xls', 'p-file-excel'],
|
||||||
|
['.docx', 'p-file-word'],
|
||||||
['.pdf', 'p-file-pdf'],
|
['.pdf', 'p-file-pdf'],
|
||||||
|
['.bmp', 'p-file-png'],
|
||||||
|
['.gif', 'p-file-png'],
|
||||||
|
['.tiff', 'p-file-png'],
|
||||||
|
['.ico', 'p-file-png'],
|
||||||
|
['.webp', 'p-file-png'],
|
||||||
|
['.mp4', 'p-file-video'],
|
||||||
|
['.webm', 'p-file-video'],
|
||||||
|
['.mov', 'p-file-video'],
|
||||||
|
['.wmv', 'p-file-video'],
|
||||||
|
['.mkv', 'p-file-video'],
|
||||||
|
['.avi', 'p-file-video'],
|
||||||
|
['.wma', 'p-file-video'],
|
||||||
|
['.flv', 'p-file-video'],
|
||||||
|
['.wav', 'p-file-mp3'],
|
||||||
|
['.wma', 'p-file-mp3'],
|
||||||
|
['.ape', 'p-file-mp3'],
|
||||||
|
['.acc', 'p-file-mp3'],
|
||||||
|
['.ogg', 'p-file-mp3'],
|
||||||
|
['.flac', 'p-file-mp3'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export function getIcon(extension: string): string {
|
export function getIcon(extension: string): string {
|
||||||
@ -526,3 +552,25 @@ export function emptyLineFilter(str: string, spilt: string) {
|
|||||||
}
|
}
|
||||||
return results.join(spilt);
|
return results.join(spilt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 文件类型映射
|
||||||
|
let fileTypes = {
|
||||||
|
image: ['.jpg', '.jpeg', '.png', '.bmp', '.gif', '.tiff', '.ico', '.svg', '.webp'],
|
||||||
|
compress: ['.zip', '.rar', '.gz', '.war', '.tgz', '.7z', '.tar.gz', '.tar'],
|
||||||
|
video: ['.mp4', '.webm', '.mov', '.wmv', '.mkv', '.avi', '.wma', '.flv'],
|
||||||
|
audio: ['.mp3', '.wav', '.wma', '.ape', '.acc', '.ogg', '.flac'],
|
||||||
|
pdf: ['.pdf'],
|
||||||
|
word: ['.doc', '.docx'],
|
||||||
|
excel: ['.xls', '.xlsx'],
|
||||||
|
text: ['.iso', '.tiff', '.exe', '.so', '.bz', '.dmg', '.apk', '.pptx', '.ppt', '.xlsb'],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getFileType = (extension: string) => {
|
||||||
|
let type = 'text';
|
||||||
|
Object.entries(fileTypes).forEach(([key, extensions]) => {
|
||||||
|
if (extensions.includes(extension.toLowerCase())) {
|
||||||
|
type = key;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return type;
|
||||||
|
};
|
||||||
|
@ -307,6 +307,7 @@
|
|||||||
<Favorite ref="favoriteRef" @close="search" />
|
<Favorite ref="favoriteRef" @close="search" />
|
||||||
<BatchRole ref="batchRoleRef" @close="search" />
|
<BatchRole ref="batchRoleRef" @close="search" />
|
||||||
<VscodeOpenDialog ref="dialogVscodeOpenRef" />
|
<VscodeOpenDialog ref="dialogVscodeOpenRef" />
|
||||||
|
<Preview ref="previewRef" />
|
||||||
</LayoutContent>
|
</LayoutContent>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -321,8 +322,8 @@ import {
|
|||||||
RemoveFavorite,
|
RemoveFavorite,
|
||||||
SearchFavorite,
|
SearchFavorite,
|
||||||
} from '@/api/modules/files';
|
} from '@/api/modules/files';
|
||||||
import { computeSize, copyText, dateFormat, downloadFile, getIcon, getRandomStr } from '@/utils/util';
|
import { computeSize, copyText, dateFormat, downloadFile, getFileType, getIcon, getRandomStr } from '@/utils/util';
|
||||||
import { StarFilled, Star, Top, Right } from '@element-plus/icons-vue';
|
import { StarFilled, Star, Top, Right, Close } from '@element-plus/icons-vue';
|
||||||
import { File } from '@/api/interface/file';
|
import { File } from '@/api/interface/file';
|
||||||
import { Mimetypes, Languages } from '@/global/mimetype';
|
import { Mimetypes, Languages } from '@/global/mimetype';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
@ -350,6 +351,7 @@ import Detail from './detail/index.vue';
|
|||||||
import RecycleBin from './recycle-bin/index.vue';
|
import RecycleBin from './recycle-bin/index.vue';
|
||||||
import Favorite from './favorite/index.vue';
|
import Favorite from './favorite/index.vue';
|
||||||
import BatchRole from './batch-role/index.vue';
|
import BatchRole from './batch-role/index.vue';
|
||||||
|
import Preview from './preview/index.vue';
|
||||||
import VscodeOpenDialog from '@/components/vscode-open/index.vue';
|
import VscodeOpenDialog from '@/components/vscode-open/index.vue';
|
||||||
|
|
||||||
const globalStore = GlobalStore();
|
const globalStore = GlobalStore();
|
||||||
@ -387,6 +389,7 @@ const fileCreate = reactive({ path: '/', isDir: false, mode: 0o755 });
|
|||||||
const fileCompress = reactive({ files: [''], name: '', dst: '', operate: 'compress' });
|
const fileCompress = reactive({ files: [''], name: '', dst: '', operate: 'compress' });
|
||||||
const fileDeCompress = reactive({ path: '', name: '', dst: '', mimeType: '' });
|
const fileDeCompress = reactive({ path: '', name: '', dst: '', mimeType: '' });
|
||||||
const fileEdit = reactive({ content: '', path: '', name: '', language: 'plaintext', extension: '' });
|
const fileEdit = reactive({ content: '', path: '', name: '', language: 'plaintext', extension: '' });
|
||||||
|
const filePreview = reactive({ path: '', name: '', extension: '', fileType: '' });
|
||||||
const codeReq = reactive({ path: '', expand: false, page: 1, pageSize: 100 });
|
const codeReq = reactive({ path: '', expand: false, page: 1, pageSize: 100 });
|
||||||
const fileUpload = reactive({ path: '' });
|
const fileUpload = reactive({ path: '' });
|
||||||
const fileRename = reactive({ path: '', oldName: '' });
|
const fileRename = reactive({ path: '', oldName: '' });
|
||||||
@ -416,6 +419,7 @@ const hoveredRowIndex = ref(-1);
|
|||||||
const favorites = ref([]);
|
const favorites = ref([]);
|
||||||
const batchRoleRef = ref();
|
const batchRoleRef = ref();
|
||||||
const dialogVscodeOpenRef = ref();
|
const dialogVscodeOpenRef = ref();
|
||||||
|
const previewRef = ref();
|
||||||
|
|
||||||
// editablePath
|
// editablePath
|
||||||
const { searchableStatus, searchablePath, searchableInputRef, searchableInputBlur } = useSearchable(paths);
|
const { searchableStatus, searchablePath, searchableInputRef, searchableInputBlur } = useSearchable(paths);
|
||||||
@ -484,7 +488,7 @@ const open = async (row: File.File) => {
|
|||||||
});
|
});
|
||||||
jump(req.path);
|
jump(req.path);
|
||||||
} else {
|
} else {
|
||||||
openCodeEditor(row.path, row.extension);
|
openView(row);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -680,6 +684,31 @@ const openDeCompress = (item: File.File) => {
|
|||||||
deCompressRef.value.acceptParams(fileDeCompress);
|
deCompressRef.value.acceptParams(fileDeCompress);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openView = (item: File.File) => {
|
||||||
|
const fileType = getFileType(item.extension);
|
||||||
|
|
||||||
|
const previewTypes = ['image', 'video', 'audio', 'pdf', 'word', 'excel'];
|
||||||
|
if (previewTypes.includes(fileType)) {
|
||||||
|
return openPreview(item, fileType);
|
||||||
|
}
|
||||||
|
|
||||||
|
const actionMap = {
|
||||||
|
compress: openDeCompress,
|
||||||
|
text: () => openCodeEditor(item.path, item.extension),
|
||||||
|
};
|
||||||
|
|
||||||
|
return actionMap[fileType] ? actionMap[fileType](item) : openCodeEditor(item.path, item.extension);
|
||||||
|
};
|
||||||
|
|
||||||
|
const openPreview = (item: File.File, fileType: string) => {
|
||||||
|
filePreview.path = item.path;
|
||||||
|
filePreview.name = item.name;
|
||||||
|
filePreview.extension = item.extension;
|
||||||
|
filePreview.fileType = fileType;
|
||||||
|
|
||||||
|
previewRef.value.acceptParams(filePreview);
|
||||||
|
};
|
||||||
|
|
||||||
const openCodeEditor = (path: string, extension: string) => {
|
const openCodeEditor = (path: string, extension: string) => {
|
||||||
codeReq.path = path;
|
codeReq.path = path;
|
||||||
codeReq.expand = true;
|
codeReq.expand = true;
|
||||||
@ -839,10 +868,11 @@ const getFavoriates = async () => {
|
|||||||
const toFavorite = (row: File.Favorite) => {
|
const toFavorite = (row: File.Favorite) => {
|
||||||
if (row.isDir) {
|
if (row.isDir) {
|
||||||
jump(row.path);
|
jump(row.path);
|
||||||
} else if (row.isTxt) {
|
|
||||||
openCodeEditor(row.path, '.' + row.name.split('.').pop());
|
|
||||||
} else {
|
} else {
|
||||||
jump(row.path.substring(0, row.path.lastIndexOf('/')));
|
let file = {} as File.File;
|
||||||
|
file.path = row.path;
|
||||||
|
file.extension = '.' + row.name.split('.').pop();
|
||||||
|
openView(file);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
146
frontend/src/views/host/file-management/preview/index.vue
Normal file
146
frontend/src/views/host/file-management/preview/index.vue
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
v-model="open"
|
||||||
|
:show-close="false"
|
||||||
|
:before-close="handleClose"
|
||||||
|
destroy-on-close
|
||||||
|
append-to-body
|
||||||
|
@opened="onOpen"
|
||||||
|
:class="isFullscreen ? 'w-full' : '!w-3/4'"
|
||||||
|
:top="'5vh'"
|
||||||
|
:fullscreen="isFullscreen"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span>{{ $t('commons.button.preview') + ' - ' + filePath }}</span>
|
||||||
|
<el-space alignment="center" :size="10" class="dialog-header-icon">
|
||||||
|
<el-tooltip :content="loadTooltip()" placement="top" v-if="fileType !== 'excel'">
|
||||||
|
<el-icon @click="toggleFullscreen"><FullScreen /></el-icon>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-icon @click="handleClose" size="20"><Close /></el-icon>
|
||||||
|
</el-space>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div v-loading="loading" :style="isFullscreen ? 'height: 90vh' : 'height: 80vh'">
|
||||||
|
<div class="flex items-center justify-center h-full">
|
||||||
|
<el-image
|
||||||
|
v-if="fileType === 'image'"
|
||||||
|
:src="fileUrl"
|
||||||
|
:style="isFullscreen ? 'height: 90vh' : 'height: 80vh'"
|
||||||
|
fit="contain"
|
||||||
|
:preview-src-list="[fileUrl]"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<video v-else-if="fileType === 'video'" :src="fileUrl" controls autoplay class="size-3/4"></video>
|
||||||
|
|
||||||
|
<audio v-else-if="fileType === 'audio'" :src="fileUrl" controls></audio>
|
||||||
|
|
||||||
|
<vue-office-pdf
|
||||||
|
v-else-if="fileType === 'pdf'"
|
||||||
|
:src="fileUrl"
|
||||||
|
:style="isFullscreen ? 'height: 90vh' : 'height: 80vh'"
|
||||||
|
class="w-full"
|
||||||
|
@rendered="renderedHandler"
|
||||||
|
@error="errorHandler"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<vue-office-docx
|
||||||
|
v-else-if="fileType === 'word'"
|
||||||
|
:src="fileUrl"
|
||||||
|
:style="isFullscreen ? 'height: 90vh' : 'height: 80vh'"
|
||||||
|
class="w-full"
|
||||||
|
@rendered="renderedHandler"
|
||||||
|
@error="errorHandler"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<vue-office-excel
|
||||||
|
v-else-if="fileType === 'excel'"
|
||||||
|
:src="fileUrl"
|
||||||
|
:style="isFullscreen ? 'height: 90vh;' : 'height: 80vh'"
|
||||||
|
class="w-full"
|
||||||
|
@rendered="renderedHandler"
|
||||||
|
@error="errorHandler"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import i18n from '@/lang';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { Close, FullScreen } from '@element-plus/icons-vue';
|
||||||
|
import VueOfficeDocx from '@vue-office/docx';
|
||||||
|
import VueOfficeExcel from '@vue-office/excel';
|
||||||
|
import VueOfficePdf from '@vue-office/pdf';
|
||||||
|
import '@vue-office/docx/lib/index.css';
|
||||||
|
import '@vue-office/excel/lib/index.css';
|
||||||
|
import { MsgError } from '@/utils/message';
|
||||||
|
|
||||||
|
interface EditProps {
|
||||||
|
fileType: string;
|
||||||
|
path: string;
|
||||||
|
name: string;
|
||||||
|
extension: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const open = ref(false);
|
||||||
|
const loading = ref(false);
|
||||||
|
const filePath = ref('');
|
||||||
|
const fileName = ref('');
|
||||||
|
const fileType = ref('');
|
||||||
|
const fileUrl = ref('');
|
||||||
|
|
||||||
|
const fileExtension = ref('');
|
||||||
|
const isFullscreen = ref(false);
|
||||||
|
const em = defineEmits(['close']);
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
open.value = false;
|
||||||
|
|
||||||
|
em('close', open.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderedHandler = () => {
|
||||||
|
loading.value = false;
|
||||||
|
};
|
||||||
|
const errorHandler = () => {
|
||||||
|
open.value = false;
|
||||||
|
MsgError(i18n.global.t('commons.msg.unSupportType'));
|
||||||
|
};
|
||||||
|
const loadTooltip = () => {
|
||||||
|
return i18n.global.t('commons.button.' + (isFullscreen.value ? 'quitFullscreen' : 'fullscreen'));
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleFullscreen = () => {
|
||||||
|
isFullscreen.value = !isFullscreen.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const acceptParams = (props: EditProps) => {
|
||||||
|
fileExtension.value = props.extension;
|
||||||
|
fileName.value = props.name;
|
||||||
|
filePath.value = props.path;
|
||||||
|
fileType.value = props.fileType;
|
||||||
|
isFullscreen.value = fileType.value === 'excel';
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
fileUrl.value = `${import.meta.env.VITE_API_URL as string}/files/download?path=${encodeURIComponent(props.path)}`;
|
||||||
|
open.value = true;
|
||||||
|
loading.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onOpen = () => {};
|
||||||
|
|
||||||
|
defineExpose({ acceptParams });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.dialog-top {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-header-icon {
|
||||||
|
color: var(--el-color-info);
|
||||||
|
}
|
||||||
|
</style>
|
Loading…
x
Reference in New Issue
Block a user