日常一些优秀的代码、优化等知识的收集和汇总
短时间内多次触发同一事件,只执行最后一次,或者只执行最开始的一次,中间的不执行。
function debounce(fn, delay = 500) {
//timer是闭包里面的 重点重点
let timer = null;
return function () {
if (timer) clearTimeout(timer);
timer = setTimeout(function () {
fn.apply(this, arguments);
}, delay);
};
}
//1秒内页面多次点击只执行最后一次
document.addEventListener('click', debance(() => {
console.log('hahah');
}, 1000))
//节流的作用例如在拖拽获取坐标
function throttle(fn, delay = 100) {
//设置一个闭包里面的定时器
let timer = null;
return function () {
if (timer) return;
timer = setTimeout(() => {
fn.apply(this, arguments);
timer = null;
}, delay);
};
}
document.addEventListener('click', throttle(() => {
console.log('hahah');
}, 1000))
(1)、用hasOwnProperty
实现数组去重的方法:
hasOwnProperty
用于判定某个对象中是否有某个属性:
var o = { a: 1};
console.log(o.hasOwnProperty('a')); // true
console.log(o.hasOwnProperty('b')); // false
分析:本质是创建temp中间对象,unique中间数组,遍历原数组的每一项元素,获取元素的值,再查询对应的中间对象temp中是否有该属性,如果没有加添加进去,同时该元素值追加到中间数组中。
var arr = ['a', 'b', 'c', 'b'];
function uniqueArr(arr){
var unique = [], temp = {};
for(var i = 0; i < arr.length; i++){
if(temp[arr[i]] === undefined){
temp[arr[i]] = 1;
unique.push(arr[i]);
}
}
return unique;
}
console.log(uniqueArr(arr));// ["a", "b", "c", "d"]
(2)、利用indexof()
indexof方法查询是否有该元素,如果没有,则返回-1,有则返回相应的索引号。
let arr = ['a', 'b', 'c', 'd', 'a', 'b']
let newArr = []
function getOnly(arr) {
for (let i = 0; i < arr.length; i++) {
if (newArr.indexOf(arr[i]) == '-1') {
newArr.push(arr[i])
}
}
return newArr
}
console.log(getOnly(arr));// ["a", "b", "c", "d"]
(3)、利用set数据类型
let arr = ['a', 'b', 'c', 'd', 'a', 'b']
let newArr = new Set(arr)
console.log(newArr);// ["a", "b", "c", "d"]
function deepClone(obj = {}) {
let res
if (typeof obj !== "object" || obj === null) {
// 如果不是对象或数组直接返回
return obj;
}
if (obj instanceof Array) {
// 如果为数组
res = [];
} else {
res = {};
}
for (key in obj) {
// 判断是否是obj的key
if (obj.hasOwnProperty(key)) {
res[key] = deepClone(obj[key]);//递归
}
}
return res;
}
obj1 = {
a: 1,
b: 2,
c: 3
}
// console.log(obj1.hasOwnProperty('a'));
let obj2 = deepClone(obj1)
console.log(obj1);//{a: 1, b: 2, c: 3}
console.log(obj2);//{a: 1, b: 2, c: 3}
obj1.d = 4
console.log(obj1);//{a: 1, b: 2, c: 3, d: 4}
console.log(obj2);//{a: 1, b: 2, c: 3}
请参考地址:https://blog.csdn.net/marendu/article/details/103733286
配置多个代理 只需让你本地请求,满足代理的规则即可
proxy:{
"/api": { //api是需要转发的请求
target: "http://www.xiongmaoyouxuan.com", //配置转发目标地址(能返回数据的服务器地址)
// ws: true,
changeOrigin: true //控制服务器接收到的请求头中host字段的值
pathRewrite: {
"^/api": "/api" //去除请求前缀,保证交给后台服务器的是正常请求地址(必须配置)
}
},
"/user": {
target: "http://www.xiongmaoyouxuan.com",
// ws: true,
changeOrigin: true
pathRewrite: {
"^/user": "/api"
}
},
在创建axios的时候,beseURL这样配置
const ajax = axios.create({
baseURL:"/",
timeout: 6000,//请求超时时间
})
创建的请求
export function getData() { //get
return request({
url: 'api/search/home',
method: 'GET'
})
}
export function getData1() { //get
return request({
url: 'user/search/home',
method: 'GET'
})
}
原文链接:https://blog.csdn.net/marendu/article/details/103733286
实际操作补充:
当有多个跨域存在时:
代理处:
proxy:{
"/api": {
target: "http://www.xiongmaoyouxuan.com/api",
// ws: true,
changeOrigin: true
pathRewrite: {
"^/api": ""
}
},
"/user": {
target: "http://www.baidu.com/user",
// ws: true,
changeOrigin: true
pathRewrite: {
"^/user": ""
}
},
利用一个公共方法对/api和/user和后续路径拼接,此处略。
使用到beforeEach 钩子函数进行验证操作,to.matched会得到所有的路由,数组形式,通过循环matched这个数组,some()方法筛选验证。
meta字段(元数据) 直接在路由配置的时候,给每个路由添加一个自定义的meta对象,在meta对象中可以设置一些状态,来进行一些操作。用它来做登录校验再合适不过了
{
path: '/actile',
name: 'Actile',
component: Actile,
meta: {
login_require: false
},
},
{
path: '/goodslist',
name: 'goodslist',
component: Goodslist,
meta: {
login_require: true
},
children:[
{
path: 'online',
component: GoodslistOnline
}
]
}
这里我们只需要判断item下面的meta对象中的login_require是不是true,就可以做一些限制了
router.beforeEach((to, from, next) => {
if (to.matched.some(function (item) {
return item.meta.login_require
})) {
next('/login')
} else
next()
})
原文链接:https://blog.csdn.net/cofecode/article/details/79181894
语法:
element.scrollIntoView(); // 等同于element.scrollIntoView(true)
element.scrollIntoView(alignToTop); //布尔参数
element.scrollIntoView(scrollIntoViewOptions); //对象参数
参数:
(1)、alignToTop [可选],目前之前这个参数得到了良好的支持 true:元素的顶部将对齐到可滚动祖先的可见区域的顶部。对应于scrollIntoViewOptions: {block: "start", inline: "nearest"}。这是默认值 false 元素的底部将与可滚动祖先的可见区域的底部对齐。对应于scrollIntoViewOptions: {block: "end", inline: "nearest"}。
(2)、scrollIntoViewOptions [可选],目前这个参数浏览器对它的支持并不好,可以查看下文兼容性详情 behavior:[可选]定义过渡动画。"auto","instant"或"smooth"。默认为"auto"。 block:[可选] "start","center","end"或"nearest"。默认为"center"。 inline:[可选] "start","center","end"或"nearest"。默认为"nearest"。
用法:
场景模拟:
1、需要切换的tab栏,写在数组里面用,span包裹遍历;
2、实际tab栏,用div包裹,所有div取一个同样的class类名;
3、点击span时获取index;
4、获取所有div的元素,document.getElementsByClassName("类名");
5、滚动到指定div处,
document.getElementsByClassName("类名").[index].scrollIntoView({behavior:"smooth});
// 1、新建xiaos的实例
const instance = axios.create({
baseURL: 'http://152.136.185.210:7878/api/m5',
timeout: 5000
})
// 2、请求的拦截
instance.interceptors.request.use(config => {
// console.log(config);
return config
})
// 3、响应的拦截
instance.interceptors.response.use(res => {
// console.log(res);
return res.data
})
// 4、导出request封装
export default{
get:(url,params)=>{
return instance.get(url,{
params,
})
},
post:(url,params)=>{
return instance.post(url,params)
}
}
使用时直接调用对应的get()或者post(),附加对应的路径和参数进行请求。
此处写关于打开和关闭弹框时的缩放动画。
1、用一个盒子box包住弹框的内容content;
2、给这个content设置一个动态style,用isshow控制这个动画开始和结束的style,默认为false;
3、style写transform:scale(1)和transform:scale(0),true是1,false是0;
4、content内部写样式transition过渡给transform,设置200毫秒(根据需求来)
5、弹框打开时为了有效果,在mounted中200毫秒后把isshow设置为true,即内容content会从0变为1,即从无到有,过渡时间为200毫秒;
6、关闭的时候类似,box弹框可以马上关闭,但是isshow控制content的消息设置200毫秒时间,把isshow改为false,如此就有打开和关闭的动画效果;
四种配置方式:暂时只写方式,不写具体的配置内容
const path = require("path");
// const ElementPlus = require("unplugin-element-plus/webpack");
const AutoImport = require("unplugin-auto-import/webpack"); //element-plus按需导入
const Components = require("unplugin-vue-components/webpack"); //element-plus按需导入
const { ElementPlusResolver } = require("unplugin-vue-components/resolvers"); //element-plus按需导入
module.exports = {
// 几种修改配置的方式:
// 第一种:使用cli特有的属性修改配置
outputDir: "build",
// 第二种:使用configureWebpack对象包裹和webpack完全一致的属性配置,最后会使用webpack-merge进行合并的
configureWebpack: {
resolve: {
alias: {
components: "@/components"
}
},
plugins: [
// 按需导入element-plus组件的样式表
require("unplugin-element-plus/webpack")({
// 引入的样式的类型,可以是css、sass、less等,
importStyle: "css",
useSource: true
}),
AutoImport({
resolvers: [ElementPlusResolver()]
}),
Components({
resolvers: [ElementPlusResolver()]
})
]
}
// 第三种:使用configureWebpack函数形式,暴露原有配置参数,下面对配置重新覆盖操作
// configureWebpack: (config) => {
// config.resolve.alias = {
// "@": path.resolve(__dirname, "src"),
// components: "@/components"
// };
// },
// 第四种:使用chainWebpack函数来链式操作进行修改
// chainWebpack: (config) => {
// config.resolve.alias
// .set("@", path.resolve(__dirname, "src"))
// .set("components", "@/components");
// }
};
常见的配置信息:
module.export={
publicPath:"/",//配置应用程序部署的子目录(默认是 `/`,相当于部署在 `https://www.my-app.com/`);
outputDir:"dist",//编译打包存放的目录默认dist
lintOnSave: false,// 如果你不需要使用eslint,把lintOnSave设为false即可
configureWebpack:{
//警告webpack的性能提示
performance:{
hints:"warning",
maxEntrypointSize: 2048000, // build时静态文件和入口文件大小设置
maxAssetSize: 1024000,//生成文件的最大体积
}
},
//本地开发调试配置(不会被打包进去)
devServer:{
disableHostCheck:false,//vue本地服务不能被外部IP访问的解决方法
hot:true,//模块热替换机制开启
inline:true,//开启inline刷新模式,自动刷新
//跨域代理
proxy:{
"/server":{
target:"http://www.ali.com/server",
changeOrigin:true,
pathRewrite:"",//注意此处重写不写是因为target已经写了,如果上面不写此处要写。
//(调用的地址必须再次拼接上这个参数,改了配置必须重启)
}
}
}
}
懒加载:又叫延时加载,即在需要的时候进行加载,随用即载
使用懒加载的原因:
像vue这种(spy)单页面应用,如果没有使用到懒加载,webpack打包的文件过大,造成进入首页时,加载的资源过多,时间过长,即使做了loading也不利于用户体验,而运用懒加载可以将页面进行划分,需要的时候加载页面,可以有效的分担首页所承担的加载压力,减少首页加载事件,简单来说就是进入首页不用一次加载过多资源造成时间过长。
懒加载的使用方法:
(1)、ES 提出的import方法:
最常用:const HelloWorld = ()=>import('需要加载的模块地址'),
const HelloWorld = () => import("@/components/HelloWorld")
{
path: '/',
name: 'HelloWorld',
component:HelloWorld
},
(2)、vue异步组件实现懒加载 :
方法如下:component:resolve=>require(['需要加载的路由的地址'],resolve)
{
path: '/',
name: 'HelloWorld',
component:resolve=>require(['@/components/HelloWorld'],resolve)
},
(1)、& 表示当前本身,可以理解为且
(2)、+ 表示下一个兄弟:
(3)、~ 表示兄弟,不限定是下一个
举例:如果有10个div,每个都取名.box,现在想设置除了第一个没有margin-top,其余都要有margin-top,则可以如下设置。
.box{
& + .box{
margin-top:10px;
}
}
//这个css设置会施加到每个.box上,每一个本身的下一个兄弟.box都加外边距,只有第一个.box没有上一个兄弟,因此本身就不会有外边距设置。此方法等同于用先设置所有,再去除第一个。
(3)、:not(:nth-child(2)) 表示除去第二个的元素
官方地址:https://router.vuejs.org/zh/guide/advanced/scroll-behavior.html
使用前端路由,当切换到新路由时,想要页面滚到顶部,或者是保持原先的滚动位置,就像重新加载页面那样。 vue-router
能做到,而且更好,它让你可以自定义路由切换时页面如何滚动。
用法示例:
const router = new VueRouter({
routes,
scrollBehavior(to, from, savedPosition) {
// console.log(to, from, savedPosition);
console.log(to, from);
if (savedPosition) {
return savedPosition
} else if (to.path == "/home") {
return { x: 0, y: 200 }
} else {
return { x: 0, y: 800, behavior: "smooth" }
}
}
});
scrollBehavior接收三个参数,to的路由,from的路由,以及savedPosition
当且仅当 popstate
导航 (通过浏览器的 前进/后退 按钮触发) 时才可用。
savedPosition是点击浏览器前进后退导航时才触发,本身打印出来是对应的位置{x:...,y:...},x和y分别是切换前的位置信息。
上述例子中当路由切换时触发scrollBehavior滚动,当点击浏览器导航键时返回对应的原始位置。当跳转到home时,滚动到y为200的地方,注意此时是直接跳转的。其余情况跳转到y为800处,注意加了behavior:"smooth",表示平滑滚动,带过渡动画。
以上是基础用法,实际可以根据路由的元信息等或者keep-alive等做判断进行回滚。
思路:选出不相同的,剩下的都是相同。
情形一:简单的对象数据,即属性值是简单数据类型。
function commonObject(obj1,obj2){
let obj1keys = Object.keys(obj1);
let obj2keys = Object.keys(obj2);
for (let i = 0; i <= obj1keys.length - 1; i++) {
let key = obj1keys[i];
// if(条件) continue //可以设置一些条件不做判断跳过,如数据的id之类的
if (!obj2keys.includes(key)) return false; //只要有一个属性名不一致就是不相同
if (obj2keys.includes(key) && obj1[key] !== obj2[key]) return false; //当属姓名相同但是属性值不同就不相同(只要有一个就可以做判断)
}
return true; //上面把不同的情况选出来了,执行到这里就是相同的对象
}
let a = { sex: "男", age: 18 };
let b = { sex: "男", age: 18 };
console.log(this.commonObject(a, b));//true
默认传给后端的格式是application/json的,但是像表格文件上传之类的需要设置成multipart/form-data格式的,因此请求头数据类型就要进行修改,并用formData类型参数,就需要转换。
asiox一共请求方式有get,post,put,put,delete get,post,put,put有三个参数,url,data和config,所以在使用时,可以写成axios.method (‘url’,data,config),但是delete只有两个参数:url和config,data在config中,所以需要写成 axios.delete(‘url’,{data:{id:1}})。 ———————————————— 版权声明:本文为CSDN博主「badboy__biubiubiu」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/kjssjj12/article/details/106410399
**方式一:**封装的请求可以根据自己的习惯进行,比如直接拿最直接的:
function UploadDoc(data){
instance({
url:"",
method:"",
ContentType:"multipart/form-data",//此为请求类型
data:data,
...
})
}
上述方法在修改ContentType时可以直接设置,在请求拦截时判断不加这个属性就默认为多少,加了按加的执行即可。
**方式二:**还有是对请求的方法直接封装的,如下(详情看第8要点):
post:(url,params)=>{
return instance.post(url,params)
}
此方法对方法进行了封装,但是其实可以传递第三个参数:
post:(url,params,type)=>{
//根据参数判断是否设置特殊的请求头
if(type){
return instance.post(url,params,{
headers:{"Content-Type":"multipart/form-data"}//请求数据类型限制
})
}else{
return instance.post(url,params)
}
}
设置完上述的请求数据类型之后,我们就要对传递的数据类型进行转换成formData:
请求的config内有一个方法transformRequest,就是用来转换formData用的,我们可以在方法内部去执行转换的操作;
一般我们都是写在请求拦截里面的,我们可以根据自己传递的自定义的参数去判断哪些请求需要去做类型的转换。
方式一中请求的过程:
//封装的方法中
//假定data参数为{file:file.raw}
function UploadDoc(data){
instance({
url:"",
method:"",
ContentType:"",//此为请求类型
data:data,
isUpload:true,//该属性用于确认该请求是否需要转换数据类型
})
}
//请求拦截中(其余多余代码省略,假定参数为config)
//判断需要转换
if(config.isUpload){
//执行transformRequest方法,内部会执行,此data为请求传递的参数,并非config
config.transformRequest = data=>{
const formData = new FormData();//创建formData对象
formData.append("file",data.file);//追加进formData对象中
return formData ;//返回formData对象
//以上三个步骤就是转换成formData的过程,内部会自动把data参数转换成formData对象类型
}
delete config.isUpload //移除用于做判断的自定义属性
}
return config
方式二的请求:
//调用方法处(此处的request为下方导出post请求的文件)
import request from "request.js"
request.post(url,data,true);//假定data参数为{file:file.raw},第三个参数为true
//封装的方法中
//type为自定义用来标识是否需要改请求类型的,假定type=true
post:(url,params,type)=>{
//根据参数判断是否设置特殊的请求头
if(type){
params.isUploading = true ;//用于作为需要调整请求参数的数据类型成formData的标识
return instance.post(url,params,{
headers:{"Content-Type":"multipart/form-data"}//请求数据类型限制
})
}else{
return instance.post(url,params)
}
}
//请求拦截的文件中(其余多余代码省略,假定参数为config)
//判断是否需要转换数据类型
if(config.data.isUpload){
//执行transformRequest方法,内部会执行,此data为请求传递的参数,并非config
config.transformRequest = data=>{
const formData = new FormData();//创建formData对象
formData.append("file",data.file);//追加进formData对象中
return formData ;//返回formData对象
//以上三个步骤就是转换成formData的过程,内部会自动把data参数转换成formData对象类型
}
delete config.data.isUpload //移除标识
}
return config
以上就是在进行上传表单等文件需要转换请求类型和请求数据参数类型的请求过程。
案例地址:https://blog.csdn.net/xingbipai5492/article/details/89704781
注意案例中定义了两个一样的数组作为表头遍历,博主是为了演示行和列分别拖拽,实际操作只需要定义一个数组用于遍历表头!
表格拖拽利用sortablejs插件;
利用插件的onEnd函数,接收开始和结束的index索引,然后对索引对应的元素进行替换,行程最终拖拽的效果。
<template>
<div style="width:800px">
<el-table :data="tableData"
border
row-key="id"
align="left">
<el-table-column v-for="(item, index) in dropCol"
:key="item.prop"
:prop="item.prop"
:label="item.label">
</el-table-column>
</el-table>
//下面是演示数据变化
<pre style="text-align: left">
{{dropCol}}
</pre>
<hr>
<pre style="text-align: left">
{{tableData}}
</pre>
</div>
</template>
<script>
import Sortable from 'sortablejs'
export default {
data() {
return {
dropCol: [
{
label: '日期',
prop: 'date'
},
{
label: '姓名',
prop: 'name'
},
{
label: '地址',
prop: 'address'
}
],
tableData: [
{
id: '1',
date: '2016-05-02',
name: '王小虎1',
address: '上海市普陀区金沙江路 100 弄'
},
{
id: '2',
date: '2016-05-04',
name: '王小虎2',
address: '上海市普陀区金沙江路 200 弄'
},
{
id: '3',
date: '2016-05-01',
name: '王小虎3',
address: '上海市普陀区金沙江路 300 弄'
},
{
id: '4',
date: '2016-05-03',
name: '王小虎4',
address: '上海市普陀区金沙江路 400 弄'
}
]
}
},
mounted() {
// 阻止默认行为
document.body.ondrop = function (event) {
event.preventDefault();
event.stopPropagation();
};
this.rowDrop()
this.columnDrop()
},
methods: {
//行拖拽
rowDrop() {
//行拖拽是在tbody内发生的(所有的tr都在tbody内)
const tbody = document.querySelector('.el-table__body-wrapper tbody')
const _this = this
Sortable.create(tbody, {
onEnd({ newIndex, oldIndex }) {
const currRow = _this.tableData.splice(oldIndex, 1)[0]//注意splice返回的是被删除元素的数组
_this.tableData.splice(newIndex, 0, currRow)
}
})
},
//列拖拽
columnDrop() {
//列拖拽是在tr内发生的(所有的表头都在一个tr内)
const wrapperTr = document.querySelector('.el-table__header-wrapper tr')
this.sortable = Sortable.create(wrapperTr, {
animation: 180,
delay: 0,
onEnd: { newIndex, oldIndex } => {
const oldItem = this.dropCol[oldIndex]
this.dropCol.splice(oldIndex, 1)
this.dropCol.splice(newIndex, 0, oldItem)
}
})
}
}
}
</script>
ul内的li拖拽同理
flexible.js这个插件会根据你的需求去将屏幕分成指定的份数,每一份都是1个rem,对应html的font-size的大小;
正常是10份,我们可以改内部的代码根据需求指定分数;
一般还搭配vscode插件pxtorem或者也叫cssrem,这个插件能帮助我们在输入对应的px尺寸时 下方会提示可选转换对应的rem值,
我们可以根据自己的需求在这个插件设置中指定font-size大小即一个rem的大小。
举个例子:
设计稿为1920的屏幕,当前某个盒子是80px高,我们把屏幕分成24份,那么80px显然就是1rem,此时我们电脑为1280的屏幕,24份也就是53.3px为1rem,那么我们实际在自己电脑中开发就得写53.3px而不是80px,但是我们是根据设计稿的px高度去开发的,显然不可能每个高度都转成自己计算rem值,因此我们可以在cssrem插件中把1rem设置成80px,实际写样式我们依旧按设计稿写80px,系统会识别这个80px为1rem,我们就选择下方这个1rem,最后实际的效果这个1rem是基于我们1280屏幕的,也就是53.3px,这样就避免了我们实际开发手动去计算80px对应当前屏幕下的尺寸,我们依旧按原设计图去开发就行。
常见的正则网址:http://c.runoob.com/front-end/854
知识点:^表示以什么开头,$表示以什么结尾,但是当在类型前面直接写^就表示取反。
^\d{n}$ :表示匹配n位的数字;
/[^\d]/g :表示匹配不为数字的内容
案例1:输入框只能输入整数(包含正负整数和0)
let str;
let first = val.substr(0,1);//先判断第一个是否为负号
if(first==="-"){
str = val.replace(/[^\d]/g,"");//把所有不为数字的替换为空
str = "-" + str;//前面补负号
}else{
str = val.replace(/[^\d]/g,"")
}
案例:只允许输入两位小数
function clearNumber(num){
num = num.replace(/[^\d.]/g,'');//所有不是数字.结尾的都替换为空
num = num.replace(/^\./g,'');//上面的值再把所有以.开头的都替换为空
num = num.replace(/\.{2,}/g,'.');//继续替换,再把所有大一一个的.都替换为一个.
return num
}
案例1:点击元素之外的区域调用事件
//(1)、在js文件中:
import Vue from 'vue'
//自定义指令-点击div区域之外触发
// 提交验证
Vue.directive('clickOutside', {
// 初始化指令
bind(el, binding) {
function clickHandler(e) {
// 这里判断点击的元素是否是本身,是本身,则返回
if (el.contains(e.target)) {
return false;
}
// 判断指令中是否绑定了函数
if (binding.expression) {
// 如果绑定了函数 则调用那个函数,此处binding.value就是handleClose方法
binding.value(e);
}
}
// 给当前元素绑定个私有变量,方便在unbind中可以解除事件监听(这个__vueClickOutside__是自定义的变量名)
el.__vueClickOutside__ = clickHandler;
document.addEventListener('click', clickHandler);
},
update() { },
unbind(el) {
// 解除事件监听
document.removeEventListener('click', el.__vueClickOutside__);
delete el.__vueClickOutside__;
},
});
//(2)、在main.js文件中:
//自定义指令引入(全局引入)
import './directive/directive'
//(3)、在引用页面(outsideClick方法内写需要执行的逻辑)
<div v-click-outside="outsideClick"></div>
案例2:权限按钮的自定义指令
//(1)、封装
import Vue from "vue"
Vue.directive("premission",{
inserted(el,binding){
const action = binding.value.action;
if(!action){
el.parentNode.removeChild(el)
}
}
})
//(2)、调用
<el-button v-premission={action:true/false}></el-button>
案例3:vue3全局指令-格式化时间
//(1)、封装
import dayjs from "dayjs";
export default function (app) {
let format = "YY:MM:DD HH:mm:ss"; //设置默认的时间格式化格式
app.directive("format-time", {
created(el, bingings) {
if (bingings.value) {
format = bingings.value; //获取用户定义的时间展示格式
}
},
mounted(el) {
const textContent = el.textContent; //获取当前值
let timestamp = parseInt(textContent); //转成数值格式
if (timestamp.length == 10) {
timestamp = timestamp * 1000; //如果后台给的是秒转换成毫秒
}
el.textContent = dayjs(timestamp).format(format); //利用dayjs进行格式转换
},
});
}
//(2)、调用
<span v-format-time:info="'YY:MM:DD HH:mm:ss'">1637910658504</span>
正常情况我们是用v-for遍历渲染表格;
//渲染内容的代码:
<template slot-scoped="scoped">
.....里面写我们的字段循环
</template>
//渲染表头的的溢出隐藏数据(与上面并列写)
<template slot="header" slot-scoped="slot">
//表头盒子(创建鼠标经过监听事件,用来计算文本的offsetWidth和scrollWidth,比较大小确定是否启用悬浮)
<div class="header_box" @mouseover.stop="getHaderTooltip(slot.column.id)">
//悬浮区(根据比较的大小动态控制disabled的值,控制悬浮的启用)
<el-tooltip :disabled="isShowTooltip" class="item" :content="slot.column.label" placement="top">
//表头(动态设置ref)
<div :ref="slot.column.id" class="long_title">{{slot.column.label}}</div>
</el-tooltip>
</div>
</template>
//鼠标移入比较可视区可盒子的offsetWidth和scrollWidth,注意scrollWidth包括溢出的大小
getHaderTooltip(idRef){
const contentOffsetWidth = this.$refs[idRef][0].offsetWidth;
const contentScrollWidth = this.$refs[idRef][0].scrollWidth;
if(contentOffsetWidth < contentScrollWidth){
this.isShowTooltip = false;
}else{
this.isShowTooltip = true;
}
}
getBoundingClientRect用于获取某个元素相对于视窗的位置集合。集合中有top, right, bottom, left等属性。
//获取到DOM元素 el
rectObject = el.getBoundingClientRect();
//返回值为对象,里面有关于视口位置的属性:
rectObject.top:元素上边到视窗上边的距离;
rectObject.right:元素右边到视窗左边的距离;
rectObject.bottom:元素下边到视窗上边的距离;
rectObject.left:元素左边到视窗左边的距离;
rectObject.width:元素的宽度;
rectObject.height:元素的高度;
//可以根据top值是否大于0动态classList.add()和classList.remove()添加和移除类名,来设置定位吸顶
目前支持修改默认的滚动条样式的只有IE/Chrome/Firefox:
IE:只支持下面滚动条的样式修改。
scrollbar-arrow-color: color; /*三角箭头的颜色*/
scrollbar-face-color: color; /*立体滚动条的颜色(包括箭头部分的背景色)*/
scrollbar-3dlight-color: color; /*立体滚动条亮边的颜色*/
scrollbar-highlight-color: color; /*滚动条的高亮颜色(左阴影?)*/
scrollbar-shadow-color: color; /*立体滚动条阴影的颜色*/
scrollbar-darkshadow-color: color; /*立体滚动条外阴影的颜色*/
scrollbar-track-color: color; /*立体滚动条背景颜色*/
scrollbar-base-color:color; /*滚动条的基色*/
Firefox(火狐):
scrollbar-color : 设置滚动条轨道和拇指的颜色
scrollbar-width :设置滚动条出现时的厚度
例:
颜色:
scrollbar-color: auto;
scrollbar-color: dark;
scrollbar-color: light;
或其他颜色,第一个是滑块颜色,第二个是轨道颜色
scrollbar-color: green #fff;
厚度:
scrollbar-width:none/thin/auto
auto 系统默认的滚动条宽度
thin 系统提供的瘦滚动条宽度,或者比默认滚动条宽度更窄的宽度
none 不显示滚动条,但是该元素依然可以滚动
Chrome(谷歌):
::-webkit-scrollbar: 整个滚动条.
::-webkit-scrollbar-button : 滚动条上的按钮 (上下箭头).
::-webkit-scrollbar-thumb : 滚动条上的滚动滑块.
::-webkit-scrollbar-track : 滚动条轨道.
::-webkit-scrollbar-track-piece : 滚动条没有滑块的轨道部分.
::-webkit-scrollbar-corner : 当同时有垂直滚动条和水平滚动条时交汇的部分.
::-webkit-resizer : 某些元素的corner部分的部分样式(例:textarea的可拖动按钮).
例:
整体部分必须写
/*定义滚动条宽高及背景,宽高分别对应横竖滚动条的尺寸*/
::-webkit-scrollbar {
width: 10px; /*对垂直流动条有效*/
height: 10px; /*对水平流动条有效*/
}
/*定义滚动条的轨道颜色、内阴影及圆角*/
::-webkit-scrollbar-track{
-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3);
background-color: rosybrown;
border-radius: 3px;
}
/*定义滑块颜色、内阴影及圆角*/
::-webkit-scrollbar-thumb{
border-radius: 7px;
-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3);
background-color: #E8E8E8;
}
/*定义两端按钮的样式*/
::-webkit-scrollbar-button {
background-color:cyan;
}
/*定义右下角汇合处的样式*/
::-webkit-scrollbar-corner {
background:khaki;
}
import {Loading} from "element-ui";
let _loading;
let loadingType = true;
//1、创建loading实例
export const showLoading = () => {
if(!loadingType) return
_loading = Loading.service({
lock:true,
body:true,
text:"正在加载中......",
background:"rgba(255,255,255,.5)",
target:"#app",
fullscreen:false,
customClass:"custom-loading"
});
loadingType = false;
}
//2、关闭loading实例
export const hideLoading = () => {
_loading.close();
loadingType = true;
}
1、子组件CustomAutoComplete.vue:
<template>
<el-autocomplete
v-model="modelData"
:placeholder="placeHolder"
:disabled="disabled"
oninput="value = value.replace(/[^a-zA-Z0-9\u4e00-\u9fa5]/g,'')"
:value-key="valueKey"
:fetch-suggestions="querySearch"
@select="handleSelect"
@focus="$emit('focus', $event)"
>
<template slot-scope="{ item }" v-if="isTempalte">
<div class="name">{{ item.id }}</div>
<span class="addr">{{ item.name }}</span>
</template>
</el-autocomplete>
</template>
<script>
export default {
name: 'CustomAutoComplete',
// 用于自定义组件的v-model的传递事件和值
model: {
prop: 'data',
event: 'blur',
},
props: {
// 该值和model的值一致,此处接收,上面是申明
// 1、绑定值
data: {
required: true,
},
// 2、占位
placeHolder: {
type: String,
default: '请输入内容',
},
// 3、是否禁用状态
disabled: {
type: Boolean,
default: false,
},
// 4、是否成采用自定义模板
isTemplate: {
type: Boolean,
default: false,
},
// ......其余可根据实际需求传递参数
},
data() {
return {
modelData: this.data, //组件内显示的值
};
},
computed: {
// 输入建议对象中用于显示的键名(可根据实际需求去写)
valueKey() {
return id; //根据需求去设置获取的数据中显示的字段,不设置默认展示全部返回的字段数据
},
},
watch: {
// 此处监听modelData,只要变化即传递给父组件,同步更新变化值,防止子组件改变了值但是父组件未同步改变
// 用blur是因为model设置了v-model的同步事件是blur
modelData(val) {
this.$emit('blur', val);
},
},
methods: {
// 联想搜索的方法(返回输入建议的方法,仅当你的输入建议数据 resolve 时,通过调用 callback(data:[]) 来返回它)
// 第一个参数是输入的内容,第二个是请求完成后需要执行的回调函数
querySearch(queryString, callback) {
// 1、 ...此处可以对参数进行自定义的调整,如多个地方使用该组件但是请求不一样,可以设置判断并传递不同的参数
// 2、发送请求,请求数据res
// 3、执行回调函数,下拉显示该数据
callback(res);
},
// 选择显示的数据,并传递给父组件
handleSelect(item) {
this.$emit('handleSelectBack', item);
},
},
};
</script>
<style></style>
2、父组件Father.vue:
<template>
<CustomAutoComplete
v-model="query.autoData"
:disabled="true"
placeHolder="请输入"
:isTemplate="false"
@handleSelectBack="selectAuto"
@focus="getFocus"
/>
</template>
<script>
import CustomAutoComplete from './CustomAutoComplete.vue';
export default {
name: 'Father',
components: { CustomAutoComplete },
data() {
return {
query: {
autoData: '',
},
};
},
methods: {
// 接收选择的下拉选项
selectAuto(item) {
// ...此处可以根据需求进行自定义的选择,当同一个页面多个地方同时接收但是展示的不一样,就可以自行选择
this.$set(this.query, 'autoData', item.id);
},
// 子组件中传递出来的focus事件,我们可以根据需求在该事件中做一些自定义的操作
getFocus() {},
},
};
</script>
<style>
</style>
module.exports = {
// 配置打包入口
chainWebpack: config => {
// 发布模式:发布环境(运行)的打包路口配置
config.when(process.env.NODE_ENV === 'production', config => {
config.entry('app').clear().add('./src/main-prod.js')
// 配置externals不打包信息
// (注意:左侧多单词名称用-连接时会报错,需再加引号)
config.set('externals', {
vue: 'Vue',
'vue-router': 'VueRouter',
axios: 'axios',
lodash: '_',
echarts: 'echarts',
nprogress: 'NProgress',
'vue-quill-editor': 'VueQuillEditor',
})
// 配置发布模式下的isProd
config.plugin('html').tap(args => {
args[0].isProd = true //生产环境做标识,下面用于设置title
// args[0].title = '电商系统' //当然此处也可以直接写
return args
})
})
// 开发模式:开发环境的打包路口配置
config.when(process.env.NODE_ENV === 'development', config => {
config.entry('app').clear().add('./src/main-dev.js')
// 配置开发模式下的isProd
config.plugin('html').tap(args => {
args[0].isProd = false //开发环境做标识,下面用于设置title
// args[0].title = 'dev-电商系统' //当然此处也可以直接写
return args
})
})
},
lintOnSave: false
}
html模板文件中:
<title>
<%= htmlWebpackPlugin.options.isProd ? '' : 'dev - ' %>电商后台管理系统
</title>
const routeFiles = require.context('../pages', true, /\.vue/) // 读取views文件夹下面所有的.vue文件
routeFiles.keys().forEach(item => {
// console.log(item)
if (item.indexOf('component') === -1) { // component下面为子组件不需要注册路由
let info = item.split('.')
routes.push({
path: info[1],
component: routeFiles(item).default
})
}
})
案例:城市数组,转换成树结构
let treeList = [{
title: "广东省",
id: "100000",
parentId: null
}, {
title: "深圳市",
id: "100100",
parentId: '100000'
}, {
title: "广州市",
id: "100200",
parentId: '100000'
}, {
title: "江西省",
id: "200000",
parentId: null
}, {
title: "上饶市",
id: "200100",
parentId: "200000"
}, {
title: "南昌市",
id: "200200",
parentId: "200000"
}, {
title: "德兴市",
id: "200300",
parentId: "200000"
}, ]
//转换结果如下(由于复制原因下面显示的是json格式,结果是对象格式,下面格式请忽略):
[
{
"title": "广东省",
"id": "100000",
"parentId": null,
"children": [
{
"title": "深圳市",
"id": "100100",
"parentId": "100000"
},
{
"title": "广州市",
"id": "100200",
"parentId": "100000"
}
]
},
{
"title": "江西省",
"id": "200000",
"parentId": null,
"children": [
{
"title": "上饶市",
"id": "200100",
"parentId": "200000"
},
{
"title": "南昌市",
"id": "200200",
"parentId": "200000"
},
{
"title": "德兴市",
"id": "200300",
"parentId": "200000"
}
]
}
]
//方法一:先把省和市分开,再合并
function composeCity(data) {
let father = [];
let son = [];
data.forEach(item => {
if (item.parentId == null) {
father.push({
...item,
children: []
});
} else {
son.push(item);
}
});
son.map(item => {
father.forEach(list => {
if (item.parentId === list.id) {
list.children.push(item)
}
})
})
return father
}
let citys = composeCity(treeList);
//方法二:遍历并过滤追加市区,再过滤提取省份
function transTree(treeList) {
treeList.forEach(list => {
let result = treeList.filter(item => list.id == item.parentId)
if (result.length > 0) {
list.children = result
}
})
let root = treeList.filter(item => item.parentId == null)
return root
}
transTree(treeList)
疑问:一个页面里面点击打开了一个新的窗口,然后在新的窗口里面保存成功数据之后需要更新前面一个窗口的数据?应该怎么实现呢?
window.postMessage() 方法可以安全地实现跨源通信。通常,对于两个不同页面的脚本,只有当执行它们的页面位于具有相同的协议(通常为https),端口号(443为https的默认值),以及主机 (两个页面的模数 Document.domain设置为相同的值) 时,这两个脚本才能相互通信。
window.postMessage() 方法提供了一种受控机制来规避此限制,只要正确的使用,这种方法就很安全。
从广义上讲,一个窗口可以获得对另一个窗口的引用(比如 targetWindow = window.opener),然后在窗口上调用 targetWindow.postMessage() 方法分发一个 MessageEvent 消息。
接收消息的窗口可以根据需要自由处理此事件。传递给 window.postMessage() 的参数(比如 message )将通过消息事件对象暴露给接收消息的窗口。 ———————————————— 版权声明:本文为CSDN博主「凯小默」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/kaimo313/article/details/107773642
实例:
demo1.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>demo-1</title>
</head>
<body>
<div id="demo-1">demo-1页面</div>
<button id="btn">点击跳转到demo-2</button>
<script>
// 添加点击事件
document.getElementById('btn').addEventListener('click', () => {
window.open('./demo-2.html');
});
// 监听window的message窗口信息传递事件(回调函数中接收信息,相当于事件总线的接收)
window.addEventListener('message', event => {
// 我们能信任信息来源吗?
if (event.origin !== window.location.origin) {
alert('我们不能信任信息来源!!!!!')
return;
}
console.log(event);
alert(event.data.message)
})
</script>
</body>
</html>
demo2.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>demo-2</title>
</head>
<body>
<div id="demo-2">demo-2页面</div>
<button id="save">保存</button>
<script>
// 添加点击事件
document.getElementById('save').addEventListener('click', () => {
// 传递窗口信息(类似事件总线的发送)
window.opener.postMessage({
name: 'demo-2',
message: '凯小默保存成功啦啦啦啦!!!!'
}, window.location.origin);
});
</script>
</body>
</html>
详情参考地址:https://www.cnblogs.com/watercaltrop/articles/4689279.html
const textarea = document,createElement("textarea");
let str = "这是要复制的内容";
textarea.value = str;
document.body.appendChild(textarea);
textarea.select();
document.execCommand("Copy");
textarea.remove()
<template>
<div class="download">
<p>这是下载测试的页面</p>
<el-upload
class="avatar-uploader"
action="https://jsonplaceholder.typicode.com/posts/"
:show-file-list="false"
:on-change="change"
:auto-upload="false"
>
<img v-if="imageUrl" :src="imageUrl" class="avatar" />
<i v-else class="el-icon-plus avatar-uploader-icon"></i>
</el-upload>
<button @click="download">下载</button>
</div>
</template>
<script>
export default {
data() {
return {
imageUrl: "",
base64: "",
};
},
methods: {
change(file) {
// URL.createObjectURL接收的参数是blob或者file对象,注意我们上传文件的file.raw本身就是blob文件流
this.imageUrl = URL.createObjectURL(file.raw);//转成可识别的url,前缀是blob:
const fileReader = new FileReader();
// fileReader.readAsDataURL接收的参数是blob或者file对象,注意我们上传文件的file.raw本身就是blob文件流
fileReader.readAsDataURL(file.raw);//转换成base64的编码
// 由于readAsDataURL是异步的,需要在onload事件中执行自定义的逻辑
fileReader.onload = (e) => {
this.base64 = e.target.result.split(",")[1];//拿到的是带类型前缀的base64,此处移除前缀
};
},
download() {
// 有些浏览器没有msSaveBlob方法,此处需要判断
// msSaveBlob可识别base64直接下载,此处也可以直接用blob对象,如file.raw
if (window.navigator?.msSaveBlob) {
//ie浏览器
window.navigator.msSaveBlob(this.base64, "这是下载文件.jpg");
} else {
// 不支持的用a链接执行可识别的blob的url进行下载
const a = document.createElement("a");
a.href = this.imageUrl;//注意这个地址不能是base64,可以是blob的url,不可以是blob对象本身
a.download = "这是下载文件.jpg";
const evn = document.createEvent("HTMLEvents");//创建元素的事件类型
evn.initEvent("click", true, true);//初始化元素上的点击事件
a.click();//监听事件,可以用addEventLinstener写
a.dispatchEvent(evn);//触发这个事件
}
},
},
};
</script>
download() {
const blobObject = new Blob(["这是一段测试的下载文本"],{type:'txt'});//创建一个blob文件流对象,可以自定义type文件类型
if (window.navigator?.msSaveBlob) {
//msSaveBlob中的参数可以是base64的文件编码也可以是blob文件对象
window.navigator.msSaveBlob(blobObject, "msSaveBlob_testFile.txt");
} else {
// 不支持的用a链接执行可识别的blob的url进行下载
const url = URL.createObjectURL(blobObject);
const a = document.createElement("a");
a.href = url; //注意这个地址不能是base64,可以是blob的url,不可以是blob对象本身
a.download = "下载文本.txt";
const evn = document.createEvent("HTMLEvents"); //创建元素的事件类型
evn.initEvent("click", true, true); //初始化元素上的点击事件
a.click(); //监听事件,可以用addEventLinstener写
a.dispatchEvent(evn); //触发这个事件
}
},
完整模拟用bloburl或blob对象下载文件及缓存:
<template>
<div>
这是测试文件下载的模块
<el-upload
class="upload-demo"
action="https://jsonplaceholder.typicode.com/posts/"
:on-change="handleChange"
:file-list="fileList"
:auto-upload="false"
>
<el-button size="small" type="primary">点击上传</el-button>
</el-upload>
<button @click="download" style="margin: 20px">下载</button>
</div>
</template>
<script>
export default {
data() {
return {
fileList: [], //文件列表数据
fileBlobUrl: "", //bloburl的临时文件地址
name: "", //文件名
base64: "", //文件对象的base64码
blob: null, //blob文件对象
};
},
methods: {
// 上传(由于暂时没有后台,因此此处演示一下:
//(1)、blob对象=>bloburl
//(2)、blob对象=>base64=>blob
// )
handleChange(file, fileLists) {
// 获取bloburl地址
this.fileBlobUrl = URL.createObjectURL(file.raw);
// 获取base64地址
const fileReader = new FileReader();
fileReader.readAsDataURL(file.raw);
fileReader.onload = (e) => {
this.base64 = e.target.result.split(",")[1]; //把blob对象转为base64
this.blob = this.dataURLtoBlob(e.target.result); //把base64转为blob对象
};
this.name = file.name;
// 列表展示用
this.fileList.push({
name: file.name,
url: URL.createObjectURL(file.raw), //转换成src可识别的bloburl地址
});
},
// 下载:
// 正常情况如果使用bloburl下载的文件,这个bloburl临时地址会存在浏览器缓存;
// 因此下次再使用该地址去下载就不会去请求后台,直接从浏览器缓存中取这个文件;
// 当然如果手动清除了浏览器缓存的话,由于这个地址还存在,但是文件不存在,因此会下载空文件
download() {
if (window.navigator?.msSaveOrOpenBlob) {
// ie浏览器
// 情形一:根据bloburl地址去下载,需先根据地址请求到blob文件
const xml = XMLHttpRequest();
xml.open("GET", this.fileBlobUrl);
xml.responseType = "blob";
xml.onload = (res) => {
window.navigator.msSaveOrOpenBlob(res.response, this.name);
};
// 情形二:给到的参数就是blob文件对象,直接下载
// window.navigator.msSaveOrOpenBlob(this.blob, "测试");
} else {
// 其他浏览器
const aDom = document.createElement("a");
// 情形一:根据bloburl地址去下载,a标签可直接识别bloburl地址
// aDom.href = this.fileBlobUrl;
// 情形二:给到的参数就是blob文件对象,a标签不能识别,需转成bloburl地址
aDom.download = this.name; // 设置文件名
let href = URL.createObjectURL(this.blob); // 将file对象转成 UTF-16 字符串
aDom.href = href; // 放入href
document.body.appendChild(aDom); // 将a标签插入 body
aDom.click(); // 触发 a 标签的点击
document.body.removeChild(aDom); // 移除刚才插入的 a 标签
URL.revokeObjectURL(href); // 释放刚才生成的 UTF-16 字符串
}
},
// base64文件代码转换成blob对象(一般后台都会给base64代码去下载文件)
dataURLtoBlob(dataurl) {
var arr = dataurl.split(",");
var bstr = atob(arr[1]);
var n = bstr.length;
var mime = arr[0].match(/:(.*?);/)[1];
var u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
// return new Blob([u8arr], { type: mime }); //返回的就是blob对象(文件对象)
return new Blob([u8arr], "filename" { type: mime }); //参数 :文件内容、文件名、文件类型
},
},
};
</script>
<style lang="less" scoped>
</style>
封装添加水印的js函数:
const watermark = {};
const setWatermark = (str, element) => {
// 如果未指定加水印的盒子则返回
if (!element) {
return;
}
// 创建唯一的id作为水印盒子
const id = "1.23452384164.123412416";
// 如果已经存在时但是再次调用该方法则先移除原有的水印
if (document.getElementById(id) !== null) {
document.getElementById(element).removeChild(document.getElementById(id));
}
// 创建一个画布
const can = document.createElement("canvas");
// 设置画布的长宽
can.width = 120;
can.height = 120;
const cans = can.getContext("2d");
// 旋转角度
cans.rotate((-15 * Math.PI) / 180);
cans.font = "18px Vedana";
// 设置填充绘画的颜色、渐变或者模式
cans.fillStyle = "rgba(200, 200, 200, 0.40)";
// 设置文本内容的当前对齐方式
cans.textAlign = "left";
// 设置在绘制文本时使用的当前文本基线
cans.textBaseline = "Middle";
// 在画布上绘制填色的文本(输出的文本,开始绘制文本的X坐标位置,开始绘制文本的Y坐标位置)
cans.fillText(str, can.width / 8, can.height / 2);
// 创建水印盒子并添加属性
const div = document.createElement("div");
div.id = id;
div.style.pointerEvents = "none";
div.style.top = "0px";
div.style.left = "0px";
div.style.position = "absolute";
div.style.zIndex = "100000";
div.style.width = "100%";
div.style.height = "100%";
// div.style.width = document.documentElement.clientWidth + "px";//可根据需求是全局body加还是指定元素上加
// div.style.height = document.documentElement.clientHeight + "px";//可根据需求是全局body加还是指定元素上加
div.style.background =
"url(" + can.toDataURL("image/png") + ") left top repeat";
// 把水印元素div追加到指定的盒子内
document.getElementById(element).appendChild(div);
// id返回出去,用于后续判断该函数是否有返回值来确认是否创建水印盒子完成
return id;
};
// 该方法只允许调用一次(调用创建水印)
watermark.set = (str, element) => {
let id = setWatermark(str, element);
// 500毫秒内如果没创建成功就继续创建
setInterval(() => {
if (document.getElementById(id) === null) {
id = setWatermark(str, element);
}
}, 500);
// 窗口大小变化时重新加载水印
window.onresize = () => {
setWatermark(str, element);
};
};
export default watermark;
调用方法添加水印:
waterMask.set("这是水印",'box')
注意:由于水印盒子设置了绝对定位,因此需要加水印的盒子应该设置相对定位,请知悉!
1、安装插件**npm i docx-preview --save
**
2、调用该插件的renderAsync方法,接收两个参数,第一个是需要预览的doc文件的blob对象(注意不是bloburl),第二个参数是需要渲染到页面的dom元素,使用ref;
代码如下:实现预览和下载。
<template>
<div class="preview-doc">
<p>模拟上传来获取文件地址</p>
<div class="box">
<el-upload class="upload-demo" ref="upload" action="https://jsonplaceholder.typicode.com/posts/" :on-change="changeFile" :file-list="fileList" :auto-upload="false">
<el-button slot="trigger" size="small" type="primary">选取文件</el-button>
</el-upload>
<el-button @click="download" size="small" type="primary">下载</el-button>
<div class="doc-mask" v-if="showDoc">
<div class="doc-box">
<div class="doc-content">
<!-- 预览文件的地方(用于渲染) -->
<div ref="file"></div>
</div>
<div class="close">
<el-button @click="showDoc = false">关闭</el-button>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
// 引入docx-preview插件
let docx = require("docx-preview");
export default {
data() {
return {
fileList: [],
fileBlobUrl: "",
showDoc: false,
};
},
mounted() {
console.log("使用插件的renderAsync方法来渲染", docx); //
},
methods: {
// 文件改变时
changeFile(file, fileList) {
console.log(file, fileList);
// 获取bloburl地址
// this.fileBlobUrl = URL.createObjectURL(file.raw);
// console.log(this.fileBlobUrl);
this.blobFile = file.raw; //获取文件 blob对象
this.showDoc = true;
this.$nextTick(()=>{
docx.renderAsync(this.blobFile, this.$refs.file);
// 渲染到页面(注意第一个参数是blob对象而不是bloburl,第二个参数是要渲染的dom元素)
})
},
// 下载
download() {
// const blob = new Blob([this.fileBlobUrl]); // 把得到的结果用流对象转一下
// const blobURL = window.URL.createObjectURL(new Blob([this.blobFile]));//转成bloburl
const blobURL = window.URL.createObjectURL(this.blobFile); //转成bloburl
let a = document.createElement("a"); //创建一个<a></a>标签
a.href = blobURL; //赋值链接
a.download = "测试.docx"; //设置文件名
a.style.display = "none"; // 障眼法藏起来a标签
document.body.appendChild(a); // 将a标签追加到文档对象中
a.click(); // 模拟点击了a标签,会触发a标签的href的读取,浏览器就会自动下载了
a.remove(); // 一次性的,用完就删除a标签
},
},
};
</script>
<style lang="less">
.box {
display: flex;
.el-button {
height: 36px;
margin-right: 10px;
}
}
.doc-mask {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.3);
.doc-box {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
background-color: #fff;
display: flex;
flex-direction: column;
.doc-content {
width: 900px;
height: 700px;
overflow: auto;
}
.close {
margin: 10px auto;
}
}
}
</style>
1、下载pdf.js:文件夹pdf = build文件夹 + web文件夹 等;
2、把文件夹放到public文件夹下,作为静态文件复制到打包后的文件中;
3、使用iframe内嵌的方式去打开pdf文件或者使用window.open以新页面的形式预览pdf;
/*内嵌方式:*/
<iframe :src="./pdf/web/viewer.html?file=这里填对应的文件url" style="width:100%;hegiht:100;"></iframe>
//新页面方式
window.open("./pdf/web/viewer.html?file=这里填对应文件url",_blank);
具体完整引入和按需引入可以参考官网:https://element-plus.gitee.io/zh-CN/guide/quickstart.html#%E5%AE%8C%E6%95%B4%E5%BC%95%E5%85%A5
以上取至官网对于vue3的ui库element-plus的全部和按需导入的方式描述。
全部引入没有问题,但是按需导入经测试发现还是有问题的。
关于按需引入的问题:正常标签组件是没有问题的,采用了按需导入的方式,只需要在vue.config.js或者vite.config.ts中配置好对应的配置信息,在使用element-plus的组件,都可以直接用,并不需要再单独导入组件,样式也是,并不需要单独导入,但是有个问题就是非标签组件,如loading、message等反馈组件在通过按需导入使用时,该组件的样式文件并不会被导入进来,即缺少样式文件,导致样式失效。
因此针对按需导入的方式使用element-plus,就必须解决这些反馈组件样式文件未导入的问题。
具体描述亦可参考文章:https://xie.infoq.cn/article/e576f022653a7e0cb872038f1
百度查询了很多该问题,得到的解决方式都是使用unplugin-element-plus插件,该插件git地址:https://github.com/element-plus/unplugin-element-plus/blob/HEAD/README.zh-CN.md
从中可以看到该插件会根据导入的组件去自动导入该组件对应的样式文件,而组件的导入我们是通过按需加载的那两个插件实现了。
使用:
补充:
- 需要注意经测试发现其实引入的文件是scss的样式文件,因此就需要安装sass-loader、sass等依赖(直接安装即可,可能存在版本不兼容问题则降低版本)。
- 当然要是觉得按照官网的按需导入之后还需要这么麻烦去解决反馈组件的样式问题,那么也可直接在main.ts中导入全局的样式index.css,这样也可以,无非就是多导入了样式文件。
- 当然这只是仅限于当下版本的element-plus出现的问题而产生的解决方法,不排除后续版本更新修复官网按需导入所暴露的该问题,目前element-plus 2.0.2版本存在该问题
- 该文编著于2022.0219
- 另补充一点:之前网上有使用babel-plugin-import插件导入样式文件+手动遍历导入组件注册来替代官网的按需导入,但是由于element-plus新版本的原因导致目录结构等的变化,导致该插件来对组件的按需导入会出现异常,因此暂时无法使用该组件来实现按需导入样式表。
在后台管理系统中,由于不同用户拥有不同的操作权限,登录后所能操作和的菜单并不一样,因此就需要对路由进行动态控制。
思路:
- 后台返回用户所能访问的权限菜单,包含path路径(该路径需要前端提供,保证前后端一致,和路由的路径对应);
- 目录结构的设计(为了方便扩展和维护,设计好目录结构):
- 在utils中创建一个公共方法,用于处理收集全部导出的路由,并根据获取的菜单从全部导出的路由中筛选当前用户权限范围内的路由。
- 根据开发实际情况,调用上方导出的方法,来获取筛选到当前用户的权限范围内的路由,并把这些路由动态追加到路由对象中。
补充:附上最开始路由表
参考地址:https://mp.weixin.qq.com/s/XMZBpVyvC4wKrnj0j--49A
本实例具体是在vue3中+elemeng-plus实施的,中间会夹杂部分其他代码;
1.类型的文件代码(type.ts):
import { AxiosRequestConfig, AxiosResponse } from "axios";
/* 为了增加扩展性,我们让实例传递拦截器函数,也就是实例需要传递默认的AxiosRequestConfig
类型的参数,同时要具备拦截器中函数参数类型,因此此处需要进行一些操作,去扩展实例传递的
参数类型 */
// 1.汇总拦截器各函数的参数类型
interface ALRequestInterceptors<T = AxiosResponse> {
requestInterceptor?: (config: AxiosRequestConfig) => AxiosRequestConfig;
requestInterceptorCatch?: (error: any) => any;
responseInterceptor?: (config: T) => T;
responseInterceptorCatch?: (error: any) => any;
}
// 2.重构总的参数类型
interface ALRequestConfig<T = AxiosResponse> extends AxiosRequestConfig {
interceptors?: ALRequestInterceptors<T>;
showLoading?: boolean;
}
export { ALRequestConfig, ALRequestInterceptors };
2.对axios类封装完整的代码(index.ts):
// 对axios进行二次封装
import axios from "axios";
import { AxiosInstance } from "axios";
import type { ALRequestConfig, ALRequestInterceptors } from "./type";
import { ElLoading } from "element-plus";
import { LoadingInstance } from "element-plus/es/components/loading/src/loading";
import LocalCache from "@/utils/cache";
const DEFAULT_LOADING = true; //设置默认不加载loading
// 请求的封装类
class ALRequest {
//1.构建属性axios的实例及自定义的拦截器函数
instance: AxiosInstance;
interceptors?: ALRequestInterceptors;
showLoading: boolean;
loading?: LoadingInstance;
//2.构造器创建实例初始化公共参数
constructor(config: ALRequestConfig) {
this.instance = axios.create(config);
this.interceptors = config.interceptors;
this.showLoading = config.showLoading ?? DEFAULT_LOADING;
// 3.封装实例特有的拦截器
this.instance.interceptors.request.use(
this.interceptors?.requestInterceptor,
this.interceptors?.requestInterceptorCatch
);
this.instance.interceptors.response.use(
this.interceptors?.responseInterceptor,
this.interceptors?.responseInterceptorCatch
);
// 5.添加所有请求公共的拦截器
this.instance.interceptors.request.use(
(config) => {
const token = LocalCache.getCache("token") ?? "";
// 由于headers没有内置Authorization属性,因此前面加!强调有该属性,否则ts检测会报错
config.headers!.Authorization = `Bearer ${token}`;
// 加载loading;
console.log(this.showLoading);
if (this.showLoading) {
this.loading = ElLoading.service({
lock: true,
text: "Loading",
background: "rgba(0, 0, 0, 0.7)"
});
}
return config;
},
(error) => {
return error;
}
);
this.instance.interceptors.response.use(
(config) => {
this.loading?.close();
return config.data;
},
(error) => {
this.loading?.close();
return error;
}
);
}
//4.封装请求(细化到对单个请求的拦截器进行处理)
request<T = any>(config: ALRequestConfig<T>): Promise<T> {
return new Promise((resolve, reject) => {
// 1.如果单个请求携带了自定义的拦截器,则对参数进行拦截器处理
if (config.interceptors?.requestInterceptor) {
config = config.interceptors.requestInterceptor(config);
}
// 2.判断是否需要展示loading(注意此处是请求前,还未开始执行上面的请求拦截)
if (config.showLoading == false) {
this.showLoading = config.showLoading;
}
this.instance
.request<any, T>(config)
.then((res) => {
if (config.interceptors?.responseInterceptor) {
res = config.interceptors.responseInterceptor(res);
// 重置loading为true,这样不会影响下一个请求
this.showLoading = DEFAULT_LOADING;
}
resolve(res);
})
.catch((err) => {
// 重置loading为true,这样不会影响下一个请求
this.showLoading = DEFAULT_LOADING;
reject(err);
});
});
}
get<T = any>(config: ALRequestConfig<T>): Promise<T> {
return this.request({ ...config, method: "GET" });
}
post<T = any>(config: ALRequestConfig<T>): Promise<T> {
return this.request({ ...config, method: "POST" });
}
delete<T = any>(config: ALRequestConfig<T>): Promise<T> {
return this.request({ ...config, method: "DELETE" });
}
patch<T = any>(config: ALRequestConfig<T>): Promise<T> {
return this.request({ ...config, method: "PATCH" });
}
}
export default ALRequest;
3.创建的请求实例代码(index.ts):
import ALRequest from "./request";
import { BASE_URL, TIME_OUT } from "./request/config";
// 请求实例1:
const alRequest = new ALRequest({
baseURL: BASE_URL,
timeout: TIME_OUT,
interceptors: {
requestInterceptor: (config) => {
return config;
},
requestInterceptorCatch: (error) => {
return error;
},
responseInterceptor: (config) => {
return config;
},
responseInterceptorCatch: (error) => {
return error;
}
}
});
export { alRequest };
4.部分常量代码(config.ts):
// 定义公共的常量参数
let BASE_URL = "";
const TIME_OUT = 10000;
if (process.env.NODE_ENV == "development") {
BASE_URL = "http://152.136.185.210:5000";
}
if (process.env.NODE_ENV == "production") {
BASE_URL = "http://152.136.185.210:5000";
}
export { BASE_URL, TIME_OUT };
5.部分实际接口请求代码:
import { alRequest } from "../index";
import { IAccount, ILoginResult, IUserInfo } from "./types";
import { IDataType } from "../types";
// 定义枚举类:存放登录的请求api
enum LoginApi {
AccountLogin = "/login",
LoginUserInfo = "/users/", // /users/ + id
UserMenus = "/role/" // /role/ + role + /menu
}
// 1.账户登录接口
export function accountLoginRequest(account: IAccount) {
return alRequest.post<IDataType<ILoginResult>>({
url: LoginApi.AccountLogin,
data: account
});
}
6.补充点:取消请求
上述封装并没有包含参考地址的文章中的取消请求的内容,因此此处单独把这部分内容从文章中提取出来。
以下内容摘抄自文章。
- 准备工作:
我们需要将所有请求的取消方法保存到一个集合(这里我用的数组,也可以使用Map)中,然后根据具体需要去调用这个集合中的某个取消请求方法。
首先定义两个集合,示例代码如下:
// index.ts
import type {
RequestConfig,
RequestInterceptors,
CancelRequestSource,
} from './types'
class Request {
/*
存放取消方法的集合
* 在创建请求后将取消请求方法 push 到该集合中
* 封装一个方法,可以取消请求,传入 url: string|string[]
* 在请求之前判断同一URL是否存在,如果存在就取消请求
*/
cancelRequestSourceList?: CancelRequestSource[]
/*
存放所有请求URL的集合
* 请求之前需要将url push到该集合中
* 请求完毕后将url从集合中删除
* 添加在发送请求之前完成,删除在响应之后删除
*/
requestUrlList?: string[]
constructor(config: RequestConfig) {
// 数据初始化
this.requestUrlList = []
this.cancelRequestSourceList = []
}
}
这里用的CancelRequestSource
接口,我们去定义一下:
// type.ts
export interface CancelRequestSource {
[index: string]: () => void
}
这里的key
是不固定的,因为我们使用url
做key
,只有在使用的时候才知道url
,所以这里使用这种语法。
- 取消请求方法的添加与删除
首先我们改造一下request()
方法,它需要完成两个工作,一个就是在请求之前将url
和取消请求方法push
到我们前面定义的两个属性中,然后在请求完毕后(不管是失败还是成功)都将其进行删除,实现代码如下:
// index.ts
request<T>(config: RequestConfig): Promise<T> {
return new Promise((resolve, reject) => {
// 如果我们为单个请求设置拦截器,这里使用单个请求的拦截器
if (config.interceptors?.requestInterceptors) {
config = config.interceptors.requestInterceptors(config)
}
const url = config.url
// url存在保存取消请求方法和当前请求url
if (url) {
this.requestUrlList?.push(url)
config.cancelToken = new axios.CancelToken(c => {
this.cancelRequestSourceList?.push({
[url]: c,
})
})
}
this.instance
.request<any, T>(config)
.then(res => {
// 如果我们为单个响应设置拦截器,这里使用单个响应的拦截器
if (config.interceptors?.responseInterceptors) {
res = config.interceptors.responseInterceptors<T>(res)
}
resolve(res)
})
.catch((err: any) => {
reject(err)
})
.finally(() => {
url && this.delUrl(url)
})
})
}
这里我们将删除操作进行了抽离,将其封装为一个私有方法,示例代码如下:
// index.ts
/**
* @description: 获取指定 url 在 cancelRequestSourceList 中的索引
* @param {string} url
* @returns {number} 索引位置
*/
private getSourceIndex(url: string): number {
return this.cancelRequestSourceList?.findIndex(
(item: CancelRequestSource) => {
return Object.keys(item)[0] === url
},
) as number
}
/**
* @description: 删除 requestUrlList 和 cancelRequestSourceList
* @param {string} url
* @returns {*}
*/
private delUrl(url: string) {
const urlIndex = this.requestUrlList?.findIndex(u => u === url)
const sourceIndex = this.getSourceIndex(url)
// 删除url和cancel方法
urlIndex !== -1 && this.requestUrlList?.splice(urlIndex as number, 1)
sourceIndex !== -1 && this.cancelRequestSourceList?.splice(sourceIndex as number, 1)
}
- 取消请求方法
现在我们就可以封装取消请求和取消全部请求了,我们先来封装一下取消全部请求吧,这个比较简单,只需要调用this.cancelRequestSourceList
中的所有方法即可,实现代码如下:
// index.ts
// 取消全部请求
cancelAllRequest() {
this.cancelRequestSourceList?.forEach(source => {
const key = Object.keys(source)[0]
source[key]()
})
}
现在我们封装一下取消请求,因为它可以取消一个和多个,那它的参数就是url
,或者包含多个URL的数组,然后根据传值的不同去执行不同的操作,实现代码如下:
// index.ts
// 取消请求
cancelRequest(url: string | string[]) {
if (typeof url === 'string') {
// 取消单个请求
const sourceIndex = this.getSourceIndex(url)
sourceIndex >= 0 && this.cancelRequestSourceList?.[sourceIndex][url]()
} else {
// 存在多个需要取消请求的地址
url.forEach(u => {
const sourceIndex = this.getSourceIndex(u)
sourceIndex >= 0 && this.cancelRequestSourceList?.[sourceIndex][u]()
})
}
}
- 测试请求方法
现在我们就来测试一下这个请求方法,这里我们使用**www.apishop.net/**[3]提供的免费API进行测试,测试代码如下:
<script setup lang="ts">
// app.vue
import request from './service'
import { onMounted } from 'vue'
interface Req {
apiKey: string
area?: string
areaID?: string
}
interface Res {
area: string
areaCode: string
areaid: string
dayList: any[]
}
const get15DaysWeatherByArea = (data: Req) => {
return request<Req, Res>({
url: '/api/common/weather/get15DaysWeatherByArea',
method: 'GET',
data,
interceptors: {
requestInterceptors(res) {
console.log('接口请求拦截')
return res
},
responseInterceptors(result) {
console.log('接口响应拦截')
return result
},
},
})
}
onMounted(async () => {
const res = await get15DaysWeatherByArea({
apiKey: import.meta.env.VITE_APP_KEY,
area: '北京市',
})
console.log(res.result.dayList)
})
</script>
如果在实际开发中可以将这些代码分别抽离。
上面的代码在命令中输出
接口请求拦截
实例请求拦截器
全局请求拦截器
实例响应拦截器
全局响应拦截器
接口响应拦截
[{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}]
- 测试取消请求
首先我们在.server/index.ts
中对取消请求方法进行导出,实现代码如下:
// 取消请求
export const cancelRequest = (url: string | string[]) => {
return request.cancelRequest(url)
}
// 取消全部请求
export const cancelAllRequest = () => {
return request.cancelAllRequest()
}
然后我们在app.vue
中对其进行引用,实现代码如下:
<template>
<el-button
@click="cancelRequest('/api/common/weather/get15DaysWeatherByArea')"
>取消请求</el-button
>
<el-button @click="cancelAllRequest">取消全部请求</el-button>
<router-view></router-view>
</template>
<script setup lang="ts">
import request, { cancelRequest, cancelAllRequest } from './service'
</script>
- __dirname: 返回被执行的 js 所在文件夹的绝对路径
- __filename: 返回被执行的 js 的绝对路径
- process.cwd(): 总是返回运行 node 命令时所在的文件夹的绝对路径
- path.resolve() 或 path.resolve('') 或 path.resolve('./'): 跟 process.cwd() 一样
// 1.表格数据(可从后端接口获取):
const data = [];
// 2.需要合并的列字段:
const mergeColNames = ["itemCode", "itemName", "itemCode", "deptCodeName", "voltLevelName"];
// 3.基准字段:合并的基准列
const baseField = "itemCode";
// 4.调用方法处理后端获取的data:
mergeTableRow({ data, mergeColNames, baseField });
//处理data的方法:
function mergeTableRow(config) {
const config = {
data,
mergeColNames,
baseField,
};
// 无合并字段直接返回
if (!mergeColNames || mergeColNames.length === 0) {
return data;
}
// 遍历需要合并的字段
mergeColNames.forEach((m) => {
const mList = {}; //后续会用到,用于初始化标识
//遍历表格数据
data = data.map((v, index) => {
const rowVal = v[m]; //每一项data中合并字段的值
// 非初始化行数据
if (mList[rowVal] && mList[rowVal].newIndex === index) {
//非初始化紧邻的下一行数据,且需要合并(基准字段的值一样)
if (data[index][baseField] == data[index - 1][baseField]) {
mList[rowVal]["num"]++; //暂时无用
mList[rowVal]["newIndex"]++; // 当前列合并了,原初始化的可能合并的下一行的索引更新
data[mList[rowVal]["index"]][m + "-span"].rowspan++; //更新上一行中存储的合并行数据(上一行的合并行+!,当前行为0,属于被合并)
// 设置当前行被合并,行列都设置为0
v[m + "-span"] = {
rowspan: 0,
colspan: 0,
};
} else {
//非初始化紧邻的下一行数据,且无法合并,因为基准字段的值不一样(设置不变,行列都为1)
mList[rowVal] = { num: 1, index: index, newIndex: index + 1 };
v[m + "-span"] = {
rowspan: 1,
colspan: 1,
};
}
} else {
// 初始化时合并数据,默认都不合并,行列都为1
mList[rowVal] = { num: 1, index: index, newIndex: index + 1 };
// 第一次直接在原data中表示1/1不合并
v[m + "-span"] = {
rowspan: 1,
colspan: 1,
};
}
return v;
});
});
return data;
}
//在element-ui中table的span-method属性中设置执行方法:objectSpanMethod
objectSpanMethod({ row, column, rowIndex, columnIndex }) {
const span = column['property'] + '-span';//column['property']获取到每一列的字段
//判断每行数据中是否对该字段设置了合并的数据,如果有就进行合并
if(row[span]){
return row[span]
}
}
分析:
- 我们配置代理后,我们再调用时并不知道具体代理的结果,因为network完全看不出来;
- 当我们有一个基础baseurl后面拼接多个微服务进行开发时给,微服务对应的地址就需要进行代理,即配置很多代理;
注意:如果我们要配置代理,则axios的baseURL最好不要写域名和端口,因为代理的本质是使用本地的localhost:端口去代理请求目标服务器,如果axios已经配置了指定的域名和端口,当发现与本地的localhost:端口不一致时,则不会进行代理服务,因此,本地代理不要在axios配置域名和端口。
案例:我们模拟一个实际情况去分析。
假设本地开发,所有的接口都有一个基础的url,为/base,我们有2个微服务(实际情况可能多个,同理即可),为/system和/service,即实际接口路径为
- /base/system/接口地址;
- /base/service/接口地址;
- 我们需要对微服务的system和service进行代理,我们的计划是,使用proxy匹配到/system和/service,指定到target为指定的
域名:端口/base
,这样我们就保留了 base的基础路径,同时对微服务代理指定服务器; - 然后在pathRewrite中重写匹配的路径,根据实际需要进行处理,由于我们此处需要保留微服务的路径,因此重写的时候保留;
模拟代码如下:
module.exports = {
devServer:{
host: "0.0.0.0",//此处根据自己需要配置启动的本地服务,方便局域网其他电脑测试,此处我配置为0.0.0.0
port: 8888, //可以自定义端口号
//配置代理
proxy:{
"/system":{
target: "www.abc.com/base", // 假设代理目标服务器为www.abc.com
changeOrigin: true, //
secure: true, //允许代理到https
logLevel: "debug", //终端查看代理日志,终端会根据接口打印实际代理到的目标地址,方便调试,最好加上
//方式一:直接用对象
pathRewrite: {
"^/system": "/system",//保留该路径
},
//方式二:使用函数
//pathRewrite(path){
// return path.replace("/system","/system")
//}
},
"/service":{
target: "www.abc.com/base", // 假设代理目标服务器为www.abc.com
changeOrigin: true, //
secure: true, //允许代理到https
logLevel: "debug", //终端查看代理日志,终端会根据接口打印实际代理到的目标地址,方便调试,最好加上
//方式一:直接用对象
pathRewrite: {
"^/service": "/service",//保留该路径
},
//方式二:使用函数
//pathRewrite(path){
// return path.replace("/service","/service")
//}
},
}
}
}
更细的方法详情可以参考代理内置插件https://github.com/chimurai/http-proxy-middleware
1、先安装依赖:
第一个:将页面html转换成图片
npm install --save html2canvas
第二个:将图片生成pdf
npm install jspdf --save
2、考虑到封装性,我们可以通过js创建自定义组件,将方法挂载在Vue的原型链上:
定义全局函数…创建一个htmlToPdf.js文件在指定位置(增加dpi和scale)
// 导出页面为PDF格式
import html2Canvas from 'html2canvas'
import JsPDF from 'jspdf'
export default{
install (Vue, options) {
Vue.prototype.getPdf = function (dom,title) {
//var title = this.htmlTitle //DPF标题
//html2Canvas(document.querySelector('#pdfDom'), {
html2Canvas(dom, {
allowTaint: true,
taintTest: false,
useCORS: true,
//width:960,
//height:5072,
dpi: window.devicePixelRatio*4, //将分辨率提高到特定的DPI 提高四倍
scale:4 //按比例增加分辨率
}).then(function (canvas) {
let contentWidth = canvas.width
let contentHeight = canvas.height
let pageHeight = contentWidth / 592.28 * 841.89
let leftHeight = contentHeight
let position = 0
let imgWidth = 595.28
let imgHeight = 592.28 / contentWidth * contentHeight
let pageData = canvas.toDataURL('image/jpeg', 1.0)
let PDF = new JsPDF('', 'pt', 'a4')
if (leftHeight < pageHeight) {
PDF.addImage(pageData, 'JPEG', 0, 0, imgWidth, imgHeight)
} else {
while (leftHeight > 0) {
PDF.addImage(pageData, 'JPEG', 0, position, imgWidth, imgHeight)
leftHeight -= pageHeight
position -= 841.89
if (leftHeight > 0) {
PDF.addPage()
}
}
}
PDF.save(title + '.pdf')
}
)
}
}
}
3、在main.js中使用我们定义的函数文件
import htmlToPdf from '@/components/utils/htmlToPdf'
Vue.use(htmlToPdf)
4、在需要使用的模块调用我们的getPdf方法即可
<div class="row" id="pdfDom" style="padding-top: 55px;background-color:#fff;">
//给自己需要导出的ui部分.定义id为"pdfDom".此部分将就是pdf显示的部分
</div>
<button type="button" class="btn btn-primary" @click="getPdf(dom,title)">导出PDF</button>
<script>
//此处写伪代码
可以获取指定要要导出的dom节点,以及要命名的导出文件名称,传递给getPdf参数
</script>
本文来自转载。
原文链接:https://blog.csdn.net/qq_35371821/article/details/100291417
补充:
(1)关于使用html2Canvas时自定义canvas
考虑到转换之后清晰度的问题,我们可以自定义canvas来设置。
// 自定义canvas:
const getCanvasPng = async (tableName, title) => {
const table = document.querySelector(tableName);
const width = parseInt(getComputedStyle(table).width);
const height = parseInt(getComputedStyle(table).height);
// 自定义canvas设置精度
const DPR = 2;
const canvas = document.createElement("canvas");
canvas.width = width * DPR;
canvas.height = height * DPR;
const ctx = canvas.getContext("2d");
ctx.scale(DPR, DPR);
const canvasInfo = await html2Canvas(document.querySelector("#pdfDom"), {
canvas,
allowTaint: true,
taintTest: false,
useCORS: true,
});
let imgInfo = {
data: canvasInfo.toDataURL("image/png", 1.0),
width,
height,
};
let contentWidth = imgInfo.width;
let contentHeight = imgInfo.height;
let pageHeight = (contentWidth / 592.28) * 841.89;
let leftHeight = contentHeight;
let position = 0;
let imgWidth = 595.28;
let imgHeight = (592.28 / contentWidth) * contentHeight;
let pageData = imgInfo.data;
let PDF = new JsPDF("", "pt", "a4");
if (leftHeight < pageHeight) {
PDF.addImage(pageData, "JPEG", 0, 0, imgWidth, imgHeight);
} else {
while (leftHeight > 0) {
PDF.addImage(pageData, "JPEG", 0, position, imgWidth, imgHeight);
leftHeight -= pageHeight;
position -= 841.89;
if (leftHeight > 0) {
PDF.addPage();
}
}
}
PDF.save(title + ".pdf");
};
(2)关于pdf分页
正常如果内容比较多,我们就需要分页,毕竟不可能直接一页长信息给用户,那样也没法a4打印;
于是我们就需要设置换页,其实上方代码就已经根据position进行了分页,但是这种分页是对转换后的canvas图片在写入pdf时候根据边距分页的,那么就会导致分页空隙会把文本截断,很显然这不是我们想要的。
思路:
1.在需要分页的时候我们可以考虑不同页对应不同的canvas转成的图片;
2.于是我们可以根据需求在html2Canvas生成多个dom元素的图片;
3.我们可以在多个图片信息对象插入标识去确定是否需要换页;
4.遍历上面多个图片信息的数组,去PDF.addImage,如果需要换页的进行PDF.addPage(),最后导出save;
代码实现:
// 获取数据
getRecordsList(formData).then(res => {
if (res.code == 2000) {
// 第一个表格赋值,此处自定义 25个
let count = 25;
this.tableDataListFirst = res.body.slice(0, count);
// 后续表格暂时设置每32个一页
const tempData = res.body.slice(count);
if(tempData.length > 0){
const chunk = 32;
for(let i = 0, j = tempData.length; i < j; i += chunk){
this.tableDataListNext.push(tempData.slice(i, i + chunk));
}
}
this.timer = setTimeout(()=>{
this.$nextTick(()=>{
clearTimeout(this.timer);
this.getPdf('测试下载');
})
}, 500)
}
}).catch(() => {
this.downloadLoading = false;
})
// 把dom元素转换成canvas的图片并处理成png
async getCanvasPng(domName, changePage){
const dom = document.querySelector(domName);
let imgWidth, imgHeight, pageData;
await html2Canvas(dom, {
allowTaint: true,
taintTest: false,
useCORS: true,
//width:960,
//height:5072,
dpi: window.devicePixelRatio * 4, //将分辨率提高到特定的DPI 提高四倍
scale:4 //按比例增加分辨率
}).then(function (canvas) {
let contentWidth = canvas.width
let contentHeight = canvas.height
// let contentWidth = parseInt(getComputedStyle(dom).width);
// let contentHeight = parseInt(getComputedStyle(dom).height);
// canvas.style.width = contentWidth;
// canvas.style.height = contentHeight;
imgWidth = 595.28;
imgHeight = 592.28 / contentWidth * contentHeight;
pageData = canvas.toDataURL('image/jpeg', 1.0);
})
return {
data: pageData,
imgWidth,
imgHeight,
changePage
}
},
// 图片写入pdf
async getPdf(title){
this.$nextTick( async ()=>{
let PDF = new JsPDF('', 'pt', 'a4')
const pngData1 = await this.getCanvasPng('.first-page'); // pdf第一页
let imgArr = [pngData1];
await Promise.all(this.tableDataListNext.map(async (item, index)=>{
const pngDataNext = await this.getCanvasPng(`.list-table-${index}`, true);
imgArr.push(pngDataNext)
}))
for(const image of imgArr){
// const leftGap = 48;
// const topGap = 52;
const {data, imgWidth, imgHeight, changePage } = image;
changePage && PDF.addPage();
PDF.addImage(data, 'JPEG', 0, 0, imgWidth, imgHeight);
}
PDF.save(title + '.pdf');
this.downloadLoading = false;
})
}
(3)关于安装jspdf依赖异常的情况(不一定会出现,视情况而定))
经测试,会存在安装jspdf依赖实现导出pdf的时候,canvas转换的图片插入的pdf中,会存在很多黑幕的情况,因此必须想办法解决。
解决方法:不安装该依赖,直接采用导入jspdf.umd.js的脚本,来使用。
这就需要我们去动态控制script脚本的引入,以下为部分代码:
// 动态处理script脚本:
const url = config.env === "development" ? "" : "/dist"; //此处根据需求获取
const loadScript = (url) => {
// 为了方便才用promise
return new Promise((resolve, reject) => {
if (document.getElementsByClassName("js-jsPDF").length) {
return resolve(); //如果已经导入则退出
}
const script = document.createElement("script");
script.type = "text/javascript";
script.className = "js-jsPDF";
if (script.readyState) {
script.onreadystatechange = () => {
if (script.readyState === "loaded" || script.readyState === "complete") {
script.onreadystatechange = null;
resolve();
}
};
} else {
script.onload = () => {
resolve();
};
}
script.src = url;
document.getElementsByTagName("head")[0].appendChild(script);
});
};
//使用时
const {jsPDF} = jspdf;
const pdf = new jsPDF('','pt','a4');
我们在处理表格预览的时候,经常会用到一些ui库去帮助我们更好的渲染表格,例如element-ui等,但是有时候因为需求的原因,直接使用ui库的表格,并不能很好的实现我们的需求,或者说改起来会比较麻烦,所以我们就可以使用原生的table去实现,当然仅限于简单的却奇怪的展示。如某些类似打印的入职登记表等,
需要多出合并行列的表格。
考虑 md存放的图片是本地的地址,因此此处提供一张网上的地址,类似我们要实现表格的格式。
地址2:https://www.renrendoc.com/paper/89321741.html
本地图片:
<table>
<tr>
<td colspan="350">基本信息</td>
</tr>
<tr>
<td colspan="60">1</td>
<td colspan="80">2</td>
<td colspan="90">3</td>
<td colspan="120">4</td>
</tr>
<tr>
<td colspan="60" rowspan="12">5</td>
<td colspan="80" rowspan="12">6</td>
<td colspan="90" rowspan="4">7</td>
<td colspan="120">8</td>
</tr>
<tr>
<td colspan="120">9</td>
</tr>
<tr>
<td colspan="120">10</td>
</tr>
<tr>
<td colspan="120">11</td>
</tr>
<tr>
<td colspan="90" rowspan="4">12</td>
<td colspan="120">13</td>
</tr>
<tr>
<td colspan="120">14</td>
</tr>
<tr>
<td colspan="120">15</td>
</tr>
<tr>
<td colspan="120">16</td>
</tr>
</table>
<style lang="less" scoped>
table {
width: 700px;
border-collapse: collapse;
text-align: center;
table-layout: fixed;
}
tr td {
border-right: 1px solid #000;
border-top: 1px solid #000;
}
tr {
border-left: 1px solid #000;
border-bottom: 1px solid #000;
}
</style>
分析:
- 我们此处没有采用th表头,而是直接使用的tr和td;
- table最好加属性*border-collapse: collapse;*用于合并边框,*table-layout: fixed;*用于保证每个单元格td的长度是根据内容撑开的,基本都要加上;
- 给td加colspan属性,表示单元格占的水平的份数,即列的份数,这样可以根据不同的tr中的td占得份数不同,实现类似合并列的效果;
- 而给td加rowspan属性,表示单元格占得纵轴的份数,即行数,这样可以根据不同的tr中的td占得份数不同,实现类似合并行的效果;
- 具体实现可以参考上方的代码的例子。
有时候页面带有按钮、链接等等情况,我们要复制内容,并不好选择,这个时候我们可以打开控制塔,输入下面的代码,
document.designMode="on"
改变文档的设计模式,所有的文本都变为可编辑状态,直接复制,而不会出现按钮会点击到,链接会跳转等等情况。
在进行chrome断点调试时,我们可以在断点处点击右键,选择Edit breakpoint,设置对应的条件,这样就可以根据条件触发断点,像循环事件中就不会每一次都触发。
打开浏览器控制台,Command + Shift + P
可以打开快捷查找,可以下拉选项展示所有控制台的菜单项。
以掘金文章为例:
- 一边是内容区,一边是内容对应的标题菜单区域;
- 滚动内容区,标题菜单区域联动高亮;
- 点击菜单区域,内容区滚动到指定位置;
功能总结:内容区滚动导致标题区高亮+标题区点击导致内容区滚动。
下面基于Vue2来讲解下思路(Vue3同理):
说明:下面的部分变量,为了方便理解,暂时未挂载在Vue的data上,实际开发有些是必须要挂的,请知悉。
-
内容区和标题区分别封装成组件;
-
需要渲染成标题的数据,保证有唯一id;
-
内容区每一个对应的标题,进行如下操作:
- 设置ref为
titleRef
,方便后面统一获取ref元素; - 使用
data-id="id"
方式暴露每一个ref的标题id,方便后面遍历refs数组获取每一个ref的dataset.id
标题id; - 给每一个标题元素动态追加class类名,如
class="title-scroll-id"
,该类名主要用于点击标题区时使用scrollIntoView同步滚动内容区,后面会讲到。
- 设置ref为
-
在mounted中使用nextTick回调获取所有标题的ref元素,遍历ref数组,收集所有的标题元素距离顶部的距离(
offsetTop
)数据:mounted() { this.$nextTick(() => { const titleRefs = this.$refs.titleRef; // 存储元素距离顶部距离 const titlePosition = []; for(const item of titleRefs){ const id = item.dataset.id; titlePosition.push({ id, offsetTop: item.offsetTop }) } }) }
-
注册全局滚动事件(可根据实际需求注册),获取滚动距离顶部的长度scrollTop,以Vue为例,可以使用如下方式获取距离:
// 获取根元素 const rootDom = this.$root.$el; const scrollFn = () => { // const s_top = document.documentElement.scrollTop; const s_top = rootDom.scrollTop; } // 方式一: document.body.onscroll = scrollFn; // 方式二: rootDom.addEventListener('scroll', scrollFn);
-
扩展滚动事件的回调函数
scrollFn
,确定当前滚动位置所处的标题id:// 说明:遍历之前收集的titlePosition,根据当前滚动的距离去定位当前的id,需要考虑是否第一个、最后一个,及其余的情况; const scrollFn = () => { const s_top = rootDom.scrollTop; const len = titlePosition.length; const currentId = ''; // 当前滚动内容所属标题的id for(const [index, item] of titlePosition.entries){ const { id, offsetTop } = item; if(s_top > offsetTop){ if(index == len){ // 最后一个 currentId = id; }else { continue; } }else { if(index === 0){ // 第一个 currentId = titlePosition[index].id; }else{ // 其余 currentId = titlePosition[index -1].id; } } } }
-
得到
currentId
,可以使用emit方式发送给父组件,父组件更新传递给标题组件的标题id,标题区根据这个id高亮处理(根据类名高亮字体颜色,此处代码不再赘述); -
至此,内容区滚动所导致的标题区高亮功能完成。下面实现标题区点击导致内容区滚动。
-
该功能相较于上面比较简单,标题区点击,拿到标题id,发送给父组件,父组件获取到id,调用内容组件的根据id滚动到指定区域的方法即可:
titleScroll(id){ // 根据id获取点击的标题元素 const dom = document.querySelector(`title-scroll-${id}`); // 使用scrollIntoView圆滑滚动到点击的标题位置 dom.scrollIntoView({ behavior: 'smooth' }); }
以上就是完整的内容区域和标题区域同步功能介绍。
注意事项:点击所触发的titleScroll
方法,应避免执行scrollFn
回调,防止抖动,故需要自行引入一个点击滚动过程的标识,此处不再赘述,请知悉!
markdown-it
库对支持对md文件/内容转成html元素,进而支持通过html方式预览内容。
下面采用vue3的方式去介绍:
原理就是md内容使用插件markdown-it
处理成原始html元素,再用vue的v-html指令解读html内容,渲染到页面。
-
# 安装 yarn add markdown-it -D # 考虑到用ts,可安装声明文件 yarn add @types/markdown-it -D
-
基础使用:
// js中 import MarkdownIt from "markdown-it"; const md = MarkdownIt(); const res = ref(''); res.value = md.render('# 测试声明标题'); // template中 <span v-html="md"></span>
-
导入本地文件:
// js中 import mdData from "@/md/test.md?raw"; // 注意vue3中导入本地文件内容后面需要加?raw import MarkdownIt from "markdown-it"; const md = MarkdownIt(); const res = ref(''); res.value = md.render(mdData); // template中 <span v-html="md"></span>
-
以上就成功预览了md文件,但是会发现一些问题。
- 显示的内容,没有样式,只是单纯的h系列、pre、code、p、span、td、tr......等元素自带的样式;
- 代码块没有明显的区分、表格同样;
- 代码块某些变量等没有高亮;
显然,目前这种情况还无法达到需求。
-
追加样式:使用
github-markdown-css
库给md转换的html表上加上md文件大众化的样式。注意为了表明这是md文件的样式,需要改外层盒子加一个markdown-body的类名。// 安装 yarn add github-markdown-css -D // main.js文件导入markdown样式 import 'github-markdown-css';
-
高亮代码:使用
highlight.js
库去给围栏代码块应用语法高亮功能。该插件是扩展markdown-it
插件的,并不是单独使用。// 安装 yarn add highlight.js -D // js中 import MarkdownIt from "markdown-it"; import hljs from "highlight.js"; import mdData from "@/md/test.md?raw"; // 注意vue3中导入本地文件内容后面需要加?raw const md = MarkdownIt({ html: true, linkify: true, typographer: true, //扩展高亮插件 highlight: function (str: string, lang: any) { if (lang && hljs.getLanguage(lang)) { try { // return hljs.highlight(lang, str).value; // 也可以返回带标签的 return '<pre class="hljs">'+ '<code>' + hljs.highlight(lang, str, true).value + '</code>' + '</pre>'; } catch (__) { } } return '<pre class="hljs"><code>' + md.utils.escapeHtml(str) + "</code></pre>"; // 使用额外的默认转义 } }); const res = ref(''); res.value = md.render(mdData); // template中:注意外层必须包一个类名markdown-body,github-markdown-css中样式指定的父级类名 <div class="markdown-body"> <span v-html="res"></span> </div>
以上就是关于预览md文件的介绍。
参考文献:
https://www.npmjs.com/package/markdown-it https://blog.csdn.net/weixin_44090753/article/details/125313105 https://www.5axxw.com/questions/content/led33d https://markdown-it.docschina.org/#%E7%94%A8%E6%B3%95%E7%A4%BA%E4%BE%8B
说明:有时候我们需要控制某个字段输入的长度是字节数,而不是字符串长度,则需要根据字符的ascall码去识别占几个字节。
// 以下演示不能小于某个字节长度
let validCharCodeLen = (value) => {
if(value){
const len = value.length;
let minCharCodelen = 10; // 自定义限制字节的长度数
let currentLen = 0; // 当前累积的字节数
let isOk = false; // 标识是否符合要求
for(let i = 0; i< len; i++){
if(value[i].charCodeAt() <= 255){
// 小于255是一个字节
currentLen ++;
} else {
currentLen += 2;
}
if(currentLen >= minCharCodelen){
isOk = true;
break;
}
}
let msg = '';
isOk ? (msg = '通过') : (msg = '不通过');
return msg;
}
}
说明:flex布局时,如果横向排列时,一个盒子有宽度,另一个盒子添加flex: 1;
时就可以自动填充父元素的剩余宽度;那么竖向排列呢?我们可以给第一个盒子设置flex-grow: 0;
,第二个盒子设置flex-grow: 1;
,就能得到第二个盒子自动获取剩余父元素高度,当然有时候可能不生效,我们可以继续给第二个盒子设置height: 0;
即可。
此处不再贴代码,描述的很清晰!
一下提供三种方法,前面两种都有各自的问题,推荐使用第三种。
此处采用vue的代码模式展示:
template部分:
<div class="test-print">
<!-- 以下是实际内容 -->
<div class="print-area">
....
</div>
</div>
**css部分:**让需要打印的元素展示在页面看不到的地方
.print-area{
position: fixed;
left: 100%;
right: 100%;
}
**js部分:**注意print默认打印预览展示body的内容
-
方法一:直接替换原有的body内容
const body = document.body; const printArea = document.querySelector('.test-print'); let oldStr = body.innerHTML; body.innerHTML = printArea.innerHTML; window.print(); body.innerHTML = oldStr; location.reload();
问题:使用
body.innerHTML = oldStr;
还原页面信息时,只是单纯的页面dom元素展示还原,dom的事件会丢失,此时如果原本页面元素有注册事件,则恢复后原事件会失效,故需使用location.reload();
刷新页面,但刷新毕竟体验感不是很好。 -
方法二:选择新开page页,在新开的page页的body上赋值需要打印的元素,打印预览完成关闭page页
const printArea = document.querySelector('.test-print'); const newPage = window.open(); newPage.document.body.innerHTML = printArea.innerHTML; newPage.print(); newPage.close();
问题:该方法避免了方法一中的事件失效问题,因为自始至终都未改变当前页面的body内容。不过,新开page页同样会给用户一种不太丝滑的体验,如果不介意则可以用此方法。
-
方法三:使用iframe内嵌的方法,直接在当前页面使用iframe内嵌需要打印的html结构
需要额外补充html结构。
template部分:
<div class="test-print"> <!-- 以下是实际内容 --> <!-- 注意需要打印的dom元素的样式必须写在行内 --> <div class="print-area"> .... </div> </div> <iframe id="iframe" name="iframeName" style="display: none;"></iframe>
css部分:注意该样式在内嵌进iframe后会丢失,当然我们确实在iframe内嵌后也不需要这个样式
.print-area{ position: fixed; left: 100%; right: 100%; }
js部分:
const printArea = document.querySelector('.test-print'); document.getElementById('iframe').contentDocument.body.innerHTML = printArea.innerHTML; window.frames['iframeName'].print();
问题:该方法避免了上面两种方法所导致的体验丝滑感失去的问题,唯一需要强调的是,使用iframe做内嵌html展示打印元素时,打印元素需要展示的样式必须写在元素行内,当dom元素嵌入到iframe时,外部写的样式并不会生效。
原理:
- 前端根据需求绘制dom,渲染需要展示的pdf文件的格式内容;
- 使用
html2canvas
插件把指定的dom元素转成canvas画布,并得到对应图片的base64数据源; - 使用
jspdf
插件把图片base64数据源写入到pdf上,并导出,得到对应的pdf文件;
上面我们简单介绍了关于使用这两款插件导出pdf的原理,方便大家更直接的理解和使用。
下面我们将根据不同的业务场景去合理利用这两个插件导出我们需要的pdf文件,尤其是涉及如何根据实际情况选择不同的方式避免或者解决内容截断这个问题。
下面会先列出一种目前比较标准的导出pdf的过程,当然这个标准方法有一个致命的问题:当图片写入到pdf上并,自动换行时,就会出现内容截断的问题。
我们先说明下为什么会出现内容截断的问题:当我们使用html2canvas
把dom元素转成canvas对应的base64图片数据源时,我们紧接着是把图片渲染到pdf上。当这个图片内容足够长时,pdf一页的长度不够插入,这个时候就需要分页(这里其实也算不上自动分页,是我们调用pdf的api手动分页的),但是由于图片是连续的,它在一页的pdf上是从上到下渲染的,渲染到本页最下面时,就可能会出现一行文字只渲染到上半截部分,该页就写不了,剩下一半截文字只能渲染到下一页,这就是内容截断。
上面我们描述了下为什么会出现截断的情况,大家有个心理预期,后面我们会讲解如果去避免这个问题。
针对上面的描述,其实我们心理应该对导出pdf的过程有了一个清晰的认知,无非就是怎么调用这两个插件的api去完成我们的功能。
下面会列出一个目前比较标准的常规写法(以下是基于Vue的一些伪代码)。
js复制代码// 根据dom获取图片数据:注意纵向时A4尺寸是592.28*841.89,故横向时就是841.89*592.28
getCanvasToImage(dom, xGap = 80) {
if (typeof dom === 'string') dom = document.querySelector(dom);
return new Promise((resolve, reject) => {
html2canvas(dom, {
allowTaint: false,
tainTest: false,
useCORS: true,
dpi: window.devicePixelRatio * 2,
scale: 2
}).then(canvas => {
// 这是实际dom转canvas的画布尺寸
let contentWidth = canvas.width;
let contentHeight = canvas.height;
// 实际渲染到pdf上图片的尺寸(把canvas按比例缩放成A4的尺寸,考虑到我们需要保留左右边距)。以下纵向为例
let imgWidth = 592.28 - xGap; // 保留了总的横向xGap的边距,下面让图片在x轴上的xGap/2位置渲染就可保证在x轴居中
let imgHeight = imgWidth / contentWidth * contentHeight;
let imageData = canvas.toDataURL('image/jpg');
resolve({ imageData, imgWidth, imgHeight });
})
})
}
// 导出pdf标准模式(无顶部边距):注意我们的案例保留了左右边距
handleExportPDF() {
const dom = document.querySelector('.content-list'); // 获取实际需要渲染的dom
const xGap = 80; // 设置总的横向边距(包含左右)
this.getCanvasToImage(dom, xGap).then(({ imageData, imgWidth, imgHeight }) => {
let tempTotalHeight = imgHeight; // 临时变量 记录原imgHeight高度,实际还剩多少没绘制
let position = 0; // 临时变量 记录y轴位置
const PDF = new jsPDF('', 'pt', 'a4'); // 注意第一个参数为空是纵向,为l则横向
while (tempTotalHeight > 0) {
// 注意第三第四参数图片在PDF中x轴和y轴的位置,轴方向参考canvas,y轴下为正,上为负,轴心在当前页的左上角
// xGap/2保证x轴居中;
PDF.addImage(imageData, 'JPEG', xGap / 2, position, imgWidth, imgHeight);
position -= 841.89; // 每绘制完一页,图片在pdf轴中上移一页的y轴距离,好让需要绘制的下一页内容展示出来
// 计算剩余未绘制的图片高度:其实每页绘制的高度就是a4纸高度841.89
tempTotalHeight = tempTotalHeight - 841.89 ;
if (tempTotalHeight > 0) {
PDF.addPage(); // 当还有剩余图片未绘制完则分页,在下一页中绘制
}
}
PDF.save('测试.pdf');
});
}
// 只需要调用handleExportPDF即可
this.handleExportPDF();
上面是一个标准的导出pdf模式,为了pdf文件内容的优雅,我们特意设置了左右边距。这个标准导出过程,相信大家还是比较容易理解的。当然这种模式前面我们也说了,如果图片足够长,会出现内容截断问题,这一点需要明白。
上面的标准模式我们并没有设置顶部边距,而只是设置了左右边距。
下面我们就设置一下顶部边距,但是在此之前有一点需要说明,当图片内容比较长需要pdf做分页时,第二及后面的页我们无法设置顶部的距离,也就是说由于图片的绘制是连续的,我们只能保证第一页有顶部边距。
其实大家可以仔细想想就知道,我们在绘制的过程中,PDF.addImage第三第四的参数是图片在pdf坐标系的x和y位置,我们自始至终是上下左右移动图片,保证需要绘制的内容在当然pdf页的主区域来绘制。
之所以把设置第一页顶部边距的模式单独拎出来,主要是因为避免读者不好理解这个计算的过程。
js复制代码// 根据dom获取图片数据:注意纵向时A4尺寸是592.28*841.89,故横向时就是841.89*592.28
getCanvasToImage(dom, xGap = 80) {
if (typeof dom === 'string') dom = document.querySelector(dom);
return new Promise((resolve, reject) => {
html2canvas(dom, {
allowTaint: false,
tainTest: false,
useCORS: true,
dpi: window.devicePixelRatio * 2,
scale: 2
}).then(canvas => {
// 这是实际dom转canvas的画布尺寸
let contentWidth = canvas.width;
let contentHeight = canvas.height;
// 实际渲染到pdf上图片的尺寸(把canvas按比例缩放成A4的尺寸,考虑到我们需要保留左右边距)。以下纵向为例
let imgWidth = 592.28 - xGap; // 保留了总的横向xGap的边距,下面让图片则x轴上的xGap/2位置渲染就可保证在x轴居中
let imgHeight = imgWidth / contentWidth * contentHeight;
let imageData = canvas.toDataURL('image/jpg');
resolve({ imageData, imgWidth, imgHeight });
})
})
},
// 导出pdf(有顶部边距):注意我们的案例保留了左右边距和顶部边距
handleExportPDF() {
const dom = document.querySelector('.content-list');
const xGap = 80; // 设置总的横向边距(包含左右)
const topGap = 40; // 顶部边距(实际上当图片一次性绘制有分页时,该插件只能设置第一页的上边距,无法设置连续分页后其他页面顶部边距)
this.getCanvasToImage(dom, xGap).then(({ imageData, imgWidth, imgHeight }) => {
let tempTotalHeight = imgHeight; // 临时变量 记录原imgHeight高度,后面计算实际还剩多少没绘制
let position = 0; // 临时变量 记录y轴位置
let flag = false; // 临时变量 标识自动分页了
const PDF = new jsPDF('', 'pt', 'a4'); // 注意第一个参数为空是纵向,为l则横向
while (tempTotalHeight > 0) {
// 注意第三第四参数图片在PDF中x轴和y轴的位置,轴方向参考canvas,y轴下为正,上为负
// xGap/2保证x轴居中;
// topGap+position保证第一页有上边距topGap,该插件绘制pdf时都是一直绘制到当前页底部,自动分页绘制后面页时也都是从顶部绘制
// 这里有个点要清楚,每加一页position减841.89即上移一页的高度,因为当前页的841.89高度绘制完了,继续绘制剩余的部分
PDF.addImage(imageData, 'JPEG', xGap / 2, topGap + position, imgWidth, imgHeight);
position -= 841.89; // 每绘制完一页,图片在pdf轴中上移一页的y轴距离,好让需要绘制的下一页内容展示出来
// 计算剩余未绘制的图片高度:在有顶部边距的情况,如果是连续自动分页,则只有第一页绘制的高度是841.89-topGap,其余都是841.89高度,因为第一页顶部边距不算实际绘制的图片
tempTotalHeight = tempTotalHeight - 841.89 + (flag ? topGap : 0);
if (tempTotalHeight > 0) {
PDF.addPage();
flag = true;
}
}
PDF.save('测试.pdf');
});
},
// 只需要调用handleExportPDF即可
this.handleExportPDF();
在标准模式的基础上,我们又增加了首页顶部边距,同标准模式一样,如果图片高度很长需要分页,同样有截断问题的出现。当然,如果我们需要渲染的图片内容都不够绘制满一页,那么我们压根不需要考虑截断问题,因为不涉及分页,就不会出现截断情况,那么上面两种模式读者自行选择使用即可。
前面两种模式,当图片内容很高需要换行时,就会出现截断问题,那么我们应该如何避免内容被截断呢?
我们仔细想一想,jspdf
在绘制图片的时候是连续的,如果图片足够长就一定需要分页,一分页就不可避免要面对截断问题,那么我们是不是可以想办法让jspdf
每次绘制的图片都不超过一页A4(这里我们以A4举例)的高度?
没错,要解决这个问题,就需要我们限制html2canvas
获取的文本图片高度,我们可以把一个长dom拆分成多个dom去绘制多个canvas,并获取多个图片数据,再把多个图片分别绘制到一页pdf上,这要就避免了截断问题。
js复制代码// 根据dom获取图片数据:注意纵向时A4尺寸是592.28*841.89,故横向时就是841.89*592.28
getCanvasToImage(dom, xGap = 80) {
if (typeof dom === 'string') dom = document.querySelector(dom);
return new Promise((resolve, reject) => {
html2canvas(dom, {
allowTaint: false,
tainTest: false,
useCORS: true,
dpi: window.devicePixelRatio * 2,
scale: 2
}).then(canvas => {
// 这是实际dom转canvas的画布尺寸
let contentWidth = canvas.width;
let contentHeight = canvas.height;
// 实际渲染到pdf上图片的尺寸(把canvas按比例缩放成A4的尺寸,考虑到我们需要保留左右边距)。以下纵向为例
let imgWidth = 592.28 - xGap; // 保留了总的横向xGap的边距,下面让图片则x轴上的xGap/2位置渲染就可保证在x轴居中
let imgHeight = imgWidth / contentWidth * contentHeight;
let imageData = canvas.toDataURL('image/jpg');
resolve({ imageData, imgWidth, imgHeight });
})
})
},
// 导出pdf(指定不同dom):注意我们的案例保留了左右边距和每页的顶部边距
handleExportPDF() {
const doms = document.querySelectorAll('.content-list'); // 获取实际需要渲染的dom集合
const xGap = 80; // 设置总的横向边距(包含左右)
const topGap = 40; // 顶部边距(每一页的顶部边距)
Promise.all(Array.prototype.map(doms, dom => this.getCanvasToImage(dom, xGap))).then((imageArr, index) => {
imageArr.map(({ imageData, imgWidth, imgHeight }) => {
PDF.addImage(imageData, 'JPEG', xGap / 2, topGap, imgWidth, imgHeight);
// 不是最后一页都换页
if (imageArr.length - 1 !== index) {
PDF.addPage();
}
})
PDF.save('测试.pdf');
})
},
// 只需要调用handleExportPDF即可
this.handleExportPDF();
我们通过把最初的绘制一个dom的图片数据换成了绘制多个dom的图片数据,来规避因为图片过长导致分页截断的问题,现在我们保证了每一个dom的图片数据内容都只是在单页的pdf里面。
案例中我们用的是document.querySelectorAll('.content-list')
去获取集合,实际情况需要读者根据业务情况去处理,这里我就以我以前做过的一个案例去描述,当然下面只是描述,代码编写需要读者自行去操作,想必这并不能难倒大家。
案例:导出一个pdf,上面是一些汇总的文本和图片logo之类的,下面是一个数据量不确定的动态的table表格,采用的是element-ui的el-table渲染的,这里的表格每行的高度是相对固定的,只是行数不确定而已。
实现思路:
- 处理后台请求的数据,我会把table的数据处理,提取30条数据(读者可根据实际情况选择);
- 这30条数据单独用el-table渲染,和上面的logo及汇总的文本组成一个dom;
- 剩下的table数据,我进行了分组,每50条数据为一组,单独遍历用el-table渲染,这就动态的获取了dom集合,当然我们需要css控制非第一个el-table隐藏header;
- 然后用上面的模式把dom集合转成图片base64数据源并绘制到pdf上;
上面我简单介绍了一个特殊的情况,旨在于告知读者需要针对实际情况去灵活解决问题。
其实上面三种模式,已经可以解决绝大多数的用户导出pdf的场景了,那么为什么还有这里的第4种情况呢? 我们可以回头去看看第3种模式的分析过程,我们第3种模式的前提是dom集合中每个dom的高度是相对固定的,而且这个dom本身是不能太长的,如果太长的话以至于放不到一个pdf页面里面,那第3种模式依旧会出现截断问题。
举个例子,如果有一个dom元素,它是表格,这个表格是不标准的,有各种行合并和列合并,这个表格的高度是不确定的,甚至行合并的行高度都是不确定的,那么用第3种模式根本解决不了截断问题,因为你压根没办法指定某一个dom为第一页某一个为第二页,这种情况导出必然会导致表格的边框甚至内容被截断。
那么,我们又如何解决这种截断问题呢?
在此之前我想完善话我上面提到的例子,因为这就是我目前开发的一个业务场景。
例子:这个一个非常复杂的表格,这个表格支持编辑,有输入框、复选框、单选框,有很多行合并和列合并,行合并中,有按钮可以增加行和删除行,因此合并行中的行高不确定,有很多这种大模块,需要根据上面的单选确定渲染几个模块,根据单选和复选结果确定是否渲染新的输入框......,这是一个非常复杂的非常多字段的调查需求。当然为了方便导出,我是让非编辑状态下才支持导出,非编辑状态输入框字段我都渲染成了span,因为发现el-input在导出时会出现文字不对齐的情况,当然这是额外的说明,考虑到业务的复杂程度,笔者用的是原生的table去渲染的。
大家可以思考下这个例子,看看有什么比较好的实现导出pdf的思路,下面我就针对这个案例找出的第4种模式,其实也是第3种模式的延伸。
实现思路:
- 和第3种一样,避免截断的唯一方式就是让每一页pdf绘制的图片数据高度是不能超过一页pdf的容器高度的,从而保证不能因为图片过长而被动换行触发截断;
- 但是我们又没办法控制动态dom的高度,因此,我们可以手动指定一个变量控制我们一页pdf绘制的最大高度(不能超过pdf单页高度),这是我们能在一页pdf中所能绘制的最大实际图片渲染高度;
- 我们把整个大dom拆分成小非常多的dom;
- 把每一个dom单独转换成canvas的图片源数据,获取到图片数据源的集合(注意必须先转成图片源,因为实际绘制到pdf上的是图片源,我们要用实际图片源高度进行比较);
- 遍历这个集合,按顺序累加每一个图片的实际高度,和上面指定的最大高度比较,把累积高度后刚好不大于这个临界最大值的所有图片源分到一个组,从而得到n组图片数据源的集合,即一个二维数组,显然这个二维数组中的每一个元素,即图片源集合刚好绘制到一页pdf上,最终得到动态的pdf分页结果;
上面就整个实现过程进行了较为详细的阐述,相信大家应该都能理解,下面列出代码: 本案例中我是把tr作为单一dom,但是考虑到合并行有多个tr,此时如果也以tr为基准,会导致合并行tr中的文字(多行居中显示)会被不同的tr覆盖,以及合并行跨页边框被截断情况。因此合并行的多个tr外层有tbody包裹,让tbody作为单一dom,非合并行依旧以tr为单一dom,故我们只需要获取到table元素的children就可以得到所有的单一dom了。
js复制代码// 根据dom获取图片数据:注意纵向时A4尺寸是592.28*841.89,故横向时就是841.89*592.28
getCanvasToImage(dom, xGap = 80) {
if (typeof dom === 'string') dom = document.querySelector(dom);
return new Promise((resolve, reject) => {
html2canvas(dom, {
allowTaint: false,
tainTest: false,
useCORS: true,
dpi: window.devicePixelRatio * 2,
scale: 2
}).then(canvas => {
// 这是实际dom转canvas的画布尺寸
let contentWidth = canvas.width;
let contentHeight = canvas.height;
// 实际渲染到pdf上图片的尺寸(把canvas按比例缩放成A4的尺寸,考虑到我们需要保留左右边距)。以下纵向为例
let imgWidth = 592.28 - xGap; // 保留了总的横向xGap的边距,下面让图片则x轴上的xGap/2位置渲染就可保证在x轴居中
let imgHeight = imgWidth / contentWidth * contentHeight;
let imageData = canvas.toDataURL('image/jpg');
resolve({ imageData, imgWidth, imgHeight });
})
})
},
// 计算剩余未绘制的图片高度:在有顶部边距的情况,如果是自动分页,则只有第一页是绘制的高度是841.89-topGap,其余都是841.89高度
// 导出pdf(获取小dom集合,分组绘制到pdf上):注意我们的案例保留了左右边距和顶部边距
handleExportPDF() {
const domWrap = document.querySelectorAll('.detail-table'); // 获取最外层dom
const doms = domWrap.children;
const xGap = 80; // 设置总的横向边距(包含左右)
const topGap = 40; // 顶部边距(每一页的顶部边距)
let totalHeight = 0; // 累积的总高度
const tempMaxHeight = 750; // 最大高度,可根据需求自定义
let arrIndex = 0; // 二维数组元素的索引
const dyadicArrImages = []; //二维数组,里面每一个元素就是每一页pdf的图片源集合
// 获取每一个dom对应的图片源数据对象
Promise.all(Array.prototype.map(doms, dom => this.getCanvasToImage(dom, xGap))).map(temp => {
const { imgHeight } = temp;
totalHeight += imgHeight;
// 累积的总高度和最大绘制总高度比较
if (totalHeight > tempMaxHeight) {
arrIndex++;
}
// 追加二维数据元素
if (!dyadicArrImages[arrIndex]) dyadicArrImages[arrIndex] = [];
dyadicArrImages[arrIndex].push(temp);
})
// 遍历每一组
dyadicArrImages.map((arrImages, index) => {
// 绘制每一页
let position = topGap;
arrImages.map(({ imageData, imgWidth, imgHeight }) => {
// 注意这里是上移高度position,因为该层的遍历是在绘制同一页,绘制的一个图片源就需要移动图片源在pdf坐标轴的位置
PDF.addImage(imageData, 'JPEG', xGap / 2, position, imgWidth, imgHeight);
position += imgHeight; // 保证下一个图片源绘制在上一个图片源的下面
})
if (arrImages.length - 1 !== index) {
position = topGap; // 重置位置:新的一页从顶部开始
PDF.addPage(); //切下一页
}
})
PDF.save('测试.pdf');
},
// 只需要调用handleExportPDF即可
this.handleExportPDF();
上面详细的描述了关于设置了pdf最大实际绘制高度时的自动绘制,并分页,避免截断的模式。 当然上面的案例是基于我的实际业务场景设计的获取元素的方式,统一在table的children中获取,读者可针对各自的实际业务场景调整适合自己的获取dom方式。
作者:黍离_ 链接:https://juejin.cn/post/7359833391191425065 来源:稀土掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
在Vue项目中,有时候我们需要用第三方库,但是由于一些原因,我们没有直接用npm包管理工具去使用第三方库,而是直接下载了对应第三方库的js文件,那我们就需要把这个js脚本文件动态插入(比如某些指定的时候我们才需要加载这个库),然后使用这个库暴露的一些api,下面就简单讲解下如果动态插入script脚本文件。
声明:
- 下面是基于Vue的伪代码;
- 假定第三方的js文件位于static目录中,且文件名叫test.min.js;
// 动态插入script脚本
loadScript(url){
return new Promise(resolve => {
if(document.getElemntsByClassName('test-package').length > 0) return resolve();
const script = document.createElement('script');
script.type = 'text/javascript';
script.classsName = 'test-package';
if(script.readyState) {
// 保证脚本文件已经加载了
script.onreadystatechange = () => {
if(['loaded', 'complete'].indexOf(script.readyState) !== -1){
script.onreadystatechange = null;
resolve();
}
}
} else {
script.onload = () => {
resolve();
}
}
script.src = url;
document.getElemntsByClassName('head')[0].appendChild(script);
})
}
// 使用
useScript(){
let url = config.env === 'development' ? '' : '/dist'; // 此处是假定有一个环境变量区分开发和生产环境
loadScript(`${url}/static/test.min.js`).then(_ => {
// 就可以在这里使用test.min.js库里面暴露的变量了
})
}