目录

vue3页面html导出word文档

vue3页面html导出word文档

一、第三方包下载

使用npm下载以下插件:

npm install jszip-utils docxtemplater pizzip file-saver docxtemplater-image-module-free

二、总页面组件代码

<template>

<summaryDetails

:securityId="securityId"

:symbol="symbol"

id="summaryDetails"

/>

</template>

<script setup lang="ts">

import summaryDetails from './summaryDetails.vue'

const props = defineProps({

symbol: {

// 证券代码

type: String,

default: '000001'

},

securityId: {

// 证券id

type: [String, Number],

required: true

}

})

</script>

<style scoped lang="less"></style>

三、summaryDetails组件代码

<template>

<ItemCard title="综述" v-loading="loading">

<template #rightPart>

<el-date-picker

v-model="dateRange"

value-format="YYYY"

type="yearrange"

range-separator="至"

start-placeholder="开始年份"

end-placeholder="结束年份"

:editable="false"

:disabled-date="disabledDate"

@change="changeDate"

@clear="changeDate1"

class="date-select"

ref="datePicker"

/>

<div class="upload-box" @click="exportDataList">

<span class="icon iconfont icon-baogaodaochu"></span>

<span class="upload-text">报告导出</span>

</div>

</template>

<template #otherContent>

<div class="item">

<div class="title">简述</div>

<div class="table-box">

<AISketch :sketch-list="sketchList"></AISketch>

</div>

</div>

<div class="item">

<div class="title">综述详情</div>

<div class="table-box">

<div

class="item-detail"

v-for="(item, index) in summaryDetailsList"

:key="index"

>

<div class="detail-title">

{
{ item.title }}

<div class="line"></div>

</div>

<div class="all-detail" v-html="item.overview"></div>

<div

class="children-item"

v-for="(item1, index1) in item.items"

:key="index1"

>

<div class="children-title">

<div class="dot"></div>

{
{ item1.title }}

</div>

<div class="children-detail" v-html="item1.desc"></div>

</div>

</div>

</div>

</div>

</template>

</ItemCard>

</template>

<script setup lang="ts">

import JSZipUtils from 'jszip-utils'

import docxtemplater from 'docxtemplater'

import PizZip from 'pizzip'

import saveAs from 'file-saver'

import ImageModule from 'docxtemplater-image-module-free'

import { ElMessage } from 'element-plus'

import ItemCard from './../RiskWarn/ItemCard.vue'

import AISketch from '@/pages/monitor/views/stockMonitor/components/AISummary/AISketch.vue'

import { queryAISummaryData } from '@/pages/monitor/monitorApi/stockMonitor'

const props = defineProps({

symbol: {

// 证券代码

type: String,

default: ''

},

securityId: {

// 证券id

type: [String, Number],

required: true

},

sketchList: {

type: Array as any,

default: () => {}

}

})

interface ImageOptions {

centered: boolean

getImage: (chartId: string) => ArrayBuffer | false

getSize: () => [number, number]

}

const loading = ref(false) // 加载状态

const dateRange = ref<any>([]) // 年份范围

const securityId1 = ref(props.securityId) // 证券id

const symbol1 = ref(props.symbol) // 证券代码

const sketchList = ref<any>(props.sketchList) // 简述数据

const summaryDetailsList = ref<any>([]) // 综述详情数据

const cleanedData = ref<any>([]) // 综述详情数据

const datePicker = ref() // 时间筛选框ref

// 监听参数重新赋值

watch(

() => [props.securityId, props.symbol, props.sketchList],

(newValue: any, oldValue: any) => {

if (newValue[0] != oldValue[0]) {

securityId1.value = newValue[0]

getAISummaryData() //查询接口

}

if (newValue[1] != oldValue[1]) {

symbol1.value = newValue[1]

}

if (newValue[2] != oldValue[2]) {

sketchList.value = newValue[2]

}

dateRange.value = []

},

{ deep: true }

)

// 控制当前时间之后不能选

const disabledDate = (time: any) => {

const year = time.getFullYear()

return year < 2019 || year > new Date().getFullYear()

}

// 改变时间

const changeDate = (value: any) => {

if (value) {

dateRange.value = toRaw(value)

getAISummaryData()

datePicker.value.blur()

}

}

