From 9c2d854d62aa70e4753f43fbede7960381b9804b Mon Sep 17 00:00:00 2001
From: zhouwx <1175765986@qq.com>
Date: Thu, 06 Nov 2025 14:01:41 +0800
Subject: [PATCH] 修改
---
src/utils/imageToBase.js | 67 ++++++++++++++++
src/utils/exportWord.js | 37 +++++++++
src/views/build/conpanyFunctionConsult/digitalFileDep/project/projectReview/components/editDialog.vue | 2
public/projectReviewExample.docx | 0
src/views/build/conpanyFunctionConsult/digitalFileDep/project/projectReview/index.vue | 106 +++++++++++++++++++++-----
5 files changed, 190 insertions(+), 22 deletions(-)
diff --git a/public/projectReviewExample.docx b/public/projectReviewExample.docx
index 20b32e8..ebcda87 100644
--- a/public/projectReviewExample.docx
+++ b/public/projectReviewExample.docx
Binary files differ
diff --git a/src/utils/exportWord.js b/src/utils/exportWord.js
index 6c7fbe1..86f5a0f 100644
--- a/src/utils/exportWord.js
+++ b/src/utils/exportWord.js
@@ -3,6 +3,7 @@
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) {
@@ -13,6 +14,19 @@
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) {
@@ -39,11 +53,34 @@
}
};
};
+ // 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();
diff --git a/src/utils/imageToBase.js b/src/utils/imageToBase.js
new file mode 100644
index 0000000..cf978cb
--- /dev/null
+++ b/src/utils/imageToBase.js
@@ -0,0 +1,67 @@
+/**
+ * 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;
+ });
+}
diff --git a/src/views/build/conpanyFunctionConsult/digitalFileDep/project/projectReview/components/editDialog.vue b/src/views/build/conpanyFunctionConsult/digitalFileDep/project/projectReview/components/editDialog.vue
index 60be934..6f85f56 100644
--- a/src/views/build/conpanyFunctionConsult/digitalFileDep/project/projectReview/components/editDialog.vue
+++ b/src/views/build/conpanyFunctionConsult/digitalFileDep/project/projectReview/components/editDialog.vue
@@ -163,7 +163,7 @@
<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"
diff --git a/src/views/build/conpanyFunctionConsult/digitalFileDep/project/projectReview/index.vue b/src/views/build/conpanyFunctionConsult/digitalFileDep/project/projectReview/index.vue
index 5b3361a..f3355a6 100644
--- a/src/views/build/conpanyFunctionConsult/digitalFileDep/project/projectReview/index.vue
+++ b/src/views/build/conpanyFunctionConsult/digitalFileDep/project/projectReview/index.vue
@@ -38,6 +38,17 @@
<!-- </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>
@@ -109,7 +120,9 @@
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);
@@ -120,14 +133,24 @@
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: [],
@@ -201,7 +224,8 @@
pageNum: 1,
pageSize: 10,
companyId: null,
- itemId: null
+ itemId: null,
+ type:null
}
await getCompanyList()
}else {
@@ -209,7 +233,8 @@
pageNum: 1,
pageSize: 10,
companyId: data.queryParams.companyId,
- itemId: null
+ itemId: null,
+ type:null
}
}
choosedData.value = []
@@ -330,34 +355,73 @@
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()
}
--
Gitblit v1.9.2