Vue3 导出word代码,可导出数据+图片

网上搜到的代码都没办法直接使用,需要修改后才能用,我在这里发一版修改后的完美可用的代码,可以直接拿去用,也给自己留个备份。

我在使用网上搜到的代码的时候遇到一些问题,如果你也遇到了这些问题,那么表示这篇文章可以帮助你。

 

问题:

1、JSZIP版本太高(V3.x),导致docxtemplater无法使用,以及一些函数大改动。

2、Vue3使用的Vite而不是Webpack,因此 require,Buffer等功能不能使用。

 

解决方法:

1、手动将JSZIP由3.x改为2.3.0版本,重新npm i,即可解决。

2、Buffer要安装buffer依赖,使用

npm i buffer --save

进行安装,然后直接按如下引用:

import {Buffer} from "buffer";

另外,网络上的代码在加载图片处理依赖时使用的是

var ImageModule = require('docxtemplater-image-module-free');

需要改为

import ImageModule from "docxtemplater-image-module-free";

 

以上就是主要的问题所在,下面贴上完整代码和一些需要注意的东西。

完整代码

1、开始之前,你需要先安装如下依赖

-- 安装 docxtemplater
npm install docxtemplater pizzip  --save

-- 安装 jszip-utils
npm install jszip-utils --save

-- 安装 jszip
npm install jszip --save

-- 安装 FileSaver
npm install file-saver --save

-- 安装 buffer 用作b64转buffer
npm install buffer --save

-- 安装 docxtemplater-image-module-free 用作图片处理
npm install docxtemplater-image-module-free --save

 

2、新建一个js文件,我是在/src/utils/下新建的fileExport.js,并粘贴如下内容。

import PizZip from 'jszip';
import docxtemplater from 'docxtemplater';
import JSZipUtils from 'jszip-utils';
import {saveAs} from 'file-saver';
import {Buffer} from "buffer";
import ImageModule from "docxtemplater-image-module-free";


const base64DataURLToArrayBuffer = (dataURL) => {
    const base64Regex = /^data:image\/(png|jpg|svg|svg\+xml);base64,/;
    if (!base64Regex.test(dataURL)) {
        return false;
    }
    const stringBase64 = dataURL.replace(base64Regex, "");
    let binaryString;
    if (typeof window !== "undefined") {
        binaryString = window.atob(stringBase64);
    } else {
        binaryString = new Buffer(stringBase64, "base64").toString("binary");
    }
    const len = binaryString.length;
    const bytes = new Uint8Array(len);
    for (let i = 0; i < len; i++) {
        const ascii = binaryString.charCodeAt(i);
        bytes[i] = ascii;
    }
    return bytes.buffer;
}

/**
 * 导出word,支持图片
 * @param {Object} tempDocxPath 模板文件路径
 * @param {Object} wordData 导出数据
 * @param {Object} fileName 导出文件名
 */
export const exportWord = (tempDocxPath, wordData, fileName, imageSize = {}) => {
    // 读取并获得模板文件的二进制内容
    JSZipUtils.getBinaryContent(tempDocxPath, function (error, content) {
            if (error) {
                throw error;
            }
            let opts = {};
            opts.centered = true; // 图片居中,在word模板中定义方式为{%%image}
            opts.fileType = "docx";
            opts.getImage = function (chartId) {
                return base64DataURLToArrayBuffer(chartId);
            };
            opts.getSize = function (img, tagValue, tagName) {
                // console.log(img);//ArrayBuffer数据
                // console.log(tagValue);//base64数据
                // console.log(tagName);//wordData对象的图像属性名
                // 自定义指定图像大小
                // eslint-disable-next-line no-prototype-builtins
                if (imageSize.hasOwnProperty(tagName)) {
                    return imageSize[tagName];
                } else {
                    return [660, 440];
                }
            }
            let imageModule = new ImageModule(opts);
            // 创建一个PizZip实例,内容为模板的内容
            const zip = new PizZip(content);
            // 创建并加载docxtemplater实例对象
            const doc = new docxtemplater();
            doc.attachModule(imageModule);
            doc.loadZip(zip);

            doc.setData(wordData);

            try {
                // 用模板变量的值替换所有模板变量
                doc.render();
            } catch (error) {
                // 抛出异常
                const e = {
                    message: error.message,
                    name: error.name,
                    stack: error.stack,
                    properties: error.properties
                };
                console.log(
                    JSON.stringify({
                        error: e
                    })
                );
                throw error;
            }
            console.log('11');
            // 生成一个代表docxtemplater对象的zip文件(不是一个真实的文件,而是在内存中的表示)
            const out = doc.getZip().generate({
                type: 'blob',
                mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
            });
            // 将目标文件对象保存为目标类型的文件,并命名
            saveAs(out, fileName);
        }
    );
};

 