// 清空时间

const changeDate1 = () => {

dateRange.value = []

getAISummaryData()

}

const base64DataURLToArrayBuffer = (dataURL: string) => {

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

}

const downLoadDoc = (demoUrl: string, docxData: any, fileName: string) => {

// 读取并获得模板文件的二进制内容

JSZipUtils.getBinaryContent(demoUrl, function (error: any, content: any) {

// 抛出异常

if (error) {

throw error

}

let opts: ImageOptions = {

centered: false,

getImage: (chartId: string) => {

return base64DataURLToArrayBuffer(chartId) || false

},

getSize: () => {

return [230, 120]

}

}

// 创建一个PizZip实例,内容为模板的内容

let zip = new PizZip(content)

// 创建并加载docxtemplater实例对象

let doc = new docxtemplater()

.loadZip(zip)

.attachModule(new ImageModule(opts))

// 去除未定义值所显示的undefined

doc.setOptions({

nullGetter: function () {

return ''

}

})

// 设置模板变量的值,对象的键需要和模板上的变量名一致,值就是你要放在模板上的值

doc.setData({

...docxData

})

// eslint-disable-next-line no-useless-catch

try {

// 用模板变量的值替换所有模板变量

doc.render()

} catch (error) {

// 抛出异常

throw error

}

// 生成一个代表docxtemplater对象的zip文件(不是一个真实的文件,而是在内存中的表示)

let out = doc.getZip().generate({

type: 'blob',

mimeType:

'application/vnd.openxmlformats-officedocument.wordprocessingml.document'

})

// 将目标文件对象保存为目标类型的文件,并命名

saveAs(out, fileName)

})

}

// 导出

const exportDataList = async () => {

loading.value = true

let fileName = `${

dateRange.value && dateRange.value.length > 0

? `${dateRange.value[0]}-${dateRange.value[1]}综述`

: '今年以来综述'

}`

// 去掉 <span style="color: #ff0000"></span> 标签

const cleanedItems = sketchList.value.map((item: any) => ({

...item,

desc: item.desc.replace(/<span style="color: #ff0000">(.*?)<\/span>/g, '$1')

}))

// 去掉 <br/> 标签

cleanedData.value = summaryDetailsList.value.map((item: any) => {

// 解决更改对象导致原数组换行标签消失问题(重新复制一份对象进行删除标签)

const newItem = JSON.parse(JSON.stringify(item))

return removeBrTags(newItem)

})

// 下载word文档

downLoadDoc(

'public/ai-report.docx',

{ items: cleanedItems, infos: cleanedData.value },

fileName

)

loading.value = false

}

// 去掉<br/>标签方法

const removeBrTags = (obj: any) => {

if (typeof obj === 'string') {

return obj.replace(/<br\/>/g, '') // 去掉 <br/> 标签

} else if (typeof obj === 'object' && obj !== null) {

for (const key in obj) {

if (Object.prototype.hasOwnProperty.call(obj, key)) {

obj[key] = removeBrTags(obj[key]) // 递归处理

}

}

}

return obj

}

// 查询数据

const getAISummaryData = () => {

loading.value = true

sketchList.value = []

summaryDetailsList.value = []

queryAISummaryData(

dateRange.value[1],

securityId1.value,

dateRange.value[0],

symbol1.value

).then((res: any) => {

if (res.code === 200) {

sketchList.value = res.data.items

summaryDetailsList.value = res.data.data

} else {

ElMessage({

type: 'error',

message: res.message

})

}

loading.value = false

})

}

onMounted(() => {

getAISummaryData()

})

</script>

<style scoped lang="less">

.upload-box {

margin-left: 20px;

display: flex;

align-items: center;

cursor: pointer;

font-weight: normal;

.icon-baogaodaochu {

font-size: 14px;

color: #3a5bb7;

}

.upload-text {

font-size: 12px;

color: #3a5bb7;

margin-left: 3px;

}

}

