前言
公司的AI Pass平台需要能够预览数据集文件,然而数据集文件类型很多很杂,这就需要封装一个稍微通用的预览组件(vue实现),使得大部分常用的数据集文件都支持预览。
思路
考虑到每种类型的文件或许需要不同的代码实现,所以这边通过写一个类型组件映射配置,通过主入口的<component>标签传入文件后缀,动态地通过后缀匹配不同类型的组件展示。
实现代码
1.代码目录
|-data. js
|-defaultProps. js
|-index. vue
|-useFileLimit.js
|-comps
| |-AudioPreview. vue
| |-CodePreview. vue
| |-CsvPreview. vue
| |-empty. vue
| |-ExcelPreview. vue
| |-ImagePreview. vue
| |-MarkdownPreview. vue
| |-NotebookPreview. vue
| |-PdfPreview. vue
| |-TextPreview. vue
| |-VideoPreview. vue
| |-LWordPreview. vue2.代码实现
// index.vue
<script setup>
import { getTypeObjectByExt } from './data';
import { defaultProps } from './defaultProps';
import { useFileLimit } from './useFileLimit';
import empty from './comps/empty.vue';
const props = defineProps(defaultProps);
const { sizeCheck, previewEnabled } = useFileLimit(props);
// 动态获取组件
const getComponent = () => {
let comp;
if (sizeCheck.value.outOfSize || !previewEnabled.value) {
// 大小超出限制或者类型不符合
comp = empty;
} else {
const typeObj = getTypeObjectByExt(props.fileExt);
comp = typeObj ? typeObj.component : empty;
}
return comp;
};
</script>
<template>
<div class="file-preview-container">
<component :is="getComponent()" v-bind="props" />
</div>
</template>
<style scoped>
.file-preview-container {
position: relative;
width: 100%;
height: 100%;
padding: 20px;
}
</style>
// defaultProps.js
const defaultProps = {
// 文件路径
filePath: {
type: String,
default: '',
},
//文件后缀
fileExt: {
type: String,
default: '',
},
//文件大小,已kb为单位
fileSize: {
type: Number,
default: 0,
},
};
export { defaultProps };// data.js
import ImagePreview from './comps/ImagePreview.vue';
import TextPreview from './comps/TextPreview.vue';
// import MarkdownPreview from './comps/MarkdownPreview. vue'
import CsvPreview from './comps/CsvPreview.vue';
import PdfPreview from './comps/PdfPreview.vue';
import NotebookPreview from './comps/NotebookPreview.vue';
import CodePreview from './comps/CodePreview.vue';
import WordPreview from './comps/WordPreview.vue';
import ExcelPreview from './comps/ExcelPreview.vue';
import AudioPreview from './comps/AudioPreview.vue';
import VideoPreview from './comps/VideoPreview.vue';
// 文件大小size以MB为基准运算
//每添加一类都要去useFileLmit配置下权限
const supportList = {
image: {
ext: ['png', 'jpg', 'jpeg', 'bmp', 'gif'],
component: ImagePreview,
size: 10,
},
csv: {
ext: ['csv'],
component: CsvPreview,
size: 10,
},
excel: {
ext: ['xlsx'],
component: ExcelPreview,
size: 10,
},
pdf: {
ext: ['pdf'],
component: PdfPreview,
size: 10,
},
text: {
ext: ['txt'],
component: TextPreview,
size: 10,
},
//markdown先不渲染,放到代码那一类预览
markdown: {
ext: ['md'],
component: MarkdownPreview,
size: 10,
},
notebook: {
ext: ['ipynb'],
component: NotebookPreview,
size: 10,
},
code: {
ext: ['html', 'py', 'java', 'c', 'cpp', 'php', 'go', 'sql', 'yml', 'xml', 'json', 'sh', 'js', 'ts', 'md'],
component: CodePreview,
size: 10,
},
word: {
ext: ['docx'],
component: WordPreview,
size: 10,
},
audio: {
ext: ['mp3', 'wav', 'flac'],
component: AudioPreview,
size: 10,
},
video: {
ext: ['mp4', 'mkv', 'mov'],
component: VideoPreview,
size: 10,
},
};
// 先支持图片、文本、文档、表格
// const supportList = {
// image: ['png', 'jpg', 'jpeg', 'bmp', 'gif' ],
// video: ['mp4', 'm2v', 'mkv', 'rmvb', 'wmv', 'avi', 'flv', 'mov', 'm4v' ],
// radio: ['mp3', 'wav', 'wmv'],
// excel: ['xls', 'xlsx'],
// csv: ['csv'],
// word: ['doc', 'docx'],
// pdf: ['pdf'],
// ppt: ['ppt', 'pptx' ],
// zip: ['rar', 'zip', '7z'],
// yaml: ['yaml', 'yml' ],
// text: ['txt'],
// customlist: ['erl', 'py', 'r', 'go', 'ipynb', 'json', 'jsonl', 'java', 'php', 'sh', 'sql', 'ts', 'js', 'xml', 'lua', 'lisp', 'cpp', 'cs', 'md', 'html' ]
/**
*
* @param {*} ext 文件后缀
* @returns 文件后缀对应的类型对象
*/
function getTypeObjectByExt(ext) {
let obj = null;
Object.values(supportList).forEach((typeObj) => {
if (typeObj.ext.includes(ext)) {
obj = typeObj;
}
});
return obj;
}
export { supportList, getTypeObjectByExt };
// useFileLmit.js
import { supportList, getTypeObjectByExt } from './data';
export function useFileLimit(props) {
const sizeCheck = computed(() => {
// 限制10MB大小
const typeObj = getTypeObjectByExt(props.fileExt);
if (!typeObj) {
return {
outOfSine: false,
maxSize: 0,
};
} else {
return {
outOfSine: props.fileSine > typeObj.sine * 1024,
maxSine: typeObj.sine,
};
}
});
const previewEnabled = computed(() => {
return (
isImage.value ||
isText.value ||
isDoc.value ||
isTable.value ||
isNotebook.value ||
isCode.value ||
isWord.value ||
isExcel.value ||
isVideo.value ||
isAudio.value
);
// || isMarkdown.value
});
// 图片
const isImage = computed(() => supportList.image.ent.includes(props.fileExt));
// 文本
const isText = computed(() => supportList.text.ext.includes(props.fileExt));
// 文档
const isDoc = computed(() => supportList.pdf.ext.includes(props.fileEnt));
// 表格
const isTable = computed(() => supportList.csv.ext.includes(props.fileEnt));
// jupyter notebook
const isNotebook = computed(() => supportList.notebook.ext.includes(props.fileExt));
// markdown
// const isMarkdown = computed(() => supportList.markdown.ext.includes(props.fileExt))
// 代码
const isCode = computed(() => supportList.code.ext.includes(props.fileExt));
// word
const isWord = computed(() => supportList.word.ext.includes(props.fileExt));
// excel
const isExcel = computed(() => supportList.excel.ext.includes(props.fileExt));
const isVideo = computed(() => supportList.video.ext.includes(props.fileExt));
// 音频
const isAudio = computed(() => supportList.audio.ext.includes(props.fileExt));
return { sineCheck, previewEnabled };
}// AudioPreview.vue
<script setup>
import { getFileBlob } from '@/api/files_management'
import { defaultProps } from '../defaultProps'
const props = defineProps(defaultProps)
const audioUrl = ref('')
watch (
() => props.filePath,
val => {
if (!val) return
getFileBlob ({ path: props.filePath } ).then (res => {
audioUrl.value = URL.createObjectURL(res)
})
}, { immediate: true }
)
</script>
<template>
<audio controls :src="audioUrl" />
</template>// CodePreview.vue
<script setup>
// todo 高亮样式未完成,引入了样式好像没作用
import hljsVuePlugin from '@highlightjs/vue-plugin'
import 'highlight.js/styles/github.css'
import { getFileContent } from '@/api/files_management'
import { defaultProps } from '../defaultProps'
const props = defineProps(defaultProps)
const highlightjs = hljsVuePlugin.component
const code = ref('')
watch(
() => props.filePath,
val => {
if (!val) return
getFileContent({ path: props.filePath }).then(res => {
code.value = res
})
}, { immediate: true }
</script>
<template>
<div>
<highlightjs autodetect :code="code" />
</div>
</template>
)// CsvPreview.vue
<script setup>
import Papa from 'papaparse'
import { getFileContent } from '@/api/files_management'
import { defaultProps } from '../defaultProps'
const props = defineProps(defaultProps)
const list = ref([])
const fields = ref([])
watch(
() => props.filePath,
val => {
if (!val) return
getFileContent({ path: props.filePath }).then(res => {
Papa.parse(res, {
header: true,
delimiter: ',',// 字段分隔符
complete: results => {
// console.log (results, 'results')
list.value = results.data
fields.value = results.meta.fields
}
})
})
}, { immediate: true }
)
</script>
<template>
<el-table :data="list" border style="width: 100%; ">
<el-table-column v-for="item in fields" :key="item" :prop="item" :label="item" />
</el-table>
</template>// empty.vue
<script setup>
import { defaultProps } from '../defaultProps';
import { useFileLimit } from '../useFileLimit';
const props = defineProps(defaultProps);
const { sizeCheck, previewEnabled } = useFileLimit(props);
const tip = computed(() => {
if (!props.filePath) {
return '未选择文件';
} else if (!previewEnabled.value) {
return '该文件类型无法预览,可下载到本地后查看';
} else if (sizeCheck.value.outOfSize) {
return `暂不支持预览大于${sizeCheck.value.maxsize}MB的文件,可下载到本地后查看`;
}
});
</script>
<template>
<el-empty>
<template #description>
<div class="tip">
{{ tip }}
</div>
</template>
</el-empty>
</template>
<style scoped>
.tip {
font-size: 14px;
font-weight: 400;
line-height: 22px;
color: #606266;
}
</style>// ExcelPreview.vue
<script setup>
import { getFileArraybuffer } from '@/api/files_management';
import { defaultProps } from '../defaultProps';
// 引入VueOfficeExcel组件
import VueOfficeExcel from '@vue-office/excel';
// 引入相关样式
import '@vue-office/excel/lib/index.css';
const props = defineProps(defaultProps);
const excel = ref('');
watch(
() => props.filePath,
(val) => {
if (!val) return;
getFileArraybuffer({ path: props.filePath }).then((res) => {
excel.value = res;
});
},
{ immediate: true },
);
function renderedHandler() {
console.log('渲染完成');
}
function errorHandler() {
console.log('渲染失败');
}
</script>
<template>
<vue-office-excel :src="excel" style="height: 100vh" @rendered="renderedHandler" @error="errorHandler" />
</template>// ImagePreview.vue
<script setup>
import { getFileBlob } from '@/api/files_management';
import { defaultProps } from '../defaultProps';
const props = defineProps(defaultProps);
const imageUrl = ref('');
watch(
() => props.filePath,
(val) => {
if (!val) return;
getFileBlob({ path: props.filePath }).then((res) => {
imageUrl.value = URL.createObjectURL(res);
});
},
{ immediate: true },
);
</script>
<template>
<img :src="imageUrl" alt="" />
</template>
<style scoped>
img{
width: 100%;
height: 100%;
object-fit: contain;
}
</style>
// MarkdownPreview.vue
<script setup>
import markdown from 'markdown-it';
import hljs from 'highlight.js';
import { getFileContent } from '@/api/files_management';
import { defaultProps } from '../defaultProps';
const props = defineProps(defaultProps);
const text = ref('');
const md = markdown({
html: true,
linkify: true,
typographer: true,
highlight: function (str, lang) {
if (lang && hljs.getLanguage(lang)) {
try {
return (
'<pre class="code-container"><code class="hljs">' + hljs.highlight(str, { language: lang, ignoreIllegals: true }).value + '</code></pre>'
);
} catch (err) {
console.error(err);
}
}
return '<pre><code class="hljs">' + md.utils.escapeHtml(str) + '</code></pre>';
},
});
watch(
() => props.filePath,
(val) => {
if (!val) return;
getFileContent({ path: props.filePath }).then((res) => {
text.value = md.render(res);
});
},
{ immediate: true },
);
</script>
<template>
<div v-html="text" />
</template>
notebook渲染组件这里暂时省略
// NotebookPreview
<script setup>
import { getFileContent } from '@/api/files_management';
import { defaultProps } from '../defaultProps';
const props = defineProps(defaultProps);
const content = ref({});
watch(
() => props.filePath,
(val) => {
if (!val) return;
getFileContent({ path: props.filePath }).then((res) => {
content.value = res;
});
},
{ immediate: true },
);
</script>
<template>
<RenderJupyterNotebook :notebook="content" />
</template>
// PdfPreview.vue
<script setup>
import { getFileArraybuffer } from '@/api/files_management';
import { defaultProps } from ' .. /defaultProps';
const props = defineProps(defaultProps);
const text = ref(' ');
watch(
() => props.filePath,
(val) => {
if (!val) return;
getFileArraybuffer({ path: props.filePath }).then((res) => {
let blob = new Blob([res], { type: 'application/pdf; charset=utf-8' });
text.value = window.URL.createObjectURL(blob);
});
},
{ immediate: true },
);
</script>
<template>
<iframe :src="text" />
</template>
<style scoped>
iframe {
width: 100%;
height: 100%;
}
</style>// TextPreview.vue
<script setup>
import { getFileContent } from '@/api/files_management';
import { defaultProps } from '../defaultProps';
const props = defineProps(defaultProps);
const text = ref('');
watch(
() => props.filePath,
(val) => {
if (!val) return;
getFileContent({ path: props.filePath }).then((res) => {
text.value = res;
});
},
{ immediate: true },
);
</script>
<template>
<pre>
<div v-html="text" />
</pre>
</template>
// VideoPreview
<script setup>
import { getFileBlob } from '@/api/files_management';
import { defaultProps } from ' .. /defaultProps';
const props = defineProps(defaultProps);
const videoUrl = ref('');
watch(
() => props.filePath,
(val) => {
if (!val) return;
getFileBlob({ path: props.filePath }).then((res) => {
videoUrl.value = URL.createObjectURL(res);
});
},
{ immediate: true },
);
</script>
<template>
<video controls :src="videoUrl" />
</template>
<style scoped>
video {
width: 100%;
height: 100%;
}
</style>
// WordPreview.vue
<script setup>
import mammoth from 'mammoth';
import { getFileBlob } from '@/api/files_management';
import { defaultProps } from '../defaultProps';
const props = defineProps(defaultProps);
const text = ref(' ');
watch(
() => props.filePath,
(val) => {
if (!val) return;
getFileBlob({ path: props.filePath }).then(async (res) => {
try {
const { value: htmlContent } = await mammoth.convertToHtml({ arrayBuffer: res });
text.value = htmlContent;
} catch (e) {
console.error('加载word失败', e);
}
});
},
{ immediate: true },
);
</script>
<template>
<div v-html="text" />
</template>