3、参数说明

  • tempDocxPath 是模板文件绝对路径
  • wordData 模板数据
  • fileName 要保存的文件名
  • imageSize 图片大小,接受一个int对象,格式是{ width, height },单位是px,例如{ 400, 300 }

如需导出图片,则需要将图片转为base64后写入wordData对象内。

echarts转base64的方法是

echarts.getInstanceByDom(document.getElementById('echarts')).getDataURL({
    type: 'png',
    backgroundColor: '#fff',
    pixelRatio: 1,
})

 

4、模板变量

模板内变量要和wordData里的变量名一致。普通字段:{字段名},图片字段:{%字段名}

 

以上

Vue3 错误处理机制onErrorCaptured使用方法

大家好,我在做JSON转XML时出现,如果字符串格式是XML,但是却强制进行JSON -> XML操作,控制台会报错Syntex Error,这个错误是人为导致的,并不是代码有问题,因此我需要捕获这个错误并处理。

尝试过使用try catch来捕获,发现根本没用。

经过查阅文档,发现可以使用生命周期钩子onErrorCaptured来处理

下面是我的代码,各位在使用时可以借鉴。

onErrorCaptured((err) => {
      // 判断是错误还是异常
      if (err.toString().indexOf('SyntaxError') !== -1) {
        ElNotification({
          title: '程序出错',
          message: err.message,
          type: 'error',
        })
        return false
      }
    })

 

err是返回的错误对象,你要查找特定的错误就得把他转字符串,你可以在控制台看到完整的错误信息(红色的),err.message只包含内容,不包含开头的错误或警告类型。

祝各位程序永无BUG

超简单使用elementUI的表单验证,看不懂官方文档的看这里

复杂的官方文档

大家好,编写elementUI时,看到新的element-plus的表单验证部分写的如此的复杂,我下面复制官方文档的表单验证部分的代码。

<template>
  <el-form
    ref="ruleFormRef"
    :model="ruleForm"
    :rules="rules"
    label-width="120px"
    class="demo-ruleForm"
    :size="formSize"
  >
    <el-form-item label="Activity name" prop="name">
      <el-input v-model="ruleForm.name" />
    </el-form-item>
    <el-form-item label="Activity zone" prop="region">
      <el-select v-model="ruleForm.region" placeholder="Activity zone">
        <el-option label="Zone one" value="shanghai" />
        <el-option label="Zone two" value="beijing" />
      </el-select>
    </el-form-item>
    <el-form-item label="Activity time" required>
      <el-col :span="11">
        <el-form-item prop="date1">
          <el-date-picker
            v-model="ruleForm.date1"
            type="date"
            placeholder="Pick a date"
            style="width: 100%"
          />
        </el-form-item>
      </el-col>
      <el-col class="text-center" :span="2">
        <span class="text-gray-500">-</span>
      </el-col>
      <el-col :span="11">
        <el-form-item prop="date2">
          <el-time-picker
            v-model="ruleForm.date2"
            placeholder="Pick a time"
            style="width: 100%"
          />
        </el-form-item>
      </el-col>
    </el-form-item>
    <el-form-item label="Instant delivery" prop="delivery">
      <el-switch v-model="ruleForm.delivery" />
    </el-form-item>
    <el-form-item label="Activity type" prop="type">
      <el-checkbox-group v-model="ruleForm.type">
        <el-checkbox label="Online activities" name="type" />
        <el-checkbox label="Promotion activities" name="type" />
        <el-checkbox label="Offline activities" name="type" />
        <el-checkbox label="Simple brand exposure" name="type" />
      </el-checkbox-group>
    </el-form-item>
    <el-form-item label="Resources" prop="resource">
      <el-radio-group v-model="ruleForm.resource">
        <el-radio label="Sponsorship" />
        <el-radio label="Venue" />
      </el-radio-group>
    </el-form-item>
    <el-form-item label="Activity form" prop="desc">
      <el-input v-model="ruleForm.desc" type="textarea" />
    </el-form-item>
    <el-form-item>
      <el-button type="primary" @click="submitForm(ruleFormRef)"
        >Create</el-button
      >
      <el-button @click="resetForm(ruleFormRef)">Reset</el-button>
    </el-form-item>
  </el-form>
