前端各种文本文件预览 文本编辑excel预览编辑 pdf预览word预览 excel下载pdf下载word下载
各种文本文件预览(pdf, xlsx, docx, cpp, java, sql, py, vue, html, js, json, css, xml, rust, md, txt, log, fa, fasta, tsv, csv 等各种文本文件)
其中 除pdf,xlsx,docx之外的文本还可以修改,xlsx想要修改看我另一篇博客luckyexcel 编辑预览excel文件
先看效果
引入库
//预览编辑文本用
"@codemirror/lang-cpp": "^6.0.2",
"@codemirror/lang-css": "^6.2.1",
"@codemirror/lang-html": "^6.4.9",
"@codemirror/lang-java": "^6.0.1",
"@codemirror/lang-javascript": "^6.2.2",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-markdown": "^6.2.5",
"@codemirror/lang-python": "^6.1.6",
"@codemirror/lang-rust": "^6.0.1",
"@codemirror/lang-sql": "^6.7.0",
"@codemirror/lang-vue": "^0.1.3",
"@codemirror/lang-xml": "^6.1.0",
"@codemirror/theme-one-dark": "^6.1.2",
"codemirror": "^6.0.1",
//预览word、pdf、excel文件用
"@vue-office/docx": "^1.6.2",
"@vue-office/excel": "^1.7.11",
"@vue-office/pdf": "^2.0.2",
//导出word
"buffer": "^6.0.3",
"docxtemplater": "^3.46.0",
"file-saver": "^2.0.5",
"pizzip": "^3.1.6",
"jszip-utils": "^0.1.0",
//导出pdf
"html2canvas": "^1.4.1",
"jspdf": "^2.5.1",
//导出xlsx文件(试用于electron,web也可以用)
"node-xlsx": "^0.23.0",
//或者用xlsx库
"xlsx": "^0.18.5"
导出pdf、xlsx、word文件
<template>
<div class="content" id="exportPdf">
<a-space>
<a-button type="primary" @click="exportWord">导出word</a-button>
<a-button type="primary" @click="exportXlsx">导出elcel</a-button>
<a-button type="primary" @click="exportPdf">导出pdf</a-button>
<a-button type="primary" @click="() => router.push('/preview')">文件预览</a-button>
</a-space>
<br />
<br />
<hr />
<div class="flex flex-justify-around">
<div>unocss</div>
<div>测试</div>
<div>可以用的</div>
</div>
<br />
<div class="text-center">
<a href="https://unocss.dev/interactive/" target="_blank" rel="noopener noreferrer"
>unocss 文档</a
>
</div>
</div>
</template>
<script lang="ts" setup>
import { useRouter } from 'vue-router';
import { setLogin, getUserInfo } from '../../serve/api/login';
import { globalData } from '../../setting/global';
import { Buffer } from 'buffer';
import xlsx from 'node-xlsx';
import docxtemplater from 'docxtemplater';
import PizZip from 'pizzip';
import JSZipUtils from 'jszip-utils';
import { saveAs } from 'file-saver';
import html2canvas from 'html2canvas';
import JsPDF from 'jspdf';
const router = useRouter();
const tableValue = reactive({
unit: '中国',
date: undefined,
sampleType: '你猜',
people: '黄种人',
name: '夜空',
sex: '男',
age: '25',
work: '开发',
id: '',
jiance: '商品化试剂盒',
date2: undefined,
});
const exportWord = () => {
let docxname = '导出word.docx';
JSZipUtils.getBinaryContent('/template.docx', function (error: any, content: any) {
// template.docx是模板(这里我放到public公共文件夹下面了)。我们在导出的时候,会根据此模板来导出对应的数据
// 抛出异常
if (error) {
throw error;
}
// 创建一个PizZip实例,内容为模板的内容
let zip = new PizZip(content);
// 创建并加载docx templater实例对象
let doc = new docxtemplater().loadZip(zip);
// 设置模板变量的值 主要变量替换在这里
doc.setData({
name: tableValue.name,
unit: tableValue.unit,
date: '这里也不可以不写变量',
sampleType: tableValue.sampleType,
sex: tableValue.sex,
age: tableValue.age,
});
try {
// 用模板变量的值替换所有模板变量
doc.render();
} catch (error: any) {
// 抛出异常
let e = {
message: error.message,
name: error.name,
stack: error.stack,
properties: error.properties,
};
console.log(
JSON.stringify({
error: e,
}),
);
throw error;
}
// 生成一个代表docxtemplater对象的zip文件(不是一个真实的文件,而是在内存中的表示)
let out = doc.getZip().generate({
type: 'blob',
mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
});
// 将目标文件对象保存为目标类型的文件,并命名
saveAs(out, docxname);
});
};
const exportXlsx = () => {
let data = [
[1, 222, '', '', '', ''],
['', 2, 3, 4, 5, 6],
['', 2, 3, 4, 5, 6],
['', 2, 3, 4, 5, 6],
['', 2, 3, 4, 5, 6],
[22, 2, 3, 4, 5, 6],
];
// 行列合并规则 c:col 列 r:row 行
const range0 = { s: { c: 0, r: 0 }, e: { c: 0, r: 4 } };
const range1 = { s: { c: 1, r: 0 }, e: { c: 5, r: 0 } };
const sheetOptions = {
'!merges': [range0, range1],
// cols 列宽大小
'!cols': [{ wch: 5 }, { wch: 10 }, { wch: 15 }, { wch: 20 }, { wch: 30 }, { wch: 50 }],
};
//如果不需要格式,这里的sheetOptions可以省略不写
let result = xlsx.build([{ name: 'sheet1', data }], { sheetOptions });
const ab = Buffer.from(result, 'binary');
const blob = new Blob([ab]);
const blobUrl = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = blobUrl;
a.download = '导出excel.xlsx';
a.click();
window.URL.revokeObjectURL(blobUrl);
};
const exportPdf = () => {
downloadPDF(document.querySelector('#exportPdf'), '导出pdf文件', pdfSuc(), 4);
};
const pdfSuc = () => {
message.success('导出成功');
};
/**
* ele:需要导出的容器
* pdfName:导出文件的名字
* callback: 成功回调
*/
const downloadPDF = (ele: any, pdfName: any, callback: any, scale?: number) => {
html2canvas(ele, {
dpi: 600,
scale: scale ? scale : 8,
// allowTaint: true, //允许 canvas 污染, allowTaint参数要去掉,否则是无法通过toDataURL导出canvas数据的
useCORS: true, //允许canvas画布内 可以跨域请求外部链接图片, 允许跨域请求。
width: ele.scrollWidth,
height: ele.scrollHeight,
}).then((canvas) => {
//未生成pdf的html页面高度
var leftHeight = canvas.height;
var a4Width = 595.28;
var a4Height = 801.89; //(一张A4高=841.89减去20,使得上下边距空出20,pdf.addImage生成上边距(第四个参数=10)致使使得上下边距各10)
//一页pdf显示html页面生成的canvas高度;
var a4HeightRef = Math.floor((canvas.width / a4Width) * a4Height);
//pdf页面偏移
var position = 0;
var pageData = canvas.toDataURL('image/jpeg', 1.0);
var pdf = new JsPDF('x', 'pt', 'a4');
var index = 1,
canvas1 = document.createElement('canvas'),
height;
pdf.setDisplayMode('fullwidth', 'continuous', 'FullScreen');
function createImpl(canvas) {
if (leftHeight > 0) {
index++;
var checkCount = 0;
if (leftHeight > a4HeightRef) {
var i = position + a4HeightRef;
for (i = position + a4HeightRef; i >= position; i--) {
var isWrite = true;
for (var j = 0; j < canvas.width; j++) {
var c = canvas.getContext('2d').getImageData(j, i, 1, 1).data;
if (c[0] != 0xff || c[1] != 0xff || c[2] != 0xff) {
isWrite = false;
break;
}
}
if (isWrite) {
checkCount++;
if (checkCount >= 10) {
break;
}
} else {
checkCount = 0;
}
}
height = Math.round(i - position) || Math.min(leftHeight, a4HeightRef);
if (height <= 0) {
height = a4HeightRef;
}
} else {
height = leftHeight;
}
canvas1.width = canvas.width;
canvas1.height = height;
var ctx = canvas1.getContext('2d');
ctx.drawImage(canvas, 0, position, canvas.width, height, 0, 0, canvas.width, height);
if (position != 0) {
pdf.addPage();
}
// 在pdf.addImage(pageData, 'JPEG', 左间距,上间距,宽度,高度)设置在pdf中显示;
pdf.addImage(
canvas1.toDataURL('image/jpeg', 1.0),
'JPEG',
70,
56,
a4Width - 140,
(a4Width / canvas1.width) * height - 112,
);
leftHeight -= height;
position += height;
if (leftHeight > 0) {
setTimeout(createImpl, 500, canvas);
callback();
} else {
pdf.save(pdfName);
callback();
}
}
}
// 当内容未超过pdf一页显示的范围,无需分页
if (leftHeight < a4HeightRef) {
pdf.addImage(pageData, 'JPEG', 0, 50, a4Width, (a4Width / canvas.width) * leftHeight);
pdf.save(pdfName);
// callback();
} else {
try {
pdf.deletePage(0);
setTimeout(createImpl, 500, canvas);
} catch (err) {
console.log(err);
}
}
});
};
</script>
<style lang="less" scoped>
.content {
width: 90vw;
min-height: 90vh;
margin: 5vh auto;
padding: 20px;
outline: 1px dashed #999;
}
</style>
预览各种文本
<!--
* @Descripttion:
* @Author: 苍狼一啸八荒惊
* @Date: 2024-08-12 12:18:53
* @LastEditTime: 2024-08-13 15:27:27
* @LastEditors: 夜空苍狼啸
-->
<script lang="ts" setup>
// ! 目前不支持doc、xls格式文件的预览
//引入VueOfficeDocx组件
import VueOfficeDocx from '@vue-office/docx';
import '@vue-office/docx/lib/index.css';
//引入VueOfficeExcel组件
import VueOfficeExcel from '@vue-office/excel';
import '@vue-office/excel/lib/index.css';
//引入VueOfficePdf组件
import VueOfficePdf from '@vue-office/pdf';
import { message } from 'ant-design-vue';
import { useRouter } from 'vue-router';
const router = useRouter();
const fileText: any = undefined;
const data = reactive({
loading: false,
fileList: [],
fileType: 'xlsx',
// docxSrc: 'http://static.shanhuxueyuan.com/test6.docx',
// excelSrc: 'http://static.shanhuxueyuan.com/demo/excel.xlsx',
docxSrc: '',
excelSrc: '/template.xlsx',
pdfSrc: '',
codemirror: false,
fileText,
refreshCodemirrorKey: 0,
});
const customUpload = async (info: any) => {
data.loading = true;
console.log(info);
let suffixName = info.file.name.split('.').pop();
data.fileType = suffixName;
data.codemirror = false;
let fileReader = new FileReader();
fileReader.readAsArrayBuffer(info.file);
if (['docx', 'doc'].includes(suffixName)) {
if (suffixName === 'doc') {
message.info('请上传docx格式的文件');
return;
}
fileReader.onload = () => {
data.docxSrc = fileReader.result;
};
} else if (['xlsx', 'xls'].includes(suffixName)) {
if (suffixName === 'xls') {
message.info('请上传xlsx格式的文件');
return;
}
fileReader.onload = () => {
data.excelSrc = fileReader.result;
};
} else if (['pdf'].includes(suffixName)) {
fileReader.onload = () => {
data.pdfSrc = fileReader.result;
};
} else if (
['cpp', 'java', 'sql', 'py', 'vue', 'html', 'js', 'json', 'css', 'xml', 'rust', 'md'].includes(
suffixName,
)
) {
// 文本,启用 Codemirror
data.codemirror = true;
data.fileText = await readFileAsync(info.file);
data.refreshCodemirrorKey++;
} else {
// message.info('该格式暂不支持查看!');
data.codemirror = true;
data.fileText = await readFileAsync(info.file);
data.refreshCodemirrorKey++;
}
data.loading = false;
};
const handleChange = async (e: any) => {
let file = e.target.files[0];
let suffixName = file.name.split('.').pop();
data.fileType = suffixName;
data.codemirror = false;
let fileReader = new FileReader();
fileReader.readAsArrayBuffer(file);
if (['docx', 'doc'].includes(suffixName)) {
if (suffixName === 'doc') {
message.info('请上传docx格式的文件');
return;
}
fileReader.onload = () => {
data.docxSrc = fileReader.result;
};
} else if (['xlsx', 'xls'].includes(suffixName)) {
if (suffixName === 'xls') {
message.info('请上传xlsx格式的文件');
return;
}
// 使用blob文件流
let blob = new Blob([file], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
});
data.excelSrc = URL.createObjectURL(blob);
// 读取文件的ArrayBuffer
// fileReader.onload = () => {
// data.excelSrc = fileReader.result;
// };
// data.excelSrc = URL.createObjectURL(file);
} else if (['pdf'].includes(suffixName)) {
fileReader.onload = () => {
data.pdfSrc = fileReader.result;
};
} else if (
['txt', 'vue', 'json', 'java', 'sql', 'js', 'css', 'xml', 'html', 'yaml', 'md', 'py'].includes(
suffixName,
)
) {
// 文本,启用 Codemirror
data.codemirror = true;
data.fileText = await readFileAsync(file);
data.refreshCodemirrorKey++;
} else {
// message.info('该格式暂不支持查看!');
data.codemirror = true;
data.fileText = await readFileAsync(file);
data.refreshCodemirrorKey++;
}
};
const rendered = () => {
console.log('渲染完成');
};
const errorHandler = () => {
console.log('渲染失败');
};
// 读取文本文件内容
const readFileAsync = (file: Blob | File) => {
return new Promise((resolve, reject) => {
// 读取文件里面的内容返回
var reader = new FileReader();
// 以文本格式读取文件
reader.readAsText(file, 'UTF-8');
reader.onload = function (event) {
resolve(event.target.result);
};
reader.onerror = function (event) {
reject(event.target);
};
});
};
onMounted(() => {});
</script>
<template>
<div class="content">
<div class="mb-1 color-red-500">
支持预览文件: pdf, xlsx, docx, cpp, java, sql, py, vue, html, js, json, css, xml, rust, md,
txt, log, fa, fasta, tsv, csv 等各种文本文件
</div>
<a-space class="mb-1">
<!-- accept="application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" -->
<a-upload
v-model:file-list="data.fileList"
:customRequest="customUpload"
name="file"
:multiple="false"
:showUploadList="false"
>
<a-button :loading="data.loading" type="primary">上传文件</a-button>
</a-upload>
<input type="file" ref="fileButton" @change="handleChange" />
<a-button type="primary" @click="() => router.push('/index')">文件导出</a-button>
</a-space>
<!-- office -->
<div v-if="!data.codemirror">
<!-- docx -->
<vue-office-docx
v-if="data.fileType === 'docx'"
:src="data.docxSrc"
style="height: 80vh"
@rendered="rendered"
@error="errorHandler"
/>
<!-- excel -->
<vue-office-excel
v-else-if="data.fileType === 'xlsx'"
:src="data.excelSrc"
style="height: 80vh"
@rendered="rendered"
@error="errorHandler"
/>
<!-- pdf -->
<vue-office-pdf
v-else-if="data.fileType === 'pdf'"
:src="data.pdfSrc"
style="height: 100vh"
@rendered="rendered"
@error="errorHandler"
/>
</div>
<!-- 文本 -->
<div v-else class="mt--6">
<Codemirror :fileText="data.fileText" :fileType="data.fileType" />
<!-- :key="data.refreshCodemirrorKey" -->
</div>
</div>
</template>
<style lang="less" scoped>
.content {
width: 90vw;
min-height: 90vh;
margin: 5vh auto;
padding: 20px;
outline: 1px dashed #999;
}
</style>
子组件
<!--
* @Descripttion:
* @Author: 苍狼一啸八荒惊
* @Date: 2024-08-12 13:51:19
* @LastEditTime: 2024-08-13 15:36:07
* @LastEditors: 夜空苍狼啸
-->
<script lang="ts" setup>
// codemirror api https://codemirror.net/docs/guide/
import { EditorState, Text, Compartment } from '@codemirror/state';
import { basicSetup, EditorView } from 'codemirror';
import { keymap, lineNumbers } from '@codemirror/view';
import { defaultKeymap } from '@codemirror/commands';
import { oneDark } from '@codemirror/theme-one-dark';
import { json, jsonParseLinter } from '@codemirror/lang-json';
import { css } from '@codemirror/lang-css';
import { cpp } from '@codemirror/lang-cpp';
import { html } from '@codemirror/lang-html';
import { java } from '@codemirror/lang-java';
import { javascript as js } from '@codemirror/lang-javascript';
import { markdown as md } from '@codemirror/lang-markdown';
import { python as py } from '@codemirror/lang-python';
import { sql } from '@codemirror/lang-sql';
import { rust } from '@codemirror/lang-rust';
import { vue } from '@codemirror/lang-vue';
import { xml } from '@codemirror/lang-xml';
import { saveTextAsFile } from '/@/libs/utils/download';
const props = defineProps({
fileText: {
type: String,
default: 'hello word!', //文本
},
fileType: {
type: String,
default: 'json', // 编辑模式(文件类型)
},
});
const data = reactive({
fontSize: '14',
theme: 'dark', // codeMirror主题
readOnly: false,
lineNumber: true,
});
const editorRef: Ref<InstanceType<typeof Element> | undefined> = ref();
onMounted(() => {});
onUnmounted(() => {
view?.destroy();
});
watch(
() => props.fileText,
(n) => {
init();
},
);
let view: any;
const init = () => {
view?.destroy();
let startState = EditorState.create({
doc: props.fileText,
extensions: [
data.lineNumber ? basicSetup : [],
data.theme == 'default' ? [] : oneDark,
EditorState.readOnly.of(!data.readOnly),
textType(props.fileType),
// 自定义主题
// EditorView.theme(
// {
// '&': {
// color: 'white',
// backgroundColor: '#034',
// },
// '.cm-content': {
// caretColor: '#0e9',
// },
// '&.cm-focused .cm-cursor': {
// borderLeftColor: '#0e9',
// },
// '&.cm-focused .cm-selectionBackground, ::selection': {
// backgroundColor: '#074',
// },
// '.cm-gutters': {
// backgroundColor: '#045',
// color: '#ddd',
// border: 'none',
// },
// },
// { dark: true },
// ),
],
// extensions: [keymap.of(defaultKeymap)],
});
view = new EditorView({
state: startState,
parent: unref(editorRef),
});
};
const textType = (type: string) => {
// if (supportType.includes(type)) {
// // eval 将字符串转化为函数
// return eval(type + '()');
// } else {
// return keymap.of(defaultKeymap);
// }
return type == 'json'
? json()
: type == 'css'
? css()
: type == 'cpp'
? cpp()
: type == 'html'
? html()
: type == 'java'
? java()
: type == 'js' || type == 'ts'
? js()
: type == 'md'
? md()
: type == 'py'
? py()
: type == 'sql'
? sql()
: type == 'rust'
? rust()
: type == 'vue'
? vue()
: type == 'xml'
? xml()
: keymap.of(defaultKeymap);
};
//节流
let timer_throttle: any;
const throttle = (fn: Function, wait?: number) => {
wait = wait || 100;
if (!timer_throttle) {
timer_throttle = setTimeout(() => {
fn.apply(this);
timer_throttle = null;
}, wait);
}
};
const handFontSize = (value: string) => {
let cmContent = document.querySelector('.cm-content');
if (cmContent) {
cmContent.style.fontSize = value + 'px';
}
};
const handTheme = (value: string) => init();
const handLineNumber = (value: boolean) => init();
const handReadOnly = (value: boolean) => init();
const saveFile = () => {
console.log(view?.state.doc.toString());
saveTextAsFile(view?.state.doc.toString(), 'newFile.' + props.fileType);
};
</script>
<template>
<div class="flex-end mt-2 mb-2">
<a-space>
<a-button type="primary" v-if="data.readOnly" @click="saveFile">保存</a-button>
<a-select v-model:value="data.fontSize" class="w-px-100" @change="handFontSize">
<a-select-option value="12">12px</a-select-option>
<a-select-option value="14">14px</a-select-option>
<a-select-option value="16">16px</a-select-option>
<a-select-option value="18">18px</a-select-option>
</a-select>
<a-select v-model:value="data.theme" class="w-px-100" @change="handTheme">
<a-select-option value="default">默认</a-select-option>
<a-select-option value="dark">dark</a-select-option>
</a-select>
<a-switch
checked-children="显示行号"
un-checked-children="不显示"
v-model:checked="data.lineNumber"
@change="handLineNumber"
/>
<a-switch
checked-children="可编辑"
un-checked-children="不可编辑"
v-model:checked="data.readOnly"
@change="handReadOnly"
/>
</a-space>
</div>
<div ref="editorRef"></div>
<!-- <CodemirrorCodemirror :value="fileText" :fileType :lineNumber :readOnly :theme="data.theme" /> -->
</template>
<style lang="less" scoped></style>