func-tbook-export.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. import { replaceDocx, createZip, uploadFileToOSS, docxToPdf } from "../../lib/docs";
  2. // const Parse = global.Parse;
  3. const path = require("path")
  4. const fs = require("fs")
  5. var TemplateDocxPath = path.join(__dirname,"template/模板-推荐申报表.docx")
  6. if(!fs.existsSync(TemplateDocxPath)){
  7. TemplateDocxPath = path.join(__dirname,"../../template/模板-推荐申报表.docx")
  8. }
  9. /**
  10. * 定义导出申报合集文档云函数
  11. * @example
  12. * Cloud Code test
  13. 导出流程
  14. curl -X POST -H "Content-Type: application/json" -H 'X-Parse-Application-Id: edu-textbook' -d '{ "processId": "FR7KZtefyR" }' http://127.0.0.1:61337/parse/functions/tbookExportReport
  15. curl -X POST -H "Content-Type: application/json" -H 'X-Parse-Application-Id: edu-textbook' -d '{ "processId": "FR7KZtefyR" }' http://8.140.98.43/parse/functions/tbookExportReport
  16. 导出教材列表
  17. curl -X POST -H "Content-Type: application/json" -H 'X-Parse-Application-Id: edu-textbook' -d '{ "bookList": ["9V575dapEM","2YBKitpCJL","xLdiEaHGrX"] }' http://8.140.98.43/parse/functions/tbookExportReport
  18. */
  19. export function defineTbookExportReport(){
  20. Parse.Cloud.define("tbookExportReport", async (request) => {
  21. let processId = request.params.processId;
  22. let bookList = request.params.bookList;
  23. try{
  24. let result
  25. if(processId){
  26. result = await exportProcessReportDocs(processId)
  27. }
  28. if(bookList?.length){
  29. result = await exportProcessReportDocs(null,bookList)
  30. }
  31. if(result?.docsList?.length==0){
  32. throw new Parse.Error(404,"合集内无申报教材")
  33. }
  34. return result
  35. }catch(err){
  36. console.error(err)
  37. throw new Parse.Error(404,"导出申报合集失败")
  38. }
  39. throw new Parse.Error(404,"未找到该流程合集")
  40. },{
  41. fields : {
  42. processId:{
  43. required:false
  44. },
  45. }
  46. });
  47. }
  48. /**
  49. * 导出流程教材申报文件
  50. * @returns
  51. * docsList
  52. * zipUrl
  53. */
  54. export async function exportProcessReportDocs(processId,bookList) {
  55. if(!processId && !bookList?.length) return {}
  56. let textbookList
  57. if(processId){ // 流程读取教材列表
  58. let query = new Parse.Query("EduTextbook")
  59. query.equalTo("eduProcess",processId);
  60. textbookList = await query.find();
  61. }
  62. if(bookList?.length){ // 直接导出教材列表
  63. let query = new Parse.Query("EduTextbook")
  64. query.containedIn("objectId",bookList);
  65. textbookList = await query.find();
  66. }
  67. let docsList = []
  68. for (let index = 0; index < textbookList.length; index++) {
  69. let textbook = textbookList[index];
  70. let result = await renderReportDocsByTextbook(textbook);
  71. docsList.push(result)
  72. }
  73. let zipPath,zipUrl
  74. if(docsList?.length){
  75. let now = new Date();
  76. let zipName = `申报书导出-${now.getFullYear()}${now.getMonth()+1}${now.getDate()}-${now.getHours()}${now.getMinutes()}${now.getSeconds()}.zip`
  77. zipPath = await createZip(docsList?.map(item=>item?.filePath),zipName)
  78. if(zipPath){
  79. zipUrl = (await uploadFileToOSS(zipPath))?.url || null
  80. }
  81. docsList = docsList.map(item=>{return {code:item.code,title:item.title,url:item?.url}})
  82. }
  83. let result = {
  84. docsList,
  85. zipUrl
  86. }
  87. return result
  88. }
  89. module.exports.exportProcessReportDocs = exportProcessReportDocs
  90. function renderReportDocsByTextbook(textbook){
  91. let json = textbook.toJSON();
  92. console.log(json)
  93. // 圆圈选中未选 ○ 未选 ● 选中
  94. let circleCheck = ["○","●"];
  95. // 方块选中未选 ○ 未选 ● 选中
  96. let squareCheck = [``,`☑`];
  97. // 联系电话:默认为作者首个存在的电话;
  98. let mobile = json?.authorList?.find(item => item.mobile)?.mobile || ""
  99. // 填报时间:默认为创建时间
  100. let createdAt = new Date(textbook?.createdAt);
  101. let createdDate = `${createdAt?.getFullYear()}年${createdAt?.getMonth()+1}月${createdAt?.getDate()}日`;
  102. // 专业代码:前四位
  103. let majorCode = json?.majorId || json?.major?.code
  104. let majorCode6
  105. if(majorCode?.length>4){
  106. majorCode = majorCode.slice(0,4)
  107. majorCode6 = majorCode.slice(0,6)
  108. }
  109. let majorName = json?.majorName || json?.major?.name
  110. // 是否重点立项
  111. let importantProject = json?.importantProject?.join();
  112. let isJC = circleCheck[(json?.approval?.indexOf("基础")>-1)?1:0];
  113. let isZL = circleCheck[(json?.approval?.indexOf("战略")>-1)?1:0];
  114. let is101 = circleCheck[(json?.approval?.indexOf("101计划")>-1)?1:0]; // 2024新重点
  115. let isZY = circleCheck[(json?.approval?.indexOf("中央")>-1)?1:0];
  116. let isSX = circleCheck[(json?.approval?.indexOf("四新")>-1)?1:0];
  117. let isJS = circleCheck[(importantProject?.indexOf("建设")>-1)?1:0];
  118. let isNotImpt = (json?.approval?.indexOf("101计划")==-1) && (json?.approval?.indexOf("中央")==-1) && (json?.approval?.indexOf("四新")==-1) && (json?.approval?.indexOf("建设")==-1)
  119. isNotImpt = circleCheck[isNotImpt?1:0];
  120. // 初版时间
  121. let firstDate = new Date(textbook?.get("editionFirst"));
  122. let firstYear = firstDate?.getFullYear();
  123. let firstMonth = firstDate?.getMonth()+1;
  124. // 本版时间印次
  125. let currentDate = new Date(textbook?.get("editionDate"));
  126. let currentYear = currentDate?.getFullYear();
  127. let currentMonth = currentDate?.getMonth()+1;
  128. // 最新时间印次
  129. let latestDate = new Date(textbook?.get("printDate"));
  130. let latestYear = latestDate?.getFullYear();
  131. let latestMonth = latestDate?.getMonth()+1;
  132. // 初版至今重点项目
  133. let isBSQT = !((importantProject?.indexOf("建设")>-1) || (importantProject?.indexOf("本科国家")>-1) || (importantProject?.indexOf("省级优秀")>-1) || (importantProject?.indexOf("省级规划")>-1))// 是否其他省级奖项
  134. let bsqtName = (json?.importantProject || [])?.filter(item=>(item?.indexOf("建设")==-1&&item?.indexOf("本科国家")==-1&&item?.indexOf("省级优秀")==-1&&item?.indexOf("省级规划")==-1));
  135. // 教材适用
  136. let characteristic = (json?.characteristic?.filter(item=>item?.checked).map(item=>item.label) || [])?.join(",")
  137. // 作者列表 限6人
  138. let alist = []
  139. for (let index = 0; index < 9; index++) {
  140. alist[index] = [json?.authorList?.[index]?.name||" ",json?.authorList?.[index]?.unit||" ",toYearMonth(json?.authorList?.[index]?.birth)||" ",json?.authorList?.[index]?.nationality||" ",json?.authorList?.[index]?.job||" ",json?.authorList?.[index]?.title||" ",json?.authorList?.[index]?.mobile||" ",json?.authorList?.[index]?.email||" ",json?.authorList?.[index]?.work||" "]
  141. }
  142. // 相关成果 限5项
  143. let aclist = []
  144. for (let index = 0; index < 5; index++) {
  145. aclist[index] = [json?.achievementOptions?.[index]?.name||" ",json?.achievementOptions?.[index]?.unit||" ",toYearMonth(json?.achievementOptions?.[index]?.date)||" "]
  146. }
  147. // 申报历程 限4项
  148. let clist = []
  149. for (let index = 0; index < 4; index++) {
  150. let num = json?.courses?.[index]?(index+1):" ";
  151. clist[index] = [num,toYearMonth(json?.courses?.[index]?.date)||" ",json?.courses?.[index]?.wordage||" ",json?.courses?.[index]?.num||" ",json?.courses?.[index]?.sumNum||" ",json?.courses?.[index]?.accolade||" "]
  152. }
  153. let fixData = {
  154. // 封面信息
  155. titlePad:padString(json?.title,21),
  156. ISBNPad:padString(json?.ISBN,21),
  157. ISBN:json?.ISBN,
  158. one:squareCheck[(json?.type=="单本"||json?.type=="单册")?1:0], // 单本/单册 方框
  159. full:squareCheck[json?.type=="全册"?1:0], // 全册
  160. oneCircle:circleCheck[(json?.type=="单本"||json?.type=="单册")?1:0], // 单本/单册 圆圈
  161. fullCircle:circleCheck[json?.type=="全册"?1:0], // 全册
  162. tn:json?.typeNumber,
  163. authorPad:padString(json?.author,21),
  164. mobile:padString(mobile,21),
  165. authorUnit:padString(json?.unit,21),
  166. publisherPad:padString(json?.editionUnit,21),
  167. recommandUnit:padString("",14), // 未找到
  168. majorCodePad:padString((majorCode),14),
  169. createdDate:padString(createdDate,21),
  170. // 基本信息
  171. title:json?.title,
  172. author:json?.author,
  173. unit:json?.unit,
  174. mc:majorCode,
  175. mc6:majorCode6,
  176. mn:majorName,
  177. lCN:circleCheck[(json?.lang=="中文")?1:0],
  178. lEN:circleCheck[(json?.lang=="英文")?1:0],
  179. lOT:circleCheck[(json?.lang?.indexOf("其他")>-1)?1:0],
  180. lSS:circleCheck[(json?.lang?.indexOf("少数")>-1)?1:0],
  181. authors:json?.authors, // 其他主编
  182. editor:json?.editor, // 其他编者
  183. isJC:isJC,
  184. isZL:isZL,
  185. isSX:isSX,
  186. is101:is101,
  187. isZY:isZY,
  188. isJS:isJS,
  189. isNotImpt:isNotImpt,
  190. publisher:json?.editionUnit,
  191. firstYear:firstYear,
  192. firstMonth:firstMonth,
  193. isZZ:circleCheck[(json?.carrierShape?.indexOf("纸质")>-1)?1:0],
  194. isDZ:circleCheck[(json?.carrierShape?.indexOf("电子")>-1)?1:0],
  195. isSZ:circleCheck[(json?.carrierShape?.indexOf("数字")>-1)?1:0],
  196. isQT:circleCheck[(json?.carrierShape?.indexOf("附带")>-1)?1:0],
  197. isFD:circleCheck[(json?.carrierShape?.indexOf("其他")>-1)?1:0],
  198. latestY:latestYear,
  199. latestM:latestMonth,
  200. latestNum:json?.printNumber || "",
  201. currentY:currentYear,
  202. currentM:currentMonth,
  203. currentNum:json?.editionNumber || "",
  204. printSum:json?.printSum || "",
  205. isBGJ:circleCheck[(importantProject?.indexOf("本科国家")>-1)?1:0],
  206. isBSYX:circleCheck[(importantProject?.indexOf("省级优秀")>-1)?1:0],
  207. isBSGH:circleCheck[(importantProject?.indexOf("省级规划")>-1)?1:0],
  208. isBSQT:circleCheck[isBSQT?1:0],
  209. bsqtName:bsqtName||"",
  210. isFirstNot:circleCheck[json?.importantProject?0:1],
  211. // 教材适用情况
  212. lessons:json?.lessons||[],
  213. period:json?.period||"", // 学时
  214. isBX:squareCheck[(characteristic?.indexOf("必修")>-1)?1:0],
  215. isXX:squareCheck[(characteristic?.indexOf("选修")>-1)?1:0],
  216. isTS:circleCheck[(characteristic?.indexOf("通识")>-1)?1:0],
  217. isGG:circleCheck[(characteristic?.indexOf("公共")>-1)?1:0],
  218. isZY:circleCheck[(characteristic?.indexOf("专业")>-1)?1:0],
  219. isSXZZ:squareCheck[(characteristic?.indexOf("思想")>-1)?1:0],
  220. isSY:squareCheck[(characteristic?.indexOf("实验")>-1)?1:0],
  221. // 作者列表
  222. a11:alist[0][0],a12:alist[0][1],a13:alist[0][2],a14:alist[0][3],a15:alist[0][4],a16:alist[0][5],a17:alist[0][6],a18:alist[0][7],a19:alist[0][8],
  223. a21:alist[1][0],a22:alist[1][1],a23:alist[1][2],a24:alist[1][3],a25:alist[1][4],a26:alist[1][5],a27:alist[1][6],a28:alist[1][7],a29:alist[1][8],
  224. a31:alist[2][0],a32:alist[2][1],a33:alist[2][2],a34:alist[2][3],a35:alist[2][4],a36:alist[2][5],a37:alist[2][6],a38:alist[2][7],a39:alist[2][8],
  225. a41:alist[3][0],a42:alist[3][1],a43:alist[3][2],a44:alist[3][3],a45:alist[3][4],a46:alist[3][5],a47:alist[3][6],a48:alist[3][7],a49:alist[3][8],
  226. a51:alist[4][0],a52:alist[4][1],a53:alist[4][2],a54:alist[4][3],a55:alist[4][4],a56:alist[4][5],a57:alist[4][6],a58:alist[4][7],a59:alist[4][8],
  227. a61:alist[5][0],a62:alist[5][1],a63:alist[5][2],a64:alist[5][3],a65:alist[5][4],a66:alist[5][5],a67:alist[5][6],a68:alist[5][7],a69:alist[5][8],
  228. // 成果列表
  229. ac11:aclist[0][0],ac12:aclist[0][1],ac13:aclist[0][2],
  230. ac21:aclist[1][0],ac22:aclist[1][1],ac23:aclist[1][2],
  231. ac31:aclist[2][0],ac32:aclist[2][1],ac33:aclist[2][2],
  232. ac41:aclist[3][0],ac42:aclist[3][1],ac43:aclist[3][2],
  233. ac51:aclist[4][0],ac52:aclist[4][1],ac53:aclist[4][2],
  234. // 历程列表
  235. c11:clist[0][0],c12:clist[0][1],c13:clist[0][2],c14:clist[0][3],c15:clist[0][4],c16:clist[0][5],
  236. c21:clist[1][0],c22:clist[1][1],c23:clist[1][2],c24:clist[1][3],c25:clist[1][4],c26:clist[1][5],
  237. c31:clist[2][0],c32:clist[2][1],c33:clist[2][2],c34:clist[2][3],c35:clist[2][4],c36:clist[2][5],
  238. c41:clist[3][0],c42:clist[3][1],c43:clist[3][2],c44:clist[3][3],c45:clist[3][4],c46:clist[3][5],
  239. }
  240. let bookData = json;
  241. Object.keys(fixData).forEach(key=>{
  242. bookData[key] = fixData[key]
  243. })
  244. // console.log(bookData)
  245. // console.log(json)
  246. let bookid = json.code || json?.objectId;
  247. let tempFileName = path.join(`${bookid}${json.title}.docx`)
  248. return new Promise((resolve)=>{
  249. replaceDocx(TemplateDocxPath,tempFileName,bookData,{onDocxComplete:async (filePath)=>{
  250. // 需要API支持
  251. let pdfPath = filePath.replaceAll(".docx",".pdf")
  252. filePath = await docxToPdf(filePath,pdfPath) || filePath // 成功用pdf,失败继续用docx
  253. let url = (await uploadFileToOSS(pdfPath))?.url || null
  254. resolve({
  255. code:bookid,
  256. title:json?.title,
  257. pdfPath,
  258. url
  259. })
  260. }})
  261. })
  262. }
  263. function toYearMonth(date){
  264. if(!date) return ""
  265. date = new Date(date?.iso||date);
  266. return `${date.getFullYear()}年${date.getMonth()+1}月`
  267. }
  268. function padString(str,width) {
  269. str = str || ""
  270. str = String(str)
  271. width = width || 21 // 目标宽度为21个单位
  272. let spaceChar = "&#160;" // 占位符
  273. // 计算字符串的宽度
  274. const charWidth = {
  275. 'space': 1, // 空格占用1个单位
  276. 'zh': 2, // 汉字占用2个单位
  277. 'en': 1, // 英文字母占用1个单位
  278. 'other': 1 // 其他字符(如标点符号)占用1个单位
  279. };
  280. let strWidth = 0;
  281. console.log(str)
  282. // 遍历文本中的每个字符
  283. for (let char of str) {
  284. if (/\s/.test(char)) {
  285. strWidth += charWidth.space; // 空格
  286. } else if (/[\u4e00-\u9fa5]/.test(char)) {
  287. strWidth += charWidth.zh; // 汉字
  288. } else if (/[a-zA-Z]/.test(char)) {
  289. strWidth += charWidth.en; // 英文字母
  290. } else {
  291. strWidth += charWidth.other; // 其他字符
  292. }
  293. }
  294. // for (let char of str) {
  295. // // 判断字符是否为中文
  296. // if (char.match(/[\u4e00-\u9fa5]/)) {
  297. // strWidth += 2; // 中文字符占4个单位
  298. // } else {
  299. // strWidth += 1; // 英文字符占1个单位
  300. // }
  301. // }
  302. const totalPadding = width - strWidth;
  303. // 如果已经达到或超过目标宽度,直接返回原字符串
  304. if (totalPadding <= 0) {
  305. return str;
  306. }
  307. // 计算左右两侧的空格数
  308. const leftPadding = Math.floor(totalPadding / 2) * 3;
  309. const rightPadding = Math.ceil(totalPadding / 2) * 3;
  310. // 生成填充空格的字符串
  311. const leftSpaces = spaceChar.repeat(leftPadding);
  312. const rightSpaces = spaceChar.repeat(rightPadding);
  313. // 返回补充后的字符串
  314. return leftSpaces + str + rightSpaces;
  315. }