</template>

<script lang="ts" setup>
import { reactive, ref } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'

const formSize = ref('default')
const ruleFormRef = ref<FormInstance>()
const ruleForm = reactive({
  name: 'Hello',
  region: '',
  date1: '',
  date2: '',
  delivery: false,
  type: [],
  resource: '',
  desc: '',
})

const rules = reactive<FormRules>({
  name: [
    { required: true, message: 'Please input Activity name', trigger: 'blur' },
    { min: 3, max: 5, message: 'Length should be 3 to 5', trigger: 'blur' },
  ],
  region: [
    {
      required: true,
      message: 'Please select Activity zone',
      trigger: 'change',
    },
  ],
  date1: [
    {
      type: 'date',
      required: true,
      message: 'Please pick a date',
      trigger: 'change',
    },
  ],
  date2: [
    {
      type: 'date',
      required: true,
      message: 'Please pick a time',
      trigger: 'change',
    },
  ],
  type: [
    {
      type: 'array',
      required: true,
      message: 'Please select at least one activity type',
      trigger: 'change',
    },
  ],
  resource: [
    {
      required: true,
      message: 'Please select activity resource',
      trigger: 'change',
    },
  ],
  desc: [
    { required: true, message: 'Please input activity form', trigger: 'blur' },
  ],
})

const submitForm = async (formEl: FormInstance | undefined) => {
  if (!formEl) return
  await formEl.validate((valid, fields) => {
    if (valid) {
      console.log('submit!')
    } else {
      console.log('error submit!', fields)
    }
  })
}

const resetForm = (formEl: FormInstance | undefined) => {
  if (!formEl) return
  formEl.resetFields()
}
</script>

一个简单的表单验证写的这么啰嗦,我就想校验一下登录账号和密码,需要这么麻烦吗?

其实只需要FormInstance就可以实现了。

超简单的写法

对于简单的表单验证,完全不需要上面那么啰嗦。

其实验证是很简单的,只需要写一个验证的数组(rules),并赋值给el-form组件的rule参数即可,如下

<el-form ref="formRef" :model="loginForm" :rules="rules" label-position="top"></el-form>

rules长这样

const rules = ref({
      phone: [
        {required: true, message: '必须输入手机号码', trigger: 'blur'},
        {pattern: /^1[3456789]\d{9}$/, message: '手机号码格式不正确', trigger: 'blur'},
      ],
      password: [
        {required: true, message: '必须输入密码', trigger: 'blur'},
        {min: 6, max: 20, message: '密码长度在 6 到 20 个字符', trigger: 'blur'},
      ]
    })

那你可能会问了,ref是干啥用的啊?

告诉你,ref的目的是为了在提交或者什么操作的时候,手动验证表单,如下。

比如我们有个登录按钮,和登录的方法onLogin()

<el-button type="primary" @click="onLogin(formRef)">登录</el-button>
const onLogin = (form) => {
      form.validate((valid, fields) => {
        if (valid) {
          console.log('登录成功')
        } else {
          console.log('error submit!', fields)
        }
      })
    }

相信小伙伴们已经看明白什么意思了。

简单来说,就是在登录方法里传参传这个ref进去,然后这个ref有validate()方法,通过这个方法就校验了。上面的if else是固定写法。

那么ref这个值是哪来的呢?其实是开头说的FormInstance定义出来的,定义如下:

// 引用
import {FormInstance} from "element-plus"

// 这个是写在setup()里的!!
const formRef = ref<FormInstance>('')

 

 

怎么样,是不是比官方那啰里啰嗦一大串舒服多了?

Vue3.x 控制台警告 [Violation] Added non-passive event listener to a scroll-blocking ‘touchmove’ event. Consider marking event handler as ‘passive’ to make the page more responsive.

控制台警告信息

[admonition]

[Violation] Added non-passive event listener to a scroll-blocking ‘touchmove’ event. Consider marking event handler as ‘passive’ to make the page more responsive.

[/admonition]

情况说明

以下说明引用至https://stackoverflow.com/questions/39152877/consider-marking-event-handler-as-passive-to-make-the-page-more-responsive/62639220

想了解更多关于这个的问题可以点上面链接进去看原文

For those receiving this warning for the first time, it is due to a bleeding edge feature called Passive Event Listeners that has been implemented in browsers fairly recently (summer 2016). From https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md:

