zhouwx
2025-11-06 9c2d854d62aa70e4753f43fbede7960381b9804b
修改
已添加1个文件
已修改4个文件
212 ■■■■ 文件已修改
public/projectReviewExample.docx 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/exportWord.js 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/imageToBase.js 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/build/conpanyFunctionConsult/digitalFileDep/project/projectReview/components/editDialog.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/build/conpanyFunctionConsult/digitalFileDep/project/projectReview/index.vue 106 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
public/projectReviewExample.docx
Binary files differ
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();
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;
    });
}
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"
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()
}