| | |
| | | import Docxtemplater from 'docxtemplater'; |
| | | import JSZipUtils from 'jszip-utils'; |
| | | import { saveAs } from 'file-saver'; |
| | | import ImageModule from 'docxtemplater-image-module-free'; // 新增:图片处理模 |
| | | |
| | | // 加载 .docx 模板文件 |
| | | function loadFile(url, callback) { |
| | |
| | | export function download(file, name) { |
| | | |
| | | } |
| | | // 辅助函数:将 base64 图片转为 Uint8Array(图片模块需要此格式) |
| | | function base64ToUint8Array(base64) { |
| | | // 去掉 base64 前缀(如 "data:image/png;base64,") |
| | | const base64WithoutPrefix = base64.replace(/^data:image\/\w+;base64,/, ''); |
| | | const binaryString = atob(base64WithoutPrefix); |
| | | const length = binaryString.length; |
| | | const uint8Array = new Uint8Array(length); |
| | | for (let i = 0; i < length; i++) { |
| | | uint8Array[i] = binaryString.charCodeAt(i); |
| | | } |
| | | return uint8Array; |
| | | } |
| | | |
| | | |
| | | // 生成并下载 Word 文档(templatePath是word文档模版地址,data是对应的数据) |
| | | export function generateWordDocument(templatePath, data, name) { |
| | |
| | | } |
| | | }; |
| | | }; |
| | | // 1. 配置图片处理规则(核心) |
| | | const imageModule = new ImageModule({ |
| | | // 获取图片:根据传入的图片数据(base64)转为模块需要的格式 |
| | | getImage: function (tagValue, tagName) { |
| | | // tagValue:data 中对应图片键的值(必须是 base64 字符串) |
| | | // tagName:图片占位符的键名(如 "avatar") |
| | | return base64ToUint8Array(tagValue); |
| | | }, |
| | | // 设置图片尺寸:返回 [宽度, 高度](单位:px),可动态调整 |
| | | getSize: function (tagValue, tagName) { |
| | | |
| | | // 示例:根据不同的图片键,返回不同尺寸 |
| | | switch (tagName) { |
| | | case 'avatar': // 头像:200x200 |
| | | return [200, 200]; |
| | | case 'coverImg': // 封面图:600x300 |
| | | return [600, 300]; |
| | | default: // 默认尺寸:400x300 |
| | | return [80, 100]; |
| | | } |
| | | } |
| | | }); |
| | | // 加载模板文件内容到 PizZip |
| | | const zip = new PizZip(content); |
| | | const doc = new Docxtemplater(zip, { |
| | | paragraphLoop: true, |
| | | linebreaks: true, |
| | | modules: [imageModule] // 关键:注入图片模块 |
| | | }); |
| | | |
| | | // const parser = new AngularParser(); |
| 对比新文件 |
| | |
| | | /** |
| | | * Vue3 在线图片 URL 转 base64 |
| | | * @param {string} imgUrl - 在线图片地址(如 "https://xxx.com/sign.png") |
| | | * @param {Object} options - 配置项 |
| | | * @param {string} options.format - 图片格式(默认 png,可选 jpeg/webp) |
| | | * @param {number} options.quality - 压缩质量(0-1,默认 1.0 无损) |
| | | * @returns {Promise<string>} 完整 base64 字符串(含 data:image/xxx;base64, 前缀) |
| | | */ |
| | | export function imageUrlToBase64( |
| | | imgUrl, |
| | | { format = 'png', quality = 1.0 } = {} |
| | | ) { |
| | | return new Promise((resolve, reject) => { |
| | | if (!imgUrl) { |
| | | reject(new Error('图片 URL 不能为空')); |
| | | return; |
| | | } |
| | | |
| | | // 处理 URL 特殊字符(如空格、中文) |
| | | const encodedUrl = encodeURI(imgUrl); |
| | | |
| | | const img = new Image(); |
| | | // 跨域关键配置(需图片服务器支持 CORS) |
| | | img.crossOrigin = 'Anonymous'; |
| | | |
| | | // 图片加载成功 |
| | | img.onload = () => { |
| | | const canvas = document.createElement('canvas'); |
| | | const ctx = canvas.getContext('2d'); |
| | | if (!ctx) { |
| | | reject(new Error('Canvas 初始化失败')); |
| | | return; |
| | | } |
| | | |
| | | // 保持图片原始尺寸(避免拉伸) |
| | | canvas.width = img.naturalWidth; |
| | | canvas.height = img.naturalHeight; |
| | | |
| | | // 绘制图片(清除透明背景,适配 jpeg 格式) |
| | | if (format === 'jpeg') { |
| | | ctx.fillStyle = '#ffffff'; |
| | | ctx.fillRect(0, 0, canvas.width, canvas.height); |
| | | } |
| | | ctx.drawImage(img, 0, 0); |
| | | |
| | | // 转换为 base64 |
| | | try { |
| | | const base64 = canvas.toDataURL(`image/${format}`, quality); |
| | | resolve(base64); |
| | | } catch (err) { |
| | | reject(new Error(`base64 转换失败:${err.message}`)); |
| | | } |
| | | |
| | | // 释放内存(销毁临时元素) |
| | | canvas.remove(); |
| | | img.remove(); |
| | | }; |
| | | |
| | | // 图片加载失败(跨域、URL 无效、网络错误) |
| | | img.onerror = (err) => { |
| | | reject(new Error(`图片加载失败:${err.message},可能是跨域限制或 URL 无效`)); |
| | | }; |
| | | |
| | | // 触发加载(必须放在回调绑定后) |
| | | img.src = encodedUrl; |
| | | }); |
| | | } |
| | |
| | | <el-row :gutter="24"> |
| | | <el-col :span="24"> |
| | | <el-form-item label="项目文件:" prop="productItemId"> |
| | | <el-select clearable v-model="state.form.productItemId" :disabled="state.title =='查看'" filterable style="width: 290px"> |
| | | <el-select clearable v-model="state.form.productItemId" :disabled="state.title =='查看'" filterable style="width: 308px"> |
| | | <el-option |
| | | v-for="item in state.itemFileList" |
| | | :key="item.id" |
| | |
| | | <!-- </el-select>--> |
| | | |
| | | </el-form-item> |
| | | <el-form-item label="类型:"> |
| | | <el-select v-model="data.queryParams.type" filterable placeholder="请选择" |
| | | > |
| | | <el-option |
| | | v-for="item in data.typeList" |
| | | :key="item.id" |
| | | :label="item.name" |
| | | :value="item.id"> |
| | | </el-option> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item > |
| | | <el-button type="primary" @click="getList">查询</el-button> |
| | | <el-button type="primary" plain @click="reset">重置</el-button> |
| | |
| | | import {delReview, getReviewPage, sendReview} from "@/api/selfProblems/projectReview"; |
| | | import axios from "axios"; |
| | | import {getToken} from "@/utils/auth"; |
| | | // import {generateWordDocument} from "@/views/build/conpanyFunctionConsult/digitalFileDep/manageType/qualityManual/components/exportDoc"; |
| | | import {generateWordDocument} from "@/utils/exportWord"; |
| | | import {imageUrlToBase64} from "@/utils/imageToBase"; |
| | | const userStore = useUserStore() |
| | | const { proxy } = getCurrentInstance(); |
| | | const loading = ref(false); |
| | |
| | | pageNum: 1, |
| | | pageSize: 10, |
| | | companyId: null, |
| | | itemName: null |
| | | itemName: null, |
| | | type:null |
| | | }, |
| | | total: 0, |
| | | dataList: [], |
| | | companyList: [], |
| | | industryList: [], |
| | | isAdmin: false, |
| | | typeList: [], |
| | | typeList: [ |
| | | { |
| | | id: 1, |
| | | name: '会签评审' |
| | | }, |
| | | { |
| | | id: 2, |
| | | name: '会议评审' |
| | | }, |
| | | ], |
| | | exportDialog: false, |
| | | projectList: [], |
| | | |
| | |
| | | pageNum: 1, |
| | | pageSize: 10, |
| | | companyId: null, |
| | | itemId: null |
| | | itemId: null, |
| | | type:null |
| | | } |
| | | await getCompanyList() |
| | | }else { |
| | |
| | | pageNum: 1, |
| | | pageSize: 10, |
| | | companyId: data.queryParams.companyId, |
| | | itemId: null |
| | | itemId: null, |
| | | type:null |
| | | } |
| | | } |
| | | choosedData.value = [] |
| | |
| | | const templatePath = ref('/projectReviewExample.docx') |
| | | const startGeneration = async () => { |
| | | const data = JSON.parse(JSON.stringify(choosedData.value)) |
| | | data.forEach(item => { |
| | | item.leaderList = item.reviewUsers.filter(item => item.reviewType == '评审组长').map((x,index) => { |
| | | return { |
| | | ...x, |
| | | first: index === 0 |
| | | |
| | | } |
| | | }) |
| | | item.peopleList = item.reviewUsers.filter(item => item.reviewType == '评审组员').map((x,index) => { |
| | | return { |
| | | ...x, |
| | | first: index === 0 |
| | | for(const item of data){ |
| | | item.leaderList = await Promise.all( |
| | | item.reviewUsers |
| | | .filter(user => user.reviewType === '评审组长') |
| | | .map(async (x, index) => { |
| | | let signBase64 = ''; |
| | | if (x.sign != '') { |
| | | try { |
| | | const url = import.meta.env.VITE_APP_BASE_API + '/' + x.sign; |
| | | signBase64 = await imageUrlToBase64(url, { |
| | | format: 'png', |
| | | quality: 0.8 |
| | | }); |
| | | } catch (err) { |
| | | signBase64 = ''; |
| | | } |
| | | } |
| | | return { |
| | | ...x, |
| | | first: index === 0, |
| | | sign: signBase64 || '' |
| | | }; |
| | | }) |
| | | ); |
| | | |
| | | } |
| | | }) |
| | | item.leaderTime = item.leaderTime?.substring(0,10) |
| | | item.groupTime = item.groupTime?.substring(0,10) |
| | | // 2. 处理 peopleList:同样用 Promise.all 等待异步完成 |
| | | item.peopleList = await Promise.all( |
| | | item.reviewUsers |
| | | .filter(user => user.reviewType === '评审组员') |
| | | .map(async (x, index) => { |
| | | let signBase64 = ''; |
| | | if (x.sign != '') { |
| | | try { |
| | | const url = import.meta.env.VITE_APP_BASE_API + '/' + x.sign; |
| | | signBase64 = await imageUrlToBase64(url, { |
| | | format: 'png', |
| | | quality: 0.8 |
| | | }); |
| | | } catch (err) { |
| | | signBase64 = ''; |
| | | } |
| | | } |
| | | return { |
| | | ...x, |
| | | first: index === 0, |
| | | sign: signBase64 || '' |
| | | }; |
| | | }) |
| | | ); |
| | | item.leaderTime = item.leaderTime?.substring(0, 10) |
| | | item.groupTime = item.groupTime?.substring(0, 10) |
| | | console.log(' item.tableList', item.tableList) |
| | | try { |
| | | generateWordDocument(templatePath.value, item, item.itemName + `_项目审批表.docx`); |
| | | } catch (error){ |
| | | } catch (error) { |
| | | ElMessage({ |
| | | type: 'warning', |
| | | message: '导出失败' |
| | | }); |
| | | } |
| | | }) |
| | | |
| | | |
| | | } |
| | | } |
| | | |
| | | |
| | | const changeCom = () => { |
| | | getProjectList() |
| | | } |