Passive event listeners are a new feature in the DOM spec that enable developers to opt-in to better scroll performance by eliminating the need for scrolling to block on touch and wheel event listeners. Developers can annotate touch and wheel listeners with {passive: true} to indicate that they will never invoke preventDefault. This feature shipped in Chrome 51, Firefox 49 and landed in WebKit. For full official explanation read more here.

其实说白了就是个新特性,似乎是用来提高移动端页面滚动效率的?

我也整不太明白,这也不是个错误,只是个警告。

解决方法非常简单,看下面

解决方法

有两种方法,我倾向于第二种

1、在鼠标滚动方法上加上passive,如下所示

@mouseleave.passive="leave"

2、安装扩展,一劳永逸

npm i default-passive-events -S

安装后,在main.js里引入即可

import 'default-passive-events'

 

(转载)一劳永逸解决Vue sass-loader node-sass node的安装问题

node-sass代码库地址:https://github.com/sass/node-sass

吐槽

我已经被sass这个sb玩意折磨了无数次了,直接npm i sass-loader 就会报错,之前碰巧降版本解决了问题,但是这几天发现降版本也不管用了,百度直接搜错误信息甚至都找不到一个有用的答案。

查了N多资料无果之后我换了种方法,因为要实现sass需要同时有 sass-loader  node-sass   而node-sass一看就和node有关系,我以此为切入点搜索,终于让我找到一个大佬总结的文章。

为了以后不再被这sb玩意折磨,我决定转载一下这个文章。如果能帮到各位,请去给原作者点赞!

见过的错误

1、安装sass-loader后报 TypeError: this.getOptions is not a function

2、安装sass-loader 低版本后报 Syntax Error: Error: PostCSS received undefined instead of CSS string

3、安装node-sass时报 Install fail! RunScriptError: post install error, please remove node_modules before retry!

也许还有其他错误,但是这几个我几乎每次都能遇到。

万能无错版本

大佬总结了一套万能无错版本,但是经过我测试,以下版本对应是最完美的!

重要提醒:一定要先看node版本是不是符合要求,不符合的用下面的方法修改,不然装不上。版本一定得对应!

node 12.x
sass-loader 7.3.1
node-sass 4.14.1

node 16.x
sass-loader 10.0.1
node-sass 6.0.1

node版本对应的node-sass

NodeJS Supported node-sass version Node Module
Node 17 7.0+ 102
Node 16 6.0+ 93
Node 15 5.0+, <7.0 88
Node 14 4.14+ 83
Node 13 4.13+, <5.0 79
Node 12 4.12+, <8.0 72
Node 11 4.10+, <5.0 67
Node 10 4.9+, <6.0 64
Node 8 4.5.3+, <5.0 57
Node <8 <5.0 <57

下面是node-sass和sass-loader推荐对应版本

sass-loader@^4.1.1,node-sass@^4.3.0
sass-loader@^7.0.3,node-sass@^4.7.2
sass-loader@^7.3.1,node-sass@^4.7.2
sass-loader@^7.3.1,node-sass@^4.14.1

安装node-sass和sass-loader的方法

// 以下管理器任选一种,推荐cnpm
npm i node-sass@^4.14.1
cnpm i node-sass@^4.14.1
yarn add node-sass@^4.14.1

npm i sass-loader@^7.3.1
cnpm i sass-loader@^7.3.1
yarn add sass-loader@^7.3.1

查看 node 版本的方法

node -v

修改 node 版本的方法

// 首先安装node版本管理器n
sudo npm install n -g

// 自定义版本号安装
sudo n 版本号  //例如:sudo n 10.16.0 ; 本文推荐 sudo n 12

// 安装稳定版
sudo n stable

// 安装最新版
sudo n latest

Vue中组件库生成的元素修改样式无效的解决方案

问题产生

写项目时使用如elementui,vanui等组件库时,一些组件的子组件会有如图所示的,带有“class__xxx”这样的class,同时在写代码时为了样式不冲突,我们通常都会在每个组件的style上加上scoped,然后就导致更改上述子组件样式时无效。

 

 

 

 

 

 

上述样式我们使用常规css是无法修改的,如下代码所示。

.stats {
  .van-grid-item__content {
    border-radius: 10px !important;
  }
}

解决方案

方案1 使用 /deep/

将上述css写成如下形式

.stats {
  .van-grid-item /deep/ .van-grid-item__content {
    border-radius: 10px !important;
  }
}

/deep/ 是深度选择器,加上这个之后,就可以选中scope之后的子组件。

