一、第三方包下载
使用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}