先看效果
//预览编辑文本用
"@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"
<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>