但是这个方法似乎只在低版本的Vue中有效,我使用的时候报如下错误

 

 

 

 

 

和我有一样问题的同学,请看下面的方法

方法2 使用 ::v-deep

在较新的版本中,使用::v-deep 代替了旧的 /deep/ ,因此将代码改为如下即可解决问题

.stats {
  .van-grid-item::v-deep .van-grid-item__content {
    border-radius: 10px !important;
  }
}

注意,如果要改的元素没有父元素,则直接在开头加上 ::v-deep即可,如下所示

.stats {
  // 没有父元素的情况下
  ::v-deep .van-grid-item__content {
    border-radius: 10px !important;
  }
}

 

[避坑]Vue.js $refs能获取到对象但是子对象获取不到(undefined)的问题

写项目需要用ref获取dom对象来获取元素的属性,Vue的ref属性非常好用,但是今天碰到了问题。

<div v-for="(item, index) in items" :key="index">
    <div ref="content"> </div>
</div>

定义了以上循环,循环内有个div,写上了ref属性。

此时我在methods里写了接口来获取数据,数据到了之后,这个div就有宽度和高度了,我的目的是想取到里面这个div的高度和宽度

在created里写了如下方法

that.getItemList()
that.$nextTick(() => {
    that.divHeight = that.$refs.content[0].offsetHeight
}, false)

此时报错,提示 .content这个对象不存在。

我打印 that.$refs 发现content实际是有的,但是取不到。

在翻阅文档和资料后,找到解决办法:

在更新模板渲染的方法里才能取到,也就是说,在that.getItemList()这个方法里,有一段that.items = resultData。在这赋值之后紧接着获取refs,就可以获取到了。

这也算是个知识点了,写给自己备忘。

如果你跟我有相同的问题,并且看完文章还不明白解决不了,加我QQ10066267。我们共同学习。

【Vue进阶】element ui中switch组件使用注意事项

需求描述

1、根据后台传值动态显示开关状态及文字说明(0为文字,1为图标)

实现方法

Swich默认是boolean类型,而后台传值为number类型,这个时候我们需要用number来取代boolean类型;

 

<template #default="{ row }">
          <el-switch
            v-model="row.status"
            :active-value="1"
            :inactive-value="0"
            active-color="#13ce66"
            inactive-color="#ff4949"
          ></el-switch>
</template>

需要注意的是,必须在active-value和inactive-value前面加冒号,因为不加冒号是string加了冒号才是number,基础知识、

我们使用绑定的方式,同时@change可以传值$event就是switch的当条信息值,或者直接获取绑定的model值state,scope.row为参数,还可以再添加index表示当前列表的序号。
注:改变状态时要注意返回的状态根据状态来选择相应的写法。

 

解决Vuetify.js 没有颜色的问题

问题

开了新坑,打算试试Vuetify.js 按照官网说明折腾了半天之后算是跑起来了。css样式文件也加载进来,但是加载组件后发现基本样式丢失,颜色也是黑白的,并且console也没有报错。

解决

经过翻文档和查资料,发现是Vuetify要求必须把组件包含在<v-app>标签内。于是乎我把App.vue 内的路由外包了一层<v-app>,成功解决!

[代码片段]谷歌的一个不错的CSS样式,不得不说大厂的前端设计师美感就是好。

代码

直接上效果图

我本来以为这个彩色的边框是个背景图片,看了一下源码竟然是个background样式,看来我学识还是太浅。

拿走了拿走了#滑稽

使用方法

实现这个效果需要2个div嵌套,外层的我取class为outer-div,内层的我取class为inner-div

.outer-div{
background: linear-gradient(90deg,#34a853 4%,#4285f4 0%) top/100% 34% no-repeat,linear-gradient(90deg,#fbbc04 50%,#4285f4 0%) top/100% 82% no-repeat,linear-gradient(90deg,#fbbc04 10%,#ea4335 0%) top/100% 100%;
width: 200px;
/* set width yourself */
height: 350px;
/* set height yourself */
padding: 4px;
/* default padding is 4px, you can set any value that makes you feel good */
border-radius: 8px;
/* outer-div's border-radius must be twice larger than the inner-div */
}


.inner-div {
width: 100%;
height: 100%;
border-radius: 4px;
/* inner-div's border-radius must be twice lower than the outer-div */
}

 

测试效果

随便写个DIV测试个效果

我是测试效果的呢

哈哈哈,真的好看诶