前言

公司的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. vue

2.代码实现

// 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>