func-tbook-export.js 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649
  1. import { replaceDocx, createZip, uploadFileToOSS, docxToPdf, renderDocx, toBarCode } from "../../lib/docs";
  2. // const Parse = global.Parse;
  3. const path = require("path")
  4. const fs = require("fs")
  5. var TemplateDocxDir = path.join(__dirname,"template")
  6. if(!fs.existsSync(TemplateDocxDir)){
  7. TemplateDocxDir = path.join(__dirname,"../../template")
  8. }
  9. const tempDir = path.join(__dirname , "temp");
  10. /**
  11. * 常用Word字符
  12. */
  13. // 圆圈选中未选 ○ 未选 ● 选中
  14. const circleCheck = ["○","●"];
  15. // 方块选中未选 ○ 未选 ● 选中
  16. const squareCheck = [`□`,String.fromCharCode(0xFE)];
  17. /**
  18. * 定义导出申报合集文档云函数
  19. * @example
  20. * Cloud Code test
  21. 导出流程
  22. 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
  23. 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
  24. curl -X POST -H "Content-Type: application/json" -H 'X-Parse-Application-Id: edu-textbook' -d '{ "processId": "FR7KZtefyR" }' http://8.140.98.43/parse/api/tbook/export
  25. curl -X POST -H "Content-Type: application/json" -H 'X-Parse-Application-Id: edu-textbook' -d '{ "processId": "FR7KZtefyR" }' https://145.tbook.com.cn/parse/api/tbook/export
  26. 导出教材列表
  27. 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
  28. */
  29. export function defineTbookExportReport(app){
  30. // 全新导出,每次都重新导出
  31. app.post("/parse/api/tbook/export",async (request,res)=>{
  32. let processId = request.body.processId;
  33. let bookList = request.body.bookList;
  34. console.log(request.body)
  35. try{
  36. let result
  37. if(processId){
  38. result = await exportProcessReportDocs(processId)
  39. }
  40. if(bookList?.length){
  41. result = await exportProcessReportDocs(null,bookList)
  42. }
  43. if(result?.docsList?.length==0){
  44. // throw new Parse.Error(404,"合集内无申报教材")
  45. res.json({
  46. code:400,
  47. err:"合集内无申报教材"
  48. })
  49. return
  50. }
  51. // return result
  52. res.json({
  53. code:200,
  54. result:result
  55. })
  56. return
  57. }catch(err){
  58. console.error(err)
  59. res.json({
  60. code:400,
  61. err:err
  62. })
  63. return
  64. // throw new Parse.Error(404,"导出申报合集失败")
  65. }
  66. // throw new Parse.Error(404,"未找到该流程合集")
  67. })
  68. // 预览导出:已导出直接返回
  69. // curl -X POST -H "Content-Type: application/json" -H 'X-Parse-Application-Id: edu-textbook' -d '{ "bookid": "j4pt2MMTXM" }' https://145.tbook.com.cn/parse/api/tbook/preview
  70. app.post("/parse/api/tbook/preview",async (request,res)=>{
  71. let bookid = request.body.bookid;
  72. let isNew = request.body.isNew || false;
  73. try{
  74. let result
  75. if(bookid){
  76. let book
  77. if(!isNew){
  78. let query = new Parse.Query("EduTextbook")
  79. query.include("childrens","department")
  80. book = await query.get(bookid);
  81. }
  82. if(book?.get("export")){
  83. res.json({
  84. code:200,
  85. data:{
  86. urlPdf:book?.get("export")
  87. }
  88. })
  89. return
  90. }
  91. result = await exportProcessReportDocs(null,[bookid])
  92. let urlPdf = result?.docsList?.[0]?.urlPdf;
  93. if(urlPdf){
  94. book.set('export',urlPdf);
  95. book.save();
  96. res.json({
  97. code:200,
  98. data:{
  99. urlPdf:urlPdf
  100. }
  101. })
  102. return
  103. }
  104. }
  105. console.log(result)
  106. if(result?.docsList?.length==0){
  107. // throw new Parse.Error(404,"合集内无申报教材")
  108. res.json({
  109. code:400,
  110. err:"无该教材"
  111. })
  112. return
  113. }
  114. // return result
  115. res.json({
  116. code:200,
  117. result:result
  118. })
  119. return
  120. }catch(err){
  121. console.error(err)
  122. res.json({
  123. code:400,
  124. err:err
  125. })
  126. return
  127. // throw new Parse.Error(404,"导出申报合集失败")
  128. }
  129. // throw new Parse.Error(404,"未找到该流程合集")
  130. })
  131. Parse.Cloud.define("tbookExportReport", async (request) => {
  132. let processId = request.params.processId;
  133. let bookList = request.params.bookList;
  134. try{
  135. let result
  136. if(processId){
  137. result = await exportProcessReportDocs(processId)
  138. }
  139. if(bookList?.length){
  140. result = await exportProcessReportDocs(null,bookList)
  141. }
  142. if(result?.docsList?.length==0){
  143. throw new Parse.Error(404,"合集内无申报教材")
  144. }
  145. return result
  146. }catch(err){
  147. console.error(err)
  148. throw new Parse.Error(404,"导出申报合集失败")
  149. }
  150. throw new Parse.Error(404,"未找到该流程合集")
  151. },{
  152. fields : {
  153. processId:{
  154. required:false
  155. },
  156. }
  157. });
  158. }
  159. /**
  160. * 导出流程教材申报文件
  161. * @returns
  162. * docsList
  163. * zipUrl
  164. */
  165. export async function exportProcessReportDocs(processId,bookList) {
  166. if(!processId && !bookList?.length) return {}
  167. let textbookList
  168. if(processId){ // 流程读取教材列表
  169. let query = new Parse.Query("EduTextbook")
  170. query.include("childrens","department")
  171. query.equalTo("recommend",true);
  172. query.equalTo("eduProcess",processId);
  173. textbookList = await query.find();
  174. }
  175. if(bookList?.length){ // 直接导出教材列表
  176. let query = new Parse.Query("EduTextbook")
  177. query.include("childrens","department")
  178. query.containedIn("objectId",bookList);
  179. textbookList = await query.find();
  180. }
  181. let docsList = []
  182. let plist = []
  183. for (let index = 0; index < textbookList.length; index++) {
  184. let textbook = textbookList[index];
  185. console.log("textbook",index)
  186. // 直接将异步调用的 Promise 添加到 plist
  187. plist.push(renderReportDocsByTextbook(textbook)); // 立即执行并返回 Promise
  188. }
  189. let presults = await Promise.all(plist);
  190. presults.forEach(result=>{
  191. if(result?.filePath){
  192. docsList.push(result)
  193. }
  194. })
  195. let zipPath,zipUrl
  196. if(docsList?.length){
  197. let now = new Date();
  198. let fileList = docsList?.map(item=>item?.pdfPath);
  199. let zipName = `申报书导出-${now.getFullYear()}${now.getMonth()+1}${now.getDate()}-${now.getHours()}${now.getMinutes()}${now.getSeconds()}.zip`
  200. zipPath = await createZip(fileList,zipName)
  201. if(zipPath){
  202. zipUrl = (await uploadFileToOSS(zipPath))?.url || null
  203. }
  204. docsList = docsList.map(item=>{return {code:item.code,title:item.title,url:item?.url,urlPdf:item?.urlPdf}})
  205. }
  206. let result = {
  207. docsList,
  208. zipUrl
  209. }
  210. return result
  211. }
  212. module.exports.exportProcessReportDocs = exportProcessReportDocs
  213. function fixVolumeData(json,volumeData,index){
  214. json.index = index + 1;
  215. // 教材基本信息
  216. // 专业代码:前四位 discipline
  217. let majorCode4 = volumeData?.discipline?.code?.slice(0,4) || json?.discipline?.code?.slice(0,4) || ""
  218. let majorName4 = volumeData?.discipline?.name || json?.discipline?.name || ""
  219. // 专业代码:前六位 major
  220. let majorCode6 = volumeData?.major?.code?.slice(0,6) || json?.major?.code?.slice(0,6) || ""
  221. let majorName6 = volumeData?.major?.name || json?.major?.name || ""
  222. // 初版时间
  223. let firstDate = new Date(volumeData?.editionFirst.iso);
  224. let firstYear = firstDate?.getFullYear();
  225. let firstMonth = firstDate?.getMonth()+1;
  226. // 本版时间印次
  227. let currentDate = new Date(volumeData?.editionDate.iso);
  228. let currentYear = currentDate?.getFullYear();
  229. let currentMonth = currentDate?.getMonth()+1;
  230. // 最新时间印次
  231. let latestDate = new Date(volumeData?.printDate.iso);
  232. let latestYear = latestDate?.getFullYear();
  233. let latestMonth = latestDate?.getMonth()+1;
  234. let latestNum = volumeData?.printNumber || "";
  235. let currentNum = volumeData?.editionNumber || "";
  236. let printSum = volumeData?.printSum?volumeData?.printSum+"万":"" || "";
  237. // 是否重点立项
  238. let importantProject = volumeData?.importantProject?.join();
  239. // 初版至今重点项目
  240. let isBSQT = !((importantProject?.indexOf("建设")>-1) || (importantProject?.indexOf("本科国家")>-1) || (importantProject?.indexOf("省级优秀")>-1) || (importantProject?.indexOf("省级规划")>-1))// 是否其他省级奖项
  241. let bsqtName = (volumeData?.importantProject || [])?.filter(item=>(item?.indexOf("建设")==-1&&item?.indexOf("本科国家")==-1&&item?.indexOf("省级优秀")==-1&&item?.indexOf("省级规划")==-1));
  242. json.importantProject = importantProject
  243. json.firstDate = firstDate
  244. json.firstYear = firstYear
  245. json.firstMonth = firstMonth
  246. json.currentDate = currentDate
  247. json.currentYear = currentYear
  248. json.currentMonth = currentMonth
  249. json.latestDate = latestDate
  250. json.latestYear = latestYear
  251. json.latestMonth = latestMonth
  252. json.currentNum = currentNum
  253. json.latestMonth = latestMonth
  254. json.latestNum = latestNum
  255. json.printSum = printSum
  256. json.isBSQT = isBSQT
  257. json.bsqtName = bsqtName
  258. json.uBS = circleCheck[(volumeData?.unitType?.indexOf("部属")>-1)?1:0];
  259. json.uHJ = circleCheck[(volumeData?.unitType?.indexOf("合建")>-1)?1:0];
  260. json.uGJ = circleCheck[(volumeData?.unitType?.indexOf("共建")>-1)?1:0];
  261. json.uQT = circleCheck[(volumeData?.unitType?.indexOf("其他")>-1)?1:0];
  262. json.mc4 = majorCode4;
  263. json.majorCodePad = majorCode4;
  264. json.mn4 = majorName4;
  265. json.mc6 = majorCode6;
  266. json.mn6 = majorName6;
  267. json.lCN = circleCheck[(volumeData?.lang=="中文")?1:0];
  268. json.lEN = circleCheck[(volumeData?.lang=="英文")?1:0];
  269. json.lMW = circleCheck[(volumeData?.lang=="盲文")?1:0];
  270. json.lOT = circleCheck[(volumeData?.lang?.indexOf("其他")>-1)?1:0];
  271. json.lSS = circleCheck[(volumeData?.lang?.indexOf("少数")>-1)?1:0];
  272. json.publisher = volumeData?.editionUnit;
  273. json.isZZ = circleCheck[(volumeData?.carrierShape?.indexOf("纸质")>-1)?1:0];
  274. json.isDZ = circleCheck[(volumeData?.carrierShape?.indexOf("电子")>-1)?1:0];
  275. json.isSZ = circleCheck[(volumeData?.carrierShape?.indexOf("数字")>-1 || volumeData?.carrierShape?.indexOf("电子")>-1)?1:0];
  276. json.isQT = circleCheck[(volumeData?.carrierShape?.indexOf("附带")>-1)?1:0];
  277. json.isFD = circleCheck[(volumeData?.carrierShape?.indexOf("其他")>-1)?1:0];
  278. json.latestY = latestYear;
  279. json.latestM = latestMonth;
  280. json.currentY = currentYear;
  281. json.currentM = currentMonth;
  282. console.log(importantProject)
  283. json.isGJS = circleCheck[(importantProject?.indexOf("建设")>-1)?1:0];
  284. json.isBGJ = circleCheck[(importantProject?.indexOf("本科国家")>-1)?1:0];
  285. json.isBSYX = circleCheck[(importantProject?.indexOf("省级优秀")>-1)?1:0];
  286. json.isBSGH = circleCheck[(importantProject?.indexOf("省级规划")>-1)?1:0];
  287. json.isBSQT = circleCheck[isBSQT&&bsqtName?1:0];
  288. json.isFirstNot = circleCheck[volumeData?.importantProject?0:1];
  289. // 作者列表 + 政治审查表
  290. json.examineList = []
  291. let examineMap = {}
  292. for (let index = 0; index < 6; index++) { // 补充空值
  293. if(!volumeData?.authorList?.[index]){
  294. volumeData.authorList.push({name:"",unit:"",birth:"",nationality:"",job:"",title:"",mobile:"",email:"",work:""})
  295. }
  296. }
  297. volumeData.authorList = volumeData?.authorList?.map((item,index)=>{ // 转换格式
  298. item.index = index + 1
  299. item.birth = toYearMonth(item?.birth)
  300. if(item?.examine){
  301. examineMap[item?.examine] = true;
  302. }
  303. return item
  304. })
  305. json.authorList = volumeData?.authorList;
  306. json.examineList = Object.keys(examineMap)
  307. // 成果列表
  308. // aclist[index] = [json?.achievementOptions?.[index]?.name||" ",json?.achievementOptions?.[index]?.unit||" ",toYearMonth(json?.achievementOptions?.[index]?.date)||" "]
  309. for (let index = 0; index < 5; index++) { // 补充空值
  310. if(!volumeData?.achievementOptions?.[index]){
  311. volumeData.achievementOptions.push({name:"",unit:"",date:""})
  312. }
  313. volumeData.achievementOptions = volumeData?.achievementOptions?.map((item,index)=>{ // 转换格式
  314. item.index = index + 1
  315. item.date = toYearMonth(item?.date)
  316. return item
  317. })
  318. }
  319. json.achievementOptions = volumeData?.achievementOptions;
  320. // 历程列表
  321. // clist[index] = [num,toYearMonth(json?.courses?.[index]?.date)||" ",json?.courses?.[index]?.wordage||" ",json?.courses?.[index]?.num||" ",json?.courses?.[index]?.sumNum||" ",json?.courses?.[index]?.accolade||" "]
  322. for (let index = 0; index < 4; index++) { // 补充空值
  323. if(!volumeData?.courses?.[index]){
  324. volumeData.courses.push({date:"",wordage:"",num:"",sumNum:"",accolade:""})
  325. }
  326. volumeData.courses = volumeData?.courses?.map((item,index)=>{ // 转换格式
  327. item.index = index + 1
  328. item.date = toYearMonth(item?.date)
  329. return item
  330. })
  331. }
  332. json.courses = volumeData?.courses;
  333. return json
  334. }
  335. function renderReportDocsByTextbook(textbook){
  336. console.log("renderReportDocsByTextbook")
  337. return new Promise(async (resolve)=>{
  338. let json = textbook.toJSON();
  339. let bookid = json.code || json?.objectId;
  340. // 默认单册为第一分册
  341. let book1Data = json?.childrens?.[0]
  342. if(book1Data){
  343. Object.keys(book1Data).forEach(key=>{
  344. json[key] = book1Data[key] || json[key] || ""
  345. })
  346. }
  347. // 联系电话:默认为作者首个存在的电话;
  348. let mobile = json?.authorList?.find(item => item.mobile)?.mobile || ""
  349. // 填报时间:默认为创建时间
  350. let createdAt = new Date(textbook?.createdAt);
  351. let createdDate = `${createdAt?.getFullYear()}年${createdAt?.getMonth()+1}月${createdAt?.getDate()}日`;
  352. // 教材适用
  353. let characteristic = (json?.characteristic?.filter(item=>item?.checked).map(item=>item.label) || [])?.join(",")
  354. json = fixVolumeData(json,book1Data,0)
  355. let volumeList = []
  356. for (let index = 0; index < 12; index++) {
  357. if(json?.childrens?.[index]){
  358. let volumeData = json?.childrens?.[index]
  359. volumeData = fixVolumeData(volumeData,volumeData,index)
  360. volumeList.push(volumeData)
  361. }
  362. }
  363. // 作者列表 限6人
  364. let alist = []
  365. for (let index = 0; index < 9; index++) {
  366. 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||" "]
  367. }
  368. // 相关成果 限5项
  369. let aclist = []
  370. for (let index = 0; index < 5; index++) {
  371. aclist[index] = [json?.achievementOptions?.[index]?.name||" ",json?.achievementOptions?.[index]?.unit||" ",toYearMonth(json?.achievementOptions?.[index]?.date)||" "]
  372. }
  373. // 申报历程 限4项
  374. let clist = []
  375. for (let index = 0; index < 4; index++) {
  376. let num = json?.courses?.[index]?(index+1):" ";
  377. clist[index] = [num,toYearMonth(json?.courses?.[index]?.date)||" ",json?.courses?.[index]?.wordage||" ",json?.courses?.[index]?.num||" ",json?.courses?.[index]?.sumNum||" ",json?.courses?.[index]?.accolade||" "]
  378. }
  379. // 附件信息
  380. let mergeFiles = []
  381. // 1.教材电子版(必须提供)
  382. // (教材出版单位配合按要求上传各地推荐的本单位出版的纸质教材最新印次的完整PDF电子版;数字教材上传全部教材内容电子版或填写能够查看全部教材内容的链接地址、账号;纸质教材附带数字资源的,上传纸质教材最新印次的完整PDF电子版,以及全部数字资源电子版或能够查看全部数字资源内容的链接地址、账号。)
  383. // 2.所有作者政治审查意见(必须提供)
  384. // (对应作者姓名上传“作者政治审查表”。作者单位党委对作者进行审查,对政治思想表现情况进行评价,确保作者的正确政治方向、价值取向,无违法违纪等记录。教材编写成员涉及多个不同单位时需要各单位分别出具意见,并由所在单位党委盖章,格式要求从申报平台下载。)
  385. // console.log("json.examineList");
  386. // console.log("json.examineList");
  387. // console.log("json.examineList");
  388. // console.log("json.examineList");
  389. // console.log(json.examineList);
  390. if(json?.approvedImgUrl){
  391. mergeFiles.push(json?.approvedImgUrl);
  392. }
  393. json.examineList.forEach(fileurl=>{
  394. if(fileurl) mergeFiles.push(fileurl)
  395. })
  396. // 3.图书编校质量自查结果记录表(必须提供)
  397. // (教材出版单位对申报教材的编校质量自查后,按要求提供图书编校质量自查结果记录表,并加盖出版社公章。全册教材的不同分册以不同文件分别上传。格式要求从申报平台下载。)
  398. if(json?.selfResults?.url){mergeFiles.push(json?.selfResults?.url)}
  399. // 4.专家审查意见表(必须提供)
  400. // (由第一主编所在单位或出版机构邀请校内外相关学科专业领域专家,对教材进行思想性、学术性审查。专家不少于3名,其中半数以上为校外专家,专家分别实名评价并签字,并注明所在单位及专业身份。评价人不得是本教材的作者。)
  401. if(json?.expertOpinion?.url){mergeFiles.push(json?.expertOpinion?.url)}
  402. // 5.教材使用情况证明材料(必须提供)
  403. // (教材出版单位提供教材主要使用高校名单及使用情况证明材料,并加盖公章。)
  404. if(json?.evidence?.url){mergeFiles.push(json?.evidence?.url)}
  405. // 6.版权信息及CIP数据(必须提供)
  406. // (版权页截图,中国版本图书馆CIP查询截图,如CIP数据中无“教材”字样的,须再上传内容提要或前言或后记中可以证明本书为教材的相关内容截图。)
  407. if(json?.CIPImgUrl){mergeFiles.push(json?.CIPImgUrl)}
  408. // 7.其他材料(可选提供)
  409. if(json?.moreMaterial?.length){
  410. json?.moreMaterial.forEach(doc=>{
  411. if(doc?.url){
  412. mergeFiles.push(doc?.url)
  413. }
  414. })
  415. }
  416. let codePngBuffer = await toBarCode(json?.code);
  417. let codePngPath = path.join(tempDir,bookid+"code.png")
  418. fs.writeFileSync(codePngPath,codePngBuffer)
  419. // let codeBarImg =
  420. // console.log(codeBarImg)
  421. // (其他佐证材料,限两份以内。)
  422. console.log(json?.approval)
  423. let isNotImpt = (json?.approval?.indexOf("101计划")==-1) && (json?.approval?.indexOf("中央")==-1) && (json?.approval?.indexOf("四新")==-1) && (json?.approval?.indexOf("建设")==-1);
  424. let fixData = {
  425. volumeList:volumeList,
  426. // 图片信息
  427. codeBarImg:codePngPath,
  428. // 封面信息
  429. titlePad:padString(json?.title,21),
  430. ISBNPad:padString(json?.ISBN,21),
  431. ISBN:json?.ISBN,
  432. inviteUnit:json?.inviteUnit||json?.department?.name||"",
  433. one:squareCheck[(json?.type=="单本"||json?.type=="单册")?1:0], // 单本/单册 方框
  434. full:squareCheck[json?.type=="全册"?1:0], // 全册
  435. oneCircle:circleCheck[(json?.type=="单本"||json?.type=="单册")?1:0], // 单本/单册 圆圈
  436. fullCircle:circleCheck[json?.type=="全册"?1:0], // 全册
  437. tn:json?.typeNumber,
  438. authorPad:padString(json?.author,21),
  439. mobile:padString(mobile,21),
  440. authorUnit:padString(json?.unit,21),
  441. publisherPad:padString(json?.editionUnit,21),
  442. createdDate:padString(createdDate,21),
  443. // 基本信息
  444. title:json?.title,
  445. isJC: circleCheck[(json?.approval?.indexOf("基础")>-1)?1:0],
  446. isZL: circleCheck[(json?.approval?.indexOf("战略")>-1)?1:0],
  447. is101: circleCheck[(json?.approval?.indexOf("101计划")>-1)?1:0], // 2024新重点
  448. isZY: circleCheck[(json?.approval?.indexOf("中央")>-1)?1:0],
  449. isSX: circleCheck[(json?.approval?.indexOf("四新")>-1)?1:0],
  450. isJS: circleCheck[(json?.approval?.indexOf("建设")>-1)?1:0],
  451. isNotImpt: circleCheck[isNotImpt?1:0],
  452. // 教材适用情况
  453. lessons:json?.lessons||[],
  454. period:json?.period||"", // 学时
  455. isBX:squareCheck[(characteristic?.indexOf("必修")>-1)?1:0],
  456. isXX:squareCheck[(characteristic?.indexOf("选修")>-1)?1:0],
  457. isTS:circleCheck[(characteristic?.indexOf("通识")>-1)?1:0],
  458. isGG:circleCheck[(characteristic?.indexOf("公共")>-1)?1:0],
  459. isZYK:circleCheck[(characteristic?.indexOf("专业")>-1)?1:0],
  460. isSXZZ:squareCheck[(characteristic?.indexOf("思想")>-1)?1:0],
  461. isSY:squareCheck[(characteristic?.indexOf("实验")>-1)?1:0],
  462. // 作者列表
  463. 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],
  464. 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],
  465. 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],
  466. 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],
  467. 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],
  468. 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],
  469. // 成果列表
  470. ac11:aclist[0][0],ac12:aclist[0][1],ac13:aclist[0][2],
  471. ac21:aclist[1][0],ac22:aclist[1][1],ac23:aclist[1][2],
  472. ac31:aclist[2][0],ac32:aclist[2][1],ac33:aclist[2][2],
  473. ac41:aclist[3][0],ac42:aclist[3][1],ac43:aclist[3][2],
  474. ac51:aclist[4][0],ac52:aclist[4][1],ac53:aclist[4][2],
  475. // 历程列表
  476. c11:clist[0][0],c12:clist[0][1],c13:clist[0][2],c14:clist[0][3],c15:clist[0][4],c16:clist[0][5],
  477. c21:clist[1][0],c22:clist[1][1],c23:clist[1][2],c24:clist[1][3],c25:clist[1][4],c26:clist[1][5],
  478. c31:clist[2][0],c32:clist[2][1],c33:clist[2][2],c34:clist[2][3],c35:clist[2][4],c36:clist[2][5],
  479. c41:clist[3][0],c42:clist[3][1],c43:clist[3][2],c44:clist[3][3],c45:clist[3][4],c46:clist[3][5],
  480. }
  481. let bookData = json;
  482. Object.keys(fixData).forEach(key=>{
  483. bookData[key] = fixData[key] || bookData[key] || ""
  484. })
  485. // console.log(bookData)
  486. // console.log(json)
  487. let tempFileName = path.join(`${bookid}_${json.title}_申报书及附件.docx`)
  488. // 开始文件合并导出
  489. let filePath,pdfPath,urlDocx,urlPdf
  490. let TemplateDocxPath
  491. if(json?.childrens?.length>1){
  492. TemplateDocxPath = path.join(TemplateDocxDir,"模板-本科教材申报书-全册.docx")
  493. }else{
  494. TemplateDocxPath = path.join(TemplateDocxDir,"模板-本科教材申报书-单册.docx")
  495. }
  496. try{
  497. // DOCX模板合成 速度2-3秒
  498. filePath = renderDocx(TemplateDocxPath,tempFileName,bookData)
  499. urlDocx = (await uploadFileToOSS(filePath))?.url || null
  500. console.log("DOCX CREATED:",filePath)
  501. // PDF文档拼接 速度10-30s 需要API支持
  502. pdfPath = filePath.replaceAll(".docx",".pdf")
  503. let options = {
  504. mergeFiles:mergeFiles
  505. }
  506. pdfPath = await docxToPdf(filePath,pdfPath,options) || filePath // 成功用pdf,失败继续用docx
  507. console.log("PDF CREATED:",filePath)
  508. if(pdfPath){
  509. urlPdf = (await uploadFileToOSS(pdfPath))?.url || null
  510. }
  511. }catch(err){
  512. console.error(err)
  513. }
  514. resolve({
  515. code:bookid,
  516. title:json?.title,
  517. filePath,
  518. pdfPath,
  519. urlDocx,
  520. urlPdf,
  521. })
  522. return
  523. replaceDocx(TemplateDocxPath,tempFileName,bookData,{onDocxComplete:async (filePath)=>{
  524. // 需要API支持
  525. let pdfPath = filePath.replaceAll(".docx",".pdf")
  526. filePath = await docxToPdf(filePath,pdfPath) || filePath // 成功用pdf,失败继续用docx
  527. let url = (await uploadFileToOSS(pdfPath))?.url || null
  528. resolve({
  529. code:bookid,
  530. title:json?.title,
  531. filePath,
  532. url
  533. })
  534. }})
  535. })
  536. }
  537. function toYearMonth(date){
  538. // console.log("toYearMonth",date)
  539. if(typeof date == "string" && (date?.indexOf("年")>-1 || date?.indexOf("-")>-1)) return date
  540. if(!date) return ""
  541. let datestr = date?.iso||date
  542. if(!datestr) return ""
  543. date = new Date(datestr);
  544. // return `${date.getFullYear()}年${date.getMonth()+1}月`
  545. return `${date.getFullYear()}-${get2bitint(date.getMonth()+1)}`
  546. }
  547. function get2bitint(value){
  548. if(value>=10) return value;
  549. if(value>=0&&value<10) return "0" + value;
  550. return value
  551. }
  552. function padString(str,width) {
  553. str = str || ""
  554. return str
  555. str = String(str)
  556. width = width || 21 // 目标宽度为21个单位
  557. let spaceChar = " " // 占位符 render可用空格,但document.xml用&#160;
  558. // 计算字符串的宽度
  559. const charWidth = {
  560. 'space': 1, // 空格占用1个单位
  561. 'zh': 3, // 汉字占用3个单位
  562. 'en': 1, // 英文字母占用1个单位
  563. 'other': 1 // 其他字符(如标点符号)占用1个单位
  564. };
  565. let strWidth = 0;
  566. // console.log(str)
  567. // 遍历文本中的每个字符
  568. for (let char of str) {
  569. if (/\s/.test(char)) {
  570. strWidth += charWidth.space; // 空格
  571. } else if (/[\u4e00-\u9fa5]/.test(char)) {
  572. strWidth += charWidth.zh; // 汉字
  573. } else if (/[a-zA-Z]/.test(char)) {
  574. strWidth += charWidth.en; // 英文字母
  575. } else {
  576. strWidth += charWidth.other; // 其他字符
  577. }
  578. }
  579. // for (let char of str) {
  580. // // 判断字符是否为中文
  581. // if (char.match(/[\u4e00-\u9fa5]/)) {
  582. // strWidth += 2; // 中文字符占4个单位
  583. // } else {
  584. // strWidth += 1; // 英文字符占1个单位
  585. // }
  586. // }
  587. const totalPadding = width - strWidth;
  588. // 如果已经达到或超过目标宽度,直接返回原字符串
  589. if (totalPadding <= 0) {
  590. return str;
  591. }
  592. // 计算左右两侧的空格数
  593. const leftPadding = Math.floor(totalPadding / 2) * 3;
  594. const rightPadding = Math.ceil(totalPadding / 2) * 3;
  595. // 生成填充空格的字符串
  596. const leftSpaces = spaceChar.repeat(leftPadding);
  597. const rightSpaces = spaceChar.repeat(rightPadding);
  598. // 返回补充后的字符串
  599. return leftSpaces + str + rightSpaces;
  600. }