.item {

border: 1px solid #ebeef8;

border-top: none;

.title {

display: flex;

align-items: center;

padding-left: 26px;

height: 38px;

background: #f4f7fc;

border: 1px solid #ebeef8;

border-left: none;

border-right: none;

font-weight: bold;

font-size: 14px;

color: #333333;

position: relative;

&::before {

content: '';

position: absolute;

left: 15px;

top: 50%;

margin-top: -5px;

width: 4px;

height: 10px;

background: #3a5bb7;

border-radius: 0px 2px 2px 0px;

}

}

.table-box {

padding: 15px;

.item-detail {

margin-bottom: 20px;

&:last-child {

margin-bottom: 0;

}

.detail-title {

width: 80px;

height: 30px;

background: rgba(58, 91, 183, 0.06);

border-radius: 6px;

font-size: 13px;

color: #3a5bb7;

display: flex;

align-items: center;

justify-content: center;

margin-right: 12px;

}

.all-detail {

margin: 10px 0;

}

.children-item {

margin-bottom: 10px;

font-size: 14px;

color: #333333;

&:last-child {

margin-bottom: 0;

}

.children-title {

margin: 5px 0;

display: flex;

align-items: center;

.dot {

width: 4px;

height: 4px;

background: #333333;

margin-right: 10px;

}

}

}

}

}

}

</style>

四、ItemCard组件代码

<template>

<div class="item-box">

<div class="pub-item-title">

{{ title }}

<div class="right-part">

<slot name="rightPart"></slot>

</div>

</div>

<div class="content-box" id="content-to-export">

<div

class="overview-box"

v-if="

title !== '综述'

"

>

<div class="title">{
{ title }}</div>

<div class="content" v-html="aiText"></div>

</div>

<slot name="otherContent"></slot>

</div>

</div>

</template>

<script setup lang="ts">

const props = defineProps({

title: {

type: String,

default: ''

},

aiText: {

type: String,

default: ''

},

sketchList: {

type: Array as any,

default: () => {}

}

})

</script>

<style scoped lang="less">

.item-box {

width: 1410px;

margin: 0 auto;

padding: 0 20px 20px;

background: #ffffff;

border-radius: 6px;

.pub-item-title {

:deep(.right-part) {

display: flex;

align-items: center;

margin-left: auto;

.date-select {

width: 300px;

height: 28px;

--el-text-color-regular: #333333;

}

.drop-select {

margin-left: 14px;

width: 100px;

height: 28px;

font-weight: normal;

--el-text-color-regular: #333333;

.el-input__wrapper {

padding: 0 11px;

.el-input__inner {

line-height: 28px;

height: 28px;

}

}

.el-select__wrapper {

min-height: 28px;

}

}

}

}

.content-box {

margin-top: 14px;

// border: 1px solid #ebeef8;

.overview-box {

padding: 17px 15px;

border: 1px solid #ebeef8;

border-bottom: none;

.title {

padding-left: 10px;

line-height: 14px;

font-size: 14px;

color: #3a5bb7;

position: relative;

&::before {

content: '';

position: absolute;

left: 0;

top: 50%;

margin-top: -2px;

width: 4px;

height: 4px;

background: #3a5bb7;

}

}

.content {

margin-top: 10px;

text-align: justify;

color: #333333;

}

}

}

}

</style>

五、AISketch组件代码

<template>

<div class="AISketch">

<div class="item-sketch" v-for="(item, index) in sketchList" :key="index">

<div class="tags">{
{ item.title }}</div>

<div class="tags-info" v-html="item.desc"></div>

</div>

</div>

</template>

<script setup lang="ts">

const props = defineProps({

// 简述列表

sketchList: {

type: Array as any,

default: () => {}

}

})

</script>

<style lang="less" scoped>

.AISketch {

.item-sketch {

display: flex;

align-items: center;

margin-bottom: 10px;

&:last-child {

margin-bottom: 0;

}

.tags {

width: 80px;

height: 30px;

background: rgba(58, 91, 183, 0.06);

border-radius: 6px;

font-size: 13px;

color: #3a5bb7;

display: flex;

align-items: center;

justify-content: center;

margin-right: 12px;

}

.tags-info {

font-size: 13px;

color: #666666;

}

}

}

</style>

六、在public文件夹中创建word文件(ai-report.docx)

如下编写模板(样式自调):

综述

| 简述

{#items}

{title}{desc}

{/items}

| 综述详情

{#infos}

{title}

{overview}

{#items}

·{title}

{desc}

{/items}

{/infos}