docxTemplateDialog.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627
  1. <template>
  2. <el-dialog
  3. :visible.sync="show"
  4. :append-to-body="true"
  5. title="word导出"
  6. width="60%"
  7. top="10vh"
  8. :before-close="handleClose"
  9. class="dialog_diy"
  10. >
  11. <div class="box" v-loading="loading">
  12. <div class="b_left">
  13. <topicVue :cJson="checkJson" :checktype="2" :see="true" :isTeacher="1" title="" brief="" ref="topicVue"/>
  14. </div>
  15. <div class="b_right">
  16. <div class="d_box">
  17. <div class="d_b_step">
  18. <h2>第一步:下载模板文档</h2>
  19. <p>点击下载模板文档来下载指定文档</p>
  20. <el-button
  21. class="d_b_s_button"
  22. type="primary"
  23. :disabled="!downFileData"
  24. @click="downloadTemplateDocx()"
  25. >下载模板文档</el-button
  26. >
  27. </div>
  28. <div class="d_b_step">
  29. <h2>第二步:填入模板变量</h2>
  30. <p>按需制作排版Word模板文档,目前支持docx格式</p>
  31. <p>
  32. 在文件中指定位置输入下方的"字段变量",用于显示真实数据的准确位置
  33. </p>
  34. <h3>文本:</h3>
  35. <img
  36. src="https://ccrb.s3.cn-northwest-1.amazonaws.com.cn/default%2F%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_202410090922471728436994846.png"
  37. />
  38. <h3>选择题:</h3>
  39. <div class="d_b_s_imgList">
  40. <img src="https://ccrb.s3.cn-northwest-1.amazonaws.com.cn/41728546186962.png">
  41. <img src="https://ccrb.s3.cn-northwest-1.amazonaws.com.cn/51728546189479.png">
  42. </div>
  43. <div class="foldMenu">
  44. <span v-if="foldField" @click="foldField = false">折叠字段变量</span>
  45. <span v-if="!foldField" @click="foldField = true">显示字段变量</span>
  46. </div>
  47. <div
  48. v-for="(item, index) in fieldList"
  49. :key="index"
  50. class="d_b_s_fieldListItem"
  51. v-if="foldField"
  52. >
  53. <span
  54. >{{ item.name }}:{{
  55. "{" + (item.type == "image" ? "%" : "") + item.field + "}"
  56. }}</span
  57. >
  58. <span @click="copyContent(`{${item.type == 'image' ? '%' : ''}${item.field}}`)">
  59. <svg
  60. width="14"
  61. height="14"
  62. viewBox="0 0 14 14"
  63. xmlns="http://www.w3.org/2000/svg"
  64. >
  65. <path
  66. fill-rule="evenodd"
  67. clip-rule="evenodd"
  68. d="M1.85645 2.28599C1.85645 2.0493 2.04832 1.85742 2.28502 1.85742H8.71359C8.95028 1.85742 9.14216 2.0493 9.14216 2.28599C9.14216 2.52269 8.95028 2.71456 8.71359 2.71456H2.71359V8.71456C2.71359 8.95126 2.52171 9.14314 2.28502 9.14314C2.04832 9.14314 1.85645 8.95126 1.85645 8.71456V2.28599Z"
  69. />
  70. <path
  71. fill-rule="evenodd"
  72. clip-rule="evenodd"
  73. d="M4.42871 4.85631C4.42871 4.61961 4.62059 4.42773 4.85728 4.42773H11.7144C11.9511 4.42773 12.143 4.61961 12.143 4.85631V11.7134C12.143 11.9501 11.9511 12.142 11.7144 12.142H4.85728C4.62059 12.142 4.42871 11.9501 4.42871 11.7134V4.85631ZM5.28585 5.28488V11.2849H11.2859V5.28488H5.28585Z"
  74. />
  75. </svg>
  76. </span>
  77. </div>
  78. </div>
  79. <div class="d_b_step">
  80. <h2>第三步:上传填入后的模板文档</h2>
  81. <div v-if="uploadTemplateDocxData" class="d_b_s_fileCard">
  82. <svg
  83. t="1728376433028"
  84. class="icon"
  85. viewBox="0 0 1024 1024"
  86. version="1.1"
  87. xmlns="http://www.w3.org/2000/svg"
  88. p-id="5172"
  89. width="200"
  90. height="200"
  91. >
  92. <path
  93. d="M815.5 160v704c0 17.7-14.3 32-32 32h-543c-17.7 0-32-14.3-32-32V160c0-17.7 14.3-32 32-32h543c17.7 0 32 14.3 32 32z m0-96h-607c-35.3 0-64 28.7-64 64v768c0 35.3 28.7 64 64 64h607c35.3 0 64-28.7 64-64V128c0-35.3-28.7-64-64-64z"
  94. p-id="5173"
  95. ></path>
  96. <path
  97. d="M703.5 320h-384c-17.7 0-32-14.3-32-32s14.3-32 32-32h384c17.7 0 32 14.3 32 32s-14.3 32-32 32zM703.5 512h-384c-17.7 0-32-14.3-32-32s14.3-32 32-32h384c17.7 0 32 14.3 32 32s-14.3 32-32 32zM511.5 704h-192c-17.7 0-32-14.3-32-32s14.3-32 32-32h192c17.7 0 32 14.3 32 32s-14.3 32-32 32z"
  98. p-id="5174"
  99. ></path>
  100. </svg>
  101. <span>{{ uploadTemplateDocxData.name }}</span>
  102. <span class="d_b_s_f_c_del" @click="clearUploadTemplateDocxData()"
  103. >删除</span
  104. >
  105. </div>
  106. <el-button
  107. class="d_b_s_button"
  108. type="primary"
  109. :disabled="uploadTemplateDocxData != null"
  110. @click="uploadTemplateDocx()"
  111. >上传填入后的模板文档</el-button
  112. >
  113. </div>
  114. <div class="d_b_step">
  115. <h2>第四步:点击导出</h2>
  116. <p></p>
  117. <el-button
  118. class="d_b_s_button"
  119. type="primary"
  120. :disabled="!uploadTemplateDocxData"
  121. @click="exportDocx()"
  122. >导出Word文档</el-button
  123. >
  124. </div>
  125. </div>
  126. </div>
  127. </div>
  128. </el-dialog>
  129. </template>
  130. <script>
  131. import PizZip from "pizzip";
  132. import Docxtemplater from "docxtemplater";
  133. import ImageModule from "docxtemplater-image-module-free";
  134. import { saveAs } from 'file-saver';
  135. import topicVue from "../../testStudent/view/component/topic.vue";
  136. const getFile = url => {
  137. return new Promise((resolve, reject) => {
  138. var credentials = {
  139. accessKeyId: "AKIATLPEDU37QV5CHLMH",
  140. secretAccessKey: "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"
  141. }; //秘钥形式的登录上传
  142. window.AWS.config.update(credentials);
  143. window.AWS.config.region = "cn-northwest-1"; //设置区域
  144. let url2 = url;
  145. let _url2 = "";
  146. if (
  147. url2.indexOf("https://view.officeapps.live.com/op/view.aspx?src=") != -1
  148. ) {
  149. _url2 = url2.split(
  150. "https://view.officeapps.live.com/op/view.aspx?src="
  151. )[1];
  152. } else {
  153. _url2 = url2;
  154. }
  155. var s3 = new window.AWS.S3({ params: { Bucket: "ccrb" } });
  156. let name = decodeURIComponent(
  157. _url2.split("https://ccrb.s3.cn-northwest-1.amazonaws.com.cn/")[1]
  158. );
  159. var params = {
  160. Bucket: "ccrb",
  161. Key: name
  162. };
  163. s3.getObject(params, function(err, data) {
  164. if (err) {
  165. console.log(err, err.stack);
  166. resolve({ data: 1 });
  167. } else {
  168. resolve({ data: data.Body });
  169. console.log(data);
  170. }
  171. });
  172. });
  173. };
  174. export default {
  175. props: {},
  176. components:{
  177. topicVue
  178. },
  179. data() {
  180. return {
  181. show: false,
  182. loading: false,
  183. fieldList: [
  184. // { name: "第五题", field: "ti_05", type: "text", value: "第五题ti_05" },
  185. // {
  186. // name: "图片1",
  187. // field: "image_01",
  188. // type: "image",
  189. // value:
  190. // "https://ccrb.s3.cn-northwest-1.amazonaws.com.cn/default%2F%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_202410090922471728436994846.png"
  191. // }
  192. ],
  193. uploadTemplateDocxData: null, //上传的模板文档
  194. downFileData: null, // 下载模板文档的url
  195. checkJson:[],
  196. foldField:false,
  197. };
  198. },
  199. methods: {
  200. handleClose(done) {
  201. this.close();
  202. done();
  203. },
  204. open(data) {
  205. this.init();
  206. let _fileData = data.fileData;
  207. this.downFileData = _fileData;
  208. this.checkJson = data.formData.array
  209. this.fieldList = this.getFieldData(data.formData.array)
  210. this.show = true;
  211. },
  212. close() {
  213. this.show = false;
  214. this.init();
  215. },
  216. init() { // 初始化
  217. this.downFileData = null;
  218. this.fieldList = [];
  219. this.uploadTemplateDocxData = null;
  220. this.foldField = false;
  221. },
  222. getFieldData(array){
  223. let _list = [];
  224. let _index = 0;
  225. for(let i=0;i<array.length;i++){
  226. let _item = array[i];
  227. if(_item.type == 3){
  228. let _item2 = _item.json;
  229. _list.push({name: _item2.title, field: `ti_${_index}`, type: "text", value: _item2.answer2})
  230. _index++;
  231. }else if(_item.type==1){
  232. let _item2 = _item.json;
  233. let choseTxt = ``
  234. _item2.array.forEach((i,index2)=>{
  235. choseTxt += `${(_item2.answer2===index2 || _item2.answer2.includes(index2)?'☑':'□')}${i.option} `
  236. })
  237. _list.push({name:_item2.title,field: `ti_${_index}`, type: "text", value: choseTxt})
  238. _index++;
  239. }
  240. }
  241. return _list;
  242. },
  243. downloadTemplateDocx() {
  244. getFile(this.downFileData.url).then(data => {
  245. if (data.data != 1) {
  246. // 下载文件, 并存成ArrayBuffer对象
  247. const file_name = this.downFileData.fileName; // 获取文件名
  248. const file_data = data.data; // 获取文件数据
  249. let url = window.URL.createObjectURL(new Blob([file_data]));
  250. let a = document.createElement("a");
  251. a.name = file_name;
  252. a.href = url;
  253. a.download = file_name;
  254. a.click();
  255. console.log(data);
  256. this.$message.success("下载成功");
  257. } else {
  258. this.$message.error("下载失败");
  259. }
  260. });
  261. },
  262. uploadTemplateDocx() {
  263. let input = document.createElement("input");
  264. input.type = "file";
  265. // input.accept = ".wav";
  266. // input.accept = "audio/*, .txt, .pdf, .xlsx";
  267. input.accept = ".docx";
  268. input.click();
  269. input.onchange = () => {
  270. this.loading = true;
  271. let file = input.files[0];
  272. if (!/\.(docx)$/i.test(file.name)) {
  273. this.loading = false;
  274. return this.$message.error("请上传.docx格式的文件");
  275. }
  276. console.log(file);
  277. this.uploadTemplateDocxData = file;
  278. this.loading = false;
  279. // this.uploadWavFileAndGetText(file);
  280. };
  281. },
  282. async exportDocx() {
  283. if (!this.uploadTemplateDocxData)
  284. return this.$message.error("请先上传模板文档");
  285. let reader = new FileReader();
  286. reader.readAsArrayBuffer(this.uploadTemplateDocxData);
  287. reader.onload = async e => {
  288. try {
  289. this.loading = true;
  290. const binary = new Uint8Array(reader.result);
  291. //创建一个PizZip实例
  292. const zip = new PizZip(binary);
  293. // 将模板内容加载到 Docxtemplater 中
  294. const doc = new Docxtemplater().loadZip(zip);
  295. let _data = {};
  296. let _image = {};
  297. // 设置模板值
  298. // this.fieldList.forEach(i => {
  299. // _data[i.field] = i.value;
  300. // });
  301. for (let i = 0; i < this.fieldList.length; i++) {
  302. // console.log(this.fieldList[i])
  303. if (this.fieldList[i].type == "text") {
  304. //文本处理
  305. _data[this.fieldList[i].field] = this.fieldList[i].value;
  306. } else if (this.fieldList[i].type == "image") {
  307. //图片处理
  308. let _imageObj = await this.convertImageUrlToBase64(
  309. this.fieldList[i].value
  310. );
  311. _data[this.fieldList[i].field] = _imageObj.url;
  312. _image[this.fieldList[i].field] = {
  313. width: _imageObj.width,
  314. height: _imageObj.height
  315. };
  316. }
  317. }
  318. // return this.loading = false;
  319. // 图片处理
  320. const opts = {
  321. centered: false,
  322. fileType: "docx",
  323. getImage: (value, value2, value3) => {
  324. return this.base64DataURLToArrayBuffer(value);
  325. },
  326. getSize: (arrayValue, value, tagName) => {
  327. // console.log(_image)
  328. // console.log(tagName)
  329. let newWidth = _image[tagName].width;
  330. let newHeight = _image[tagName].height;
  331. // let newWidth = 550;
  332. // let newHeight = 100;
  333. return [newWidth, newHeight];
  334. }
  335. };
  336. doc.attachModule(new ImageModule(opts));
  337. //渲染模板
  338. doc.setData(_data);
  339. doc.render();
  340. //获取渲染后的文本
  341. const output = doc.getZip().generate({
  342. type: "blob",
  343. mimeType:
  344. "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
  345. compression: "DEFLATE"
  346. });
  347. saveAs(output,`${this.uploadTemplateDocxData.name}`)
  348. // let link = document.createElement("a");
  349. // link.download = this.uploadTemplateDocxData.name;
  350. // link.style.display = "none";
  351. // let blob = new Blob([output]);
  352. // link.href = URL.createObjectURL(blob);
  353. // document.body.appendChild(link);
  354. // link.click();
  355. // document.body.removeChild(link);
  356. this.loading = false;
  357. this.$message.success("导出成功");
  358. } catch (error) {
  359. console.log(error);
  360. this.loading = false;
  361. return this.$message.error("导出失败");
  362. }
  363. };
  364. },
  365. clearUploadTemplateDocxData() {
  366. this.uploadTemplateDocxData = null;
  367. },
  368. copyContent(content) {
  369. const input = document.createElement("input");
  370. // 设置 display为none会导致无法复制
  371. // input.style.display = "none";
  372. // 所以只能用其他方法隐藏
  373. input.style.opacity = 0;
  374. // 为了不影响布局
  375. input.style.position = "fixed";
  376. input.style.left = "-100%";
  377. input.style.top = "-100%";
  378. input.value = content;
  379. document.body.appendChild(input);
  380. input.select();
  381. const success = document.execCommand("copy");
  382. document.body.removeChild(input);
  383. if (!success) {
  384. return this.$message.error("复制失败");
  385. } else {
  386. return this.$message.success("复制成功");
  387. }
  388. },
  389. convertImageUrlToBase64(imageUrl) {
  390. return new Promise((resolve, reject) => {
  391. const img = new Image();
  392. img.crossOrigin = "Anonymous"; // 允许跨域请求
  393. img.src = imageUrl;
  394. img.onload = () => {
  395. const canvas = document.createElement("canvas");
  396. canvas.width = img.width;
  397. canvas.height = img.height;
  398. const ctx = canvas.getContext("2d");
  399. ctx.drawImage(img, 0, 0);
  400. const base64 = canvas.toDataURL("image/png");
  401. resolve({ url: base64, width: img.width, height: img.height });
  402. };
  403. img.onerror = error => {
  404. console.log("图片转base64失败")
  405. console.log(error)
  406. resolve({url:"",width:0,height:0});
  407. };
  408. });
  409. },
  410. base64DataURLToArrayBuffer(dataURL) {
  411. const base64Regex = /^data:image\/(png|jpg|svg|svg\+xml);base64,/;
  412. if (!base64Regex.test(dataURL)) {
  413. return false;
  414. }
  415. const stringBase64 = dataURL.replace(base64Regex, "");
  416. let binaryString;
  417. if (typeof window !== "undefined") {
  418. binaryString = window.atob(stringBase64);
  419. } else {
  420. binaryString = new Buffer(stringBase64, "base64").toString("binary");
  421. }
  422. const len = binaryString.length;
  423. const bytes = new Uint8Array(len);
  424. for (let i = 0; i < len; i++) {
  425. const ascii = binaryString.charCodeAt(i);
  426. bytes[i] = ascii;
  427. }
  428. return bytes.buffer;
  429. }
  430. }
  431. };
  432. </script>
  433. <style scoped>
  434. .dialog_diy >>> .el-dialog {
  435. /* height: 100%; */
  436. /* margin: 0 auto !important; */
  437. }
  438. .dialog_diy >>> .el-dialog__header {
  439. background: #fff !important;
  440. padding: 15px 20px;
  441. }
  442. .dialog_diy >>> .el-dialog__body {
  443. height: calc(100% - 124px);
  444. box-sizing: border-box;
  445. padding: 0px;
  446. }
  447. .dialog_diy >>> .el-dialog__title {
  448. color: #000;
  449. }
  450. .dialog_diy >>> .el-dialog__headerbtn {
  451. top: 19px;
  452. }
  453. .dialog_diy >>> .el-dialog__headerbtn .el-dialog__close {
  454. color: #000;
  455. }
  456. .dialog_diy >>> .el-dialog__headerbtn .el-dialog__close:hover {
  457. color: #000;
  458. }
  459. .dialog_diy >>> .el-dialog__body,
  460. .dialog_diy >>> .el-dialog__footer {
  461. background: #fff;
  462. }
  463. .box {
  464. width: 100%;
  465. height: 70vh;
  466. padding: 0 20px 15px;
  467. display: flex;
  468. box-sizing: border-box;
  469. }
  470. .b_left {
  471. flex: 1;
  472. height: 100%;
  473. }
  474. .b_right {
  475. min-width: 700px;
  476. max-width: 700px;
  477. height: 100%;
  478. }
  479. .d_box {
  480. width: 100%;
  481. height: 100%;
  482. overflow: auto;
  483. }
  484. .d_b_step {
  485. width: 100%;
  486. max-width: 100%;
  487. height: auto;
  488. padding: 20px;
  489. box-sizing: border-box;
  490. /* background-color: red; */
  491. }
  492. .d_b_step > h2 {
  493. margin-bottom: 10px;
  494. }
  495. .d_b_step>h3{
  496. margin-top: 20px;
  497. }
  498. .d_b_step > p {
  499. font-size: 16px;
  500. }
  501. .d_b_step > img {
  502. width: 100%;
  503. margin-top: 10px;
  504. }
  505. .d_b_s_button {
  506. margin-top: 10px;
  507. }
  508. .d_b_s_fieldListItem {
  509. margin-top: 15px;
  510. font-size: 16px;
  511. display: flex;
  512. align-items: center;
  513. }
  514. .d_b_s_fieldListItem > span {
  515. display: flex;
  516. align-items: center;
  517. justify-content: center;
  518. }
  519. .d_b_s_fieldListItem > span > svg {
  520. width: 15px;
  521. height: 15px;
  522. cursor: pointer;
  523. fill: #000;
  524. margin-left: 10px;
  525. transition: 0.3s;
  526. fill-opacity: 0.6;
  527. }
  528. .d_b_s_fieldListItem > span > svg:hover {
  529. fill: #409eff;
  530. fill-opacity: 1;
  531. }
  532. .d_b_s_fileCard {
  533. width: 100%;
  534. height: 45px;
  535. border-radius: 2px;
  536. margin-bottom: 10px;
  537. display: flex;
  538. align-items: center;
  539. box-sizing: border-box;
  540. padding: 0 10px;
  541. border: solid 1px #dfdfdf;
  542. position: relative;
  543. cursor: default;
  544. }
  545. .d_b_s_fileCard > svg {
  546. width: 30px;
  547. height: 30px;
  548. fill: #a3a8ac;
  549. margin-right: 10px;
  550. }
  551. .d_b_s_fileCard > span {
  552. max-width: calc(100% - 100px);
  553. overflow: hidden;
  554. text-overflow: ellipsis;
  555. white-space: nowrap;
  556. font-size: 16px;
  557. }
  558. .d_b_s_f_c_del {
  559. position: absolute;
  560. right: 10px;
  561. cursor: pointer;
  562. color: #e60012;
  563. }
  564. .d_b_s_imgList{
  565. margin-top: 10px;
  566. }
  567. .d_b_s_imgList>img{
  568. width: 100%;
  569. }
  570. .foldMenu{
  571. width: 100%;
  572. display: flex;
  573. justify-content: flex-end;
  574. margin: 10px 0;
  575. }
  576. .foldMenu>span{
  577. cursor: pointer;
  578. color: #409EFF;
  579. font-size: 16px;
  580. }
  581. </style>