dataList.vue 116 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724
  1. <template>
  2. <div class="data-list">
  3. <el-table
  4. :data="dataList"
  5. style="width: 100%"
  6. header-row-class-name="headerBg"
  7. empty-text="没有项目信息"
  8. >
  9. <el-table-column prop="id" label="项目编号" align="center" width="80" />
  10. <el-table-column prop="customer_id" label="客户名称" align="left">
  11. <template #default="scope">
  12. <span>{{ getCusomName(scope.row.customer_id) }}</span>
  13. </template>
  14. </el-table-column>
  15. <el-table-column prop="name" label="项目名称" align="left" />
  16. <el-table-column prop="categoryId" label="产品类别" align="left">
  17. <template #default="scope">
  18. <span>{{ getCategoryTypeName(scope.row.product_id) }}</span>
  19. </template>
  20. </el-table-column>
  21. <el-table-column prop="productName" label="产品名称" align="left">
  22. <template #default="scope">
  23. <span>{{ getCategoryName(scope.row.product_id) }}</span>
  24. </template>
  25. </el-table-column>
  26. <el-table-column prop="service_content" label="服务内容" align="left" />
  27. <el-table-column prop="start_date" label="项目开始日期" align="center" />
  28. <el-table-column prop="updateTime" label="最后更新日期" align="center" />
  29. <el-table-column prop="calculate" label="完成度" align="center">
  30. <template #default="scope">
  31. <span :class="getCompletionClass(scope.row.calculate)">
  32. {{ formatCompletion(scope.row.calculate) }}
  33. </span>
  34. </template>
  35. </el-table-column>
  36. <el-table-column prop="current_phase" label="当前阶段" align="left">
  37. <template #default="scope">
  38. <el-tag :type="getPhaseType(scope.row.current_phase)" effect="plain">
  39. {{ getPhaseName(scope.row.current_phase) }}
  40. </el-tag>
  41. </template>
  42. </el-table-column>
  43. <el-table-column prop="project_status" label="目前状态" align="center">
  44. <template #default="scope">
  45. <el-tag
  46. :type="getStatusType(scope.row.project_status)"
  47. effect="plain"
  48. >
  49. {{ getStatusName(scope.row.project_status) }}
  50. </el-tag>
  51. </template>
  52. </el-table-column>
  53. <el-table-column label="操作" align="center" fixed="right">
  54. <template #default="scope">
  55. <el-dropdown :data="scope.row" @command="handleCommand">
  56. <span class="el-dropdown-link">
  57. <el-button
  58. type="text"
  59. size="small"
  60. circle
  61. class="operation-button"
  62. style="font-size: 15px; margin-left: 10px"
  63. icon="el-icon-more"
  64. />
  65. </span>
  66. <template #dropdown>
  67. <el-dropdown-menu>
  68. <el-dropdown-item
  69. :command="{ type: 'template', row: scope.row }"
  70. >选择模版</el-dropdown-item>
  71. <el-dropdown-item
  72. :command="{ type: 'enter', row: scope.row }"
  73. >输入项目</el-dropdown-item>
  74. <el-dropdown-item
  75. v-if="
  76. scope.row.documentIds && scope.row.documentIds.length > 0
  77. "
  78. :command="{ type: 'viewTemplates', row: scope.row }"
  79. >
  80. 已绑定模版
  81. </el-dropdown-item>
  82. <el-dropdown-item :command="{ type: 'edit', row: scope.row }">
  83. <svg-icon icon-class="edit" />编辑
  84. </el-dropdown-item>
  85. <el-dropdown-item :command="{ type: 'delete', row: scope.row }">
  86. <svg-icon icon-class="delete" />删除
  87. </el-dropdown-item>
  88. </el-dropdown-menu>
  89. </template>
  90. </el-dropdown>
  91. </template>
  92. <!-- <template #default="scope">
  93. <div class="btns">
  94. <el-button type="primary" size="small" @click="selTemplate(scope.row)"
  95. >选择模版</el-button
  96. >
  97. <el-button type="primary" size="small" @click="enterPro(scope.row)"
  98. >输入项目</el-button
  99. >
  100. <el-button type="primary" size="small" @click="btnEdit(scope.row)"
  101. ><svg-icon icon-class="edit" />编辑</el-button
  102. >
  103. <el-button
  104. type="danger"
  105. size="small"
  106. @click="btnDelete(scope.row.id)"
  107. ><svg-icon icon-class="delete" />删除</el-button
  108. >
  109. </div>
  110. </template> -->
  111. </el-table-column>
  112. </el-table>
  113. <div class="page-info">
  114. <el-pagination
  115. :current-page="queryForm.page"
  116. :page-size="queryForm.pageSize"
  117. :total="recordCount"
  118. :page-count="pageTotal"
  119. background
  120. layout="prev, pager, next"
  121. @current-change="ChangePage"
  122. />
  123. </div>
  124. <el-dialog
  125. v-el-drag-dialog
  126. :visible.sync="dialogVisible"
  127. append-to-body
  128. custom-class="prod-verify"
  129. title="创建项目信息"
  130. >
  131. <dataInfo :id="currentDataId" @onClose="onClose" />
  132. </el-dialog>
  133. <!-- 选择模版 -->
  134. <el-dialog
  135. title="选择模版"
  136. :visible.sync="templateDialogVisible"
  137. width="50%"
  138. append-to-body
  139. :close-on-click-modal="false"
  140. top="5%"
  141. >
  142. <div class="dialog-header" style="margin-bottom: 20px">
  143. <!-- 左侧按钮 -->
  144. <div class="header-left">
  145. <el-button
  146. type="primary"
  147. @click="confirmTemplateSelection"
  148. >确 定</el-button>
  149. <el-button @click="closeTemplateDialog">取 消</el-button>
  150. </div>
  151. <!-- 右侧选择框 -->
  152. <div class="header-right">
  153. <el-select
  154. v-model="templateQueryForm.document_type"
  155. placeholder="请选择模板类型"
  156. style="width: 200px"
  157. @change="handleDocTypeChange"
  158. >
  159. <el-option
  160. v-for="item in documentTypes"
  161. :key="item.value"
  162. :label="item.label"
  163. :value="item.value"
  164. />
  165. </el-select>
  166. </div>
  167. </div>
  168. <!-- 添加文档类型筛选 -->
  169. <!-- <div class="filter-section" style="margin-bottom: 20px;">
  170. <el-select
  171. v-model="templateQueryForm.document_type"
  172. placeholder="请选择文档类型"
  173. @change="handleDocTypeChange"
  174. style="width: 200px;"
  175. >
  176. <el-option
  177. v-for="item in documentTypes"
  178. :key="item.value"
  179. :label="item.label"
  180. :value="item.value"
  181. />
  182. </el-select>
  183. </div> -->
  184. <el-table
  185. ref="templateTable"
  186. v-loading="templateLoading"
  187. :data="templateList"
  188. :max-height="600"
  189. element-loading-text="加载中..."
  190. element-loading-spinner="el-icon-loading"
  191. element-loading-background="rgba(255, 255, 255, 0.8)"
  192. @selection-change="handleSelectionChange"
  193. @select="handleSelect"
  194. >
  195. <el-table-column type="selection" width="55" />
  196. <el-table-column prop="id" label="ID" width="80" align="center" />
  197. <el-table-column prop="title" label="模版名称" align="left" />
  198. <el-table-column prop="create_time" label="创建时间" align="center" />
  199. </el-table>
  200. <div class="pagination-container">
  201. <el-pagination
  202. :current-page="templateQueryForm.page"
  203. :page-sizes="pageSizes"
  204. :page-size="templateQueryForm.pageSize"
  205. layout="total, sizes, prev, pager, next, jumper"
  206. :total="templateTotal"
  207. @size-change="handleSizeChange"
  208. @current-change="handleTemplatePageChange"
  209. />
  210. </div>
  211. <div slot="footer" class="dialog-footer">
  212. <!-- <span style="float: left; line-height: 32px;">
  213. 已选择 {{ getSelectedCount() }} 项
  214. </span> -->
  215. </div>
  216. </el-dialog>
  217. <el-dialog
  218. v-el-drag-dialog
  219. title="已绑定模版"
  220. :visible.sync="boundTemplatesDialogVisible"
  221. width="50%"
  222. append-to-body
  223. :close-on-click-modal="false"
  224. >
  225. <el-table
  226. ref="boundTemplateTable"
  227. :data="boundTemplateList"
  228. style="width: 100%"
  229. @selection-change="handleBoundTemplateSelect"
  230. >
  231. <el-table-column type="selection" width="55" />
  232. <el-table-column prop="id" label="ID" width="80" align="center" />
  233. <el-table-column prop="title" label="模版名称" align="left" />
  234. <el-table-column prop="createTime" label="绑定时间" align="center" />
  235. <el-table-column label="操作" align="center" width="320">
  236. <template #default="scope">
  237. <el-button
  238. type="primary"
  239. @click="previewTemplate(scope.row)"
  240. >预览</el-button>
  241. <!-- <el-button
  242. type="primary"
  243. @click="replaceTemplate(scope.row)"
  244. >替换</el-button> -->
  245. <el-dropdown
  246. style="margin-left: 10px"
  247. trigger="click"
  248. @command="(command) => handleExport(command, scope.row)"
  249. >
  250. <el-button type="success">
  251. 导出文档<i class="el-icon-arrow-down el-icon--right" />
  252. </el-button>
  253. <template #dropdown>
  254. <el-dropdown-menu>
  255. <el-dropdown-item command="word">Word格式</el-dropdown-item>
  256. <el-dropdown-item command="pdf">PDF格式</el-dropdown-item>
  257. </el-dropdown-menu> </template><!-- word -->
  258. <!-- <el-dropdown-item command="pdf-to-word"
  259. >Word</el-dropdown-item
  260. > -->
  261. </el-dropdown>
  262. </template>
  263. </el-table-column>
  264. </el-table>
  265. <div class="dialog-footer" style="margin-top: 20px; text-align: right">
  266. <el-button
  267. type="primary"
  268. :loading="batchReplaceLoading"
  269. :disabled="selectedBoundTemplates.length === 0"
  270. @click="batchReplace"
  271. >
  272. 批量替换
  273. </el-button>
  274. </div>
  275. </el-dialog>
  276. <el-dialog
  277. title=""
  278. :visible.sync="replaceVisible"
  279. width="50%"
  280. append-to-body
  281. >
  282. <!-- 显示匹配 -->
  283. <div v-if="matchResults.length" class="match-results">
  284. <div
  285. v-for="(item, index) in matchResults"
  286. :key="index"
  287. class="match-item"
  288. >
  289. <div class="match-content">
  290. <span>原始值: {{ item }}</span>
  291. <!-- <span>替换后: {{ item }}</span> -->
  292. </div>
  293. <div class="actions">
  294. <el-button size="small" @click="replaceItem(item)">替换</el-button>
  295. </div>
  296. </div>
  297. <div style="display: flex; justify-content: flex-end; margin-top: 20px">
  298. <el-button type="primary" @click="replaceAll">批量替换</el-button>
  299. </div>
  300. </div>
  301. </el-dialog>
  302. <!-- 添加选择文档弹窗 -->
  303. <el-dialog
  304. title="选择文档版本"
  305. :visible.sync="documentSelectVisible"
  306. width="50%"
  307. append-to-body
  308. :close-on-click-modal="false"
  309. >
  310. <el-table
  311. v-loading="documentVersionsLoading"
  312. :data="documentVersions"
  313. style="width: 100%"
  314. >
  315. <el-table-column prop="id" label="ID" width="80" align="center" />
  316. <el-table-column prop="title" label="文档标题" align="left" />
  317. <el-table-column prop="createTime" label="创建时间" align="center" />
  318. <el-table-column label="操作" align="center" width="250">
  319. <template #default="scope">
  320. <el-button
  321. type="primary"
  322. size="small"
  323. style="margin-right: 10px"
  324. @click="handleDocPreview(scope.row)"
  325. >
  326. 预览
  327. </el-button>
  328. <el-dropdown
  329. trigger="click"
  330. @command="(command) => handleDocExport(command, scope.row)"
  331. >
  332. <el-button type="success" size="small">
  333. 导出<i class="el-icon-arrow-down el-icon--right" />
  334. </el-button>
  335. <template #dropdown>
  336. <el-dropdown-menu>
  337. <el-dropdown-item command="word">Word格式</el-dropdown-item>
  338. <el-dropdown-item command="pdf">PDF格式</el-dropdown-item>
  339. </el-dropdown-menu>
  340. </template>
  341. </el-dropdown>
  342. </template>
  343. </el-table-column>
  344. </el-table>
  345. </el-dialog>
  346. </div>
  347. </template>
  348. <script>
  349. import {
  350. searchProject,
  351. deleteProject,
  352. searchCategory,
  353. relate,
  354. searchTemplateList,
  355. searchTemplateListNoData,
  356. getstatus,
  357. calculate,
  358. getphases
  359. } from '@/api/project'
  360. import {
  361. searchList,
  362. exportToPDF,
  363. htmlWordNew,
  364. createDocument,
  365. searchlistDoc,
  366. getDocumentInfo,
  367. updateDocument,
  368. searchlistDocTemplate
  369. } from '@/api/document'
  370. import { searchCustomer } from '@/api/customer'
  371. import { searchProduct } from '@/api/product'
  372. /* import {searchTemplate} from "@/api/template" */
  373. import dataInfo from './dataInfo.vue'
  374. import elDragDialog from '@/directive/el-drag-dialog'
  375. import axios from 'axios'
  376. import htmlDocx from 'html-docx-js/dist/html-docx' // 需要先安装: npm install html-docx-js
  377. import { saveAs } from 'file-saver' // 需要先安装: npm install file-saver
  378. export default {
  379. components: {
  380. dataInfo
  381. },
  382. directives: { elDragDialog },
  383. props: {
  384. queryForm: {
  385. type: Object,
  386. default: () => {
  387. return {
  388. page: 1,
  389. pageSize: 10,
  390. name: '',
  391. category_id: '',
  392. status: ''
  393. }
  394. }
  395. }
  396. },
  397. data() {
  398. return {
  399. templateQueryForm: {
  400. page: 1,
  401. pageSize: 10, // 默认每页显示10条
  402. document_type: '' // 添加文档类型字段
  403. },
  404. pageSizes: [10, 20, 50, 100], // 每页显示条数选项
  405. templateTotal: 0, // 模版总数
  406. dialogVisible: false, // 控制产品审核
  407. currentDataId: 0,
  408. recordCount: 0,
  409. pageTotal: 1,
  410. dataList: [],
  411. currentData: {},
  412. categoryList: [],
  413. templateDialogVisible: false,
  414. templateList: [],
  415. selectedTemplates: [], // 改为数组以支持多选
  416. boundTemplatesDialogVisible: false,
  417. previewDialogVisible: false,
  418. boundTemplateList: [],
  419. templatePreviewContent: '',
  420. currentTemplateId: null,
  421. replaceVisible: false,
  422. loading: false, // 增加 loading 状态
  423. searchResult: {},
  424. matchResults: [], // 存储匹配到的结果
  425. TemList: [],
  426. docAttr: {},
  427. templateLoading: false, // 添加模版加载状态
  428. boundTemplateLoading: false, // 添加已绑定模版加载状态
  429. selectedBoundTemplates: [], // 新增:已选择的绑定模版
  430. batchReplaceLoading: false, // 新增:批量替换loading状态
  431. getstatusList: [], // 状态列表
  432. customList: [], // 客户列表
  433. productList: [], // 产品列表
  434. currentPhaseList: [], // 阶段列表
  435. selectedTemplateIds: new Set(), // 存储所有选中的模板ID
  436. selectedPageIds: new Map(), // 存储每页的选择状态
  437. isAllSelected: false, // 是否全选
  438. selectionCache: new Map(), // 添加选择缓存
  439. // 添加文档类型选项
  440. documentTypes: [
  441. { label: '全部', value: '' },
  442. { label: '临床', value: 'Clinical' },
  443. { label: '非临床', value: 'NonClinical' }
  444. ],
  445. documentSelectVisible: false, // 文档选择弹窗显示状态
  446. documentVersions: [], // 文档版本列表
  447. documentVersionsLoading: false, // 加载状态
  448. currentTemplate: null // 当前选中的模板
  449. }
  450. },
  451. computed: {
  452. tableColumns() {
  453. return [
  454. // ... 其他列配置 ...
  455. {
  456. prop: 'calculate',
  457. label: '完成度',
  458. width: '100',
  459. render: (h, { row }) => {
  460. // 使用过渡效果显示完成度
  461. return h(
  462. 'el-tooltip',
  463. {
  464. props: {
  465. content: '加载中...',
  466. disabled: row.calculate !== 0
  467. }
  468. },
  469. [
  470. h(
  471. 'span',
  472. {
  473. class: [
  474. this.getCompletionClass(row.calculate),
  475. 'completion-text'
  476. ]
  477. },
  478. [
  479. row.calculate === 0
  480. ? h('i', { class: 'el-icon-loading' })
  481. : this.formatCompletion(row.calculate)
  482. ]
  483. )
  484. ]
  485. )
  486. }
  487. }
  488. ]
  489. }
  490. },
  491. watch: {
  492. queryForm: {
  493. handler: function(val) {
  494. this.search()
  495. },
  496. // immediate: true,//立即执行
  497. deep: true
  498. }
  499. },
  500. mounted() {
  501. this.search()
  502. this.init()
  503. },
  504. methods: {
  505. /* 使用html-docx-js方法替换 */
  506. // 预处理内容,处理分页和表格
  507. preprocessContent(content) {
  508. if (!content) return ''
  509. const tempDiv = document.createElement('div')
  510. tempDiv.innerHTML = content
  511. // 处理分页符
  512. tempDiv
  513. .querySelectorAll('div[style*="page-break-after"]')
  514. .forEach((pageBreak) => {
  515. const newPageBreak = document.createElement('br')
  516. newPageBreak.style.pageBreakBefore = 'always'
  517. newPageBreak.style.clear = 'both'
  518. newPageBreak.className = 'WordSection'
  519. pageBreak.parentNode.replaceChild(newPageBreak, pageBreak)
  520. })
  521. // 处理图片
  522. tempDiv.querySelectorAll('img').forEach((img) => {
  523. // 保持原有尺寸或设置默认尺寸
  524. const width = img.getAttribute('width') || img.style.width || '595px'
  525. const height = img.getAttribute('height') || img.style.height || 'auto'
  526. img.style.width = width
  527. img.style.height = height
  528. })
  529. // 处理表格
  530. tempDiv.querySelectorAll('table').forEach((table) => {
  531. if (this.isTableWithBorder(table)) {
  532. table.setAttribute('data-code', 'active')
  533. } else {
  534. table.setAttribute('data-code', 'noactive')
  535. }
  536. })
  537. return tempDiv.innerHTML
  538. },
  539. // 更新导出方法
  540. async exportDocumentWithHtmlDocx(template) {
  541. const res = await searchlistDocTemplate({
  542. project_id: this.currentData.id,
  543. id: template.id
  544. })
  545. if(res.status !== 200) return
  546. let templateData
  547. try {
  548. if(res.data.documents.length !== 0) {
  549. templateData = JSON.parse(res.data.documents[0].data || '[]')
  550. } else {
  551. const template_Data = await getDocumentInfo({id:template.id})
  552. if(template_Data.status !== 200) return
  553. templateData = JSON.parse(template_Data.data.data || '[]')
  554. }
  555. const contentContainer = document.createElement('div')
  556. contentContainer.className = 'content'
  557. // 处理每个模版段落
  558. templateData.forEach((item) => {
  559. const templateSection = document.createElement('div')
  560. templateSection.className = 'template-textarea'
  561. templateSection.innerHTML = this.preprocessContent(item.content)
  562. contentContainer.appendChild(templateSection)
  563. })
  564. let contentData = `
  565. <!DOCTYPE html>
  566. <html xmlns:v="urn:schemas-microsoft-com:vml"
  567. xmlns:o="urn:schemas-microsoft-com:office:office"
  568. xmlns:w="urn:schemas-microsoft-com:office:word">
  569. <head>
  570. <meta charset="UTF-8">
  571. <xml>
  572. <w:WordDocument>
  573. <w:View>Print</w:View>
  574. <w:Zoom>100</w:Zoom>
  575. <w:DoNotOptimizeForBrowser/>
  576. <w:SpaceForUL/>
  577. <w:BrowserLevel>MicrosoftInternetExplorer4</w:BrowserLevel>
  578. </w:WordDocument>
  579. </xml>
  580. <style>
  581. /* 基础样式 */
  582. body {
  583. font-family: "Times New Roman", SimSun;
  584. line-height: 1.0; /* 单倍行距 */
  585. }
  586. /* 段落样式 */
  587. p {
  588. margin-top: 0.5em; /* 段前0.5行 */
  589. margin-bottom: 0.5em; /* 段后0.5行 */
  590. line-height: 1.0 !important; /* 强制单倍行距 */
  591. mso-line-height-rule: exactly; /* Word特定的行距规则 */
  592. }
  593. /* 保持表格内容行距正常 */
  594. table p {
  595. margin: 0;
  596. line-height: inherit;
  597. }
  598. /* 列表样式 */
  599. ul, ol {
  600. margin-top: 0.5em;
  601. margin-bottom: 0.5em;
  602. line-height: 1.0;
  603. }
  604. li {
  605. line-height: 1.0;
  606. margin: 0;
  607. }
  608. /* 保持原有表格样式 */
  609. table {
  610. width: 100%;
  611. border-collapse: collapse !important;
  612. margin: 8px 0;
  613. }
  614. /* 有边框表格样式 */
  615. table[data-code="active"] {
  616. border: 1px solid black !important;
  617. border-collapse: collapse !important;
  618. }
  619. table[data-code="active"] td,
  620. table[data-code="active"] th {
  621. border: 1px solid black !important;
  622. padding: 6px !important;
  623. mso-border-alt: solid black .5pt !important;
  624. mso-border-insideh: .5pt solid black !important;
  625. mso-border-insidev: .5pt solid black !important;
  626. }
  627. /* 无边框表格样式 */
  628. table[data-code="noactive"] {
  629. border: none !important;
  630. border-collapse: collapse !important;
  631. }
  632. table[data-code="noactive"] td,
  633. table[data-code="noactive"] th {
  634. border: none !important;
  635. padding: 6px !important;
  636. }
  637. /* Word特定的表格边框样式 */
  638. .MsoNormalTable {
  639. mso-style-name: "普通表格";
  640. mso-tstyle-rowband-size: 0;
  641. mso-tstyle-colband-size: 0;
  642. mso-padding-alt: 0cm 5.4pt 0cm 5.4pt;
  643. }
  644. </style>
  645. </head>
  646. <body>
  647. ${this.processContentForWord(contentContainer.innerHTML)}
  648. </body>
  649. </html>
  650. `
  651. // 添加Word特定的分页处理
  652. contentData = contentData.replace(
  653. /<br class="WordSection"[^>]*>/g,
  654. '<br clear="all" style="page-break-before:always; mso-break-type:section-break">'
  655. )
  656. const converted = htmlDocx.asBlob(contentData, {
  657. orientation: 'portrait',
  658. margins: {
  659. top: '2.54cm',
  660. right: '2.54cm',
  661. bottom: '2.54cm',
  662. left: '2.54cm'
  663. },
  664. // 添加Word特定的文档属性
  665. docProperties: {
  666. 'w:sectPr': {
  667. 'w:type': { _attr: { 'w:val': 'nextPage' }}
  668. }
  669. }
  670. })
  671. saveAs(converted, `${template.title}.docx`)
  672. } catch (error) {
  673. console.error('导出Word文档失败:', error)
  674. throw new Error('导出文档时发生错误,请稍后重试')
  675. }
  676. },
  677. /* 处理图片 */
  678. htmlProcessImages(content) {
  679. const tempDiv = document.createElement('div')
  680. tempDiv.innerHTML = content
  681. // 处理所有图片
  682. tempDiv.querySelectorAll('img').forEach((img) => {
  683. // 创建图片容器
  684. const container = document.createElement('div')
  685. container.className = 'image-container'
  686. // 获取所有可能的尺寸来源
  687. let width =
  688. img.getAttribute('width') || img.style.width || img.width || null
  689. let height =
  690. img.getAttribute('height') || img.style.height || img.height || null
  691. // 移除单位并转换为数字
  692. if (typeof width === 'string') {
  693. width = parseInt(width.replace(/[^0-9]/g, ''))
  694. }
  695. if (typeof height === 'string') {
  696. height = parseInt(height.replace(/[^0-9]/g, ''))
  697. }
  698. // 如果没有尺寸,创建一个临时图片来获取实际尺寸
  699. if (!width || !height) {
  700. const tempImg = new Image()
  701. tempImg.src = img.src
  702. width = tempImg.width || 595 // 默认宽度
  703. height = tempImg.height || Math.round(width * 0.75) // 默认高度比例
  704. }
  705. // 确保数值有效
  706. width = Math.max(1, parseInt(width))
  707. height = Math.max(1, parseInt(height))
  708. // 处理尺寸
  709. const maxWidth = 595 // A4纸宽度(像素,减去边距)
  710. if (width > maxWidth) {
  711. // 等比例缩放
  712. const aspectRatio = width / height
  713. const newHeight = Math.round(maxWidth / aspectRatio)
  714. width = maxWidth
  715. height = newHeight
  716. }
  717. // 同时设置style和属性
  718. img.style.width = `${width}px`
  719. img.style.height = `${height}px`
  720. img.setAttribute('width', width)
  721. img.setAttribute('height', height)
  722. // 将图片包装在容器中
  723. img.parentNode.insertBefore(container, img)
  724. container.appendChild(img)
  725. })
  726. return tempDiv.innerHTML
  727. },
  728. // 预处理表格
  729. preprocessTables(content) {
  730. const tempDiv = document.createElement('div')
  731. tempDiv.innerHTML = content
  732. // 处理所有表格
  733. tempDiv.querySelectorAll('table').forEach((table, index) => {
  734. if (this.isTableWithBorder(table)) {
  735. // 处理有边框表格
  736. this.processTableWithBorder(table, index)
  737. } else {
  738. // 处理无边框表格
  739. this.processTableNoBorder(table, index)
  740. }
  741. })
  742. return tempDiv.innerHTML
  743. },
  744. // 处理有边框表格
  745. processTableWithBorder(table, index) {
  746. // 创建表格容器
  747. const container = document.createElement('div')
  748. container.className = 'table-container'
  749. // 获取表头行
  750. const headerRow = table.querySelector('tr')
  751. const hasHeader = headerRow && headerRow.querySelector('th')
  752. // 复制原始表格并添加必要的类和属性
  753. const clonedTable = table.cloneNode(true)
  754. clonedTable.className = 'table-with-border continuous-table'
  755. clonedTable.setAttribute('data-table-id', `table-${index}`)
  756. if (hasHeader) {
  757. // 保存表头信息
  758. const headerHTML = headerRow.outerHTML
  759. clonedTable.setAttribute('data-header', headerHTML)
  760. clonedTable.setAttribute('data-repeat-header', 'true')
  761. }
  762. // 处理表格的每一行
  763. clonedTable.querySelectorAll('tr').forEach((row, rowIndex) => {
  764. row.setAttribute('data-row-index', rowIndex)
  765. row.style.pageBreakInside = 'avoid'
  766. })
  767. // 将处理后的表格放入容器
  768. container.appendChild(clonedTable)
  769. table.parentNode.replaceChild(container, table)
  770. },
  771. // 处理无边框表格
  772. processTableNoBorder(table, index) {
  773. // 创建表格容器
  774. const container = document.createElement('div')
  775. container.className = 'table-container-no-border'
  776. // 复制原始表格并添加必要的类和属性
  777. const clonedTable = table.cloneNode(true)
  778. clonedTable.className = 'table-no-border'
  779. clonedTable.setAttribute('data-table-id', `table-no-border-${index}`)
  780. // 处理表格的每一行,保持原有样式
  781. clonedTable.querySelectorAll('tr').forEach((row) => {
  782. const originalRowStyles = row.getAttribute('style') || ''
  783. row.style.cssText = `
  784. page-break-inside: avoid;
  785. ${originalRowStyles}
  786. `
  787. // 处理单元格,保持原有样式
  788. row.querySelectorAll('td, th').forEach((cell) => {
  789. const originalStyles = cell.getAttribute('style') || ''
  790. cell.style.cssText = `
  791. border: none !important;
  792. padding: 6px;
  793. word-wrap: break-word;
  794. overflow: hidden;
  795. ${originalStyles}
  796. `
  797. })
  798. })
  799. // 将处理后的表格放入容器
  800. container.appendChild(clonedTable)
  801. table.parentNode.replaceChild(container, table)
  802. },
  803. // 处理表格内容
  804. processContentForWord(content) {
  805. const tempDiv = document.createElement('div')
  806. tempDiv.innerHTML = content
  807. // 处理所有表格
  808. tempDiv.querySelectorAll('table').forEach((table) => {
  809. // 检查表格是否有边框
  810. const hasBorder = this.detectTableBorder(table)
  811. if (hasBorder) {
  812. // 为有边框的表格添加必要的属性和类
  813. table.setAttribute('data-code', 'active')
  814. table.setAttribute('class', 'MsoNormalTable')
  815. table.setAttribute('border', '1')
  816. table.style.borderCollapse = 'collapse'
  817. // 处理所有单元格
  818. table.querySelectorAll('td, th').forEach((cell) => {
  819. // 保存原有样式
  820. const originalStyles = cell.getAttribute('style') || ''
  821. // 添加Word特定的边框样式
  822. cell.style.cssText = `
  823. border: 1px solid black !important;
  824. mso-border-alt: solid black .5pt !important;
  825. padding: 6px !important;
  826. ${originalStyles}
  827. `
  828. })
  829. } else {
  830. // 处理无边框表格
  831. table.setAttribute('data-code', 'noactive')
  832. table.removeAttribute('border')
  833. table.style.border = 'none'
  834. // 移除所有单元格边框
  835. table.querySelectorAll('td, th').forEach((cell) => {
  836. cell.style.border = 'none'
  837. cell.style.padding = '6px'
  838. })
  839. }
  840. })
  841. return tempDiv.innerHTML
  842. },
  843. // 添加检测表格边框的方法
  844. detectTableBorder(table) {
  845. // 1. 检查表格本身的边框属性
  846. if (
  847. table.getAttribute('border') ||
  848. /border(?!-collapse)/.test(table.style.cssText) ||
  849. table.classList.contains('bordered') ||
  850. table.hasAttribute('rules')
  851. ) {
  852. return true
  853. }
  854. // 2. 检查计算样式
  855. const computedStyle = window.getComputedStyle(table)
  856. if (
  857. computedStyle.border !== 'none' ||
  858. computedStyle.borderCollapse === 'collapse'
  859. ) {
  860. return true
  861. }
  862. // 3. 检查第一个单元格的边框
  863. const firstCell = table.querySelector('td, th')
  864. if (firstCell) {
  865. const cellStyle = window.getComputedStyle(firstCell)
  866. return (
  867. cellStyle.border !== 'none' ||
  868. cellStyle.borderTop !== 'none' ||
  869. cellStyle.borderBottom !== 'none' ||
  870. cellStyle.borderLeft !== 'none' ||
  871. cellStyle.borderRight !== 'none'
  872. )
  873. }
  874. return false
  875. },
  876. // 添加处理分的辅助方法
  877. addPageBreaks(content) {
  878. // 查找需要分页的位置(可以根据实际需求调整)
  879. const pageBreakMarkers = ['<h1', '<h2', '<!-- pagebreak -->']
  880. pageBreakMarkers.forEach((marker) => {
  881. content = content.replace(
  882. new RegExp(marker, 'g'),
  883. `<div class="page-break"></div>${marker}`
  884. )
  885. })
  886. return content
  887. },
  888. // 处理表格边框的辅助方法
  889. processTableBorders(table) {
  890. const isInfoTable = this.isInformationTable(table)
  891. if (isInfoTable) {
  892. table.classList.add('info-table')
  893. } else {
  894. // 检查表格是否需要边框
  895. const needsBorder = this.checkTableNeedsBorder(table)
  896. if (needsBorder) {
  897. table.style.border = '1px solid black'
  898. table.querySelectorAll('td, th').forEach((cell) => {
  899. cell.style.border = '1px solid black'
  900. })
  901. }
  902. }
  903. },
  904. // 检查表格是否需要边框
  905. checkTableNeedsBorder(table) {
  906. // 检查表格第一行是否包含特定标记或样式
  907. const firstRow = table.querySelector('tr')
  908. if (!firstRow) return false
  909. // 可以根据实际需求添加更多的判断条件
  910. return (
  911. firstRow.querySelector('td[style*="border"]') !== null ||
  912. table.getAttribute('border') === '1' ||
  913. table.classList.contains('bordered')
  914. )
  915. },
  916. // 添加内容处方法
  917. processContentForWord(content) {
  918. const tempDiv = document.createElement('div')
  919. tempDiv.innerHTML = content
  920. // 添加表格分页标识
  921. let tableCounter = 0
  922. // 处理表格
  923. tempDiv.querySelectorAll('table').forEach((table) => {
  924. tableCounter++
  925. const tableId = `table_${tableCounter}`
  926. table.setAttribute('data-table-id', tableId)
  927. // 判断表格类型
  928. const isInfoTable = this.isInformationTable(table)
  929. const isContinuousTable = this.isContinuousTable(table)
  930. // 基本表格样式
  931. table.style.width = '100%'
  932. table.style.borderCollapse = 'collapse'
  933. table.style.tableLayout = 'fixed'
  934. if (isInfoTable) {
  935. // 信息表格处理逻辑保持不变
  936. table.classList.add('info-table')
  937. this.processInfoTable(table)
  938. } else if (isContinuousTable) {
  939. // 处理跨页连续表格
  940. table.classList.add('continuous-table')
  941. this.processContinuousTable(table, tableCounter)
  942. } else {
  943. // 普通表格处理逻辑保持不变
  944. this.processNormalTable(table)
  945. }
  946. })
  947. // 添加表格连续性样式
  948. const styleElement = document.createElement('style')
  949. styleElement.textContent = this.generateTableStyles(tableCounter)
  950. tempDiv.insertBefore(styleElement, tempDiv.firstChild)
  951. return tempDiv.innerHTML
  952. },
  953. // 判断是否为连续表格
  954. isContinuousTable(table) {
  955. // 检查表格是否有分页标记
  956. const prevElement = table.previousElementSibling
  957. return prevElement && prevElement.classList.contains('page-break')
  958. },
  959. // 处理连续表格
  960. processContinuousTable(table, tableCounter) {
  961. // 设置表格基本样式
  962. table.style.borderCollapse = 'collapse'
  963. table.style.border = '1px solid black'
  964. // 处理所有单元格
  965. table.querySelectorAll('tr').forEach((row, rowIndex) => {
  966. row.style.borderTop = '1px solid black'
  967. row.style.borderBottom = '1px solid black'
  968. row.querySelectorAll('td, th').forEach((cell) => {
  969. const originalStyles = cell.getAttribute('style') || ''
  970. cell.style.cssText = `
  971. border: 1px solid black !important;
  972. padding: 6px;
  973. word-wrap: break-word;
  974. overflow: hidden;
  975. ${originalStyles}
  976. `
  977. })
  978. })
  979. },
  980. // 生成表格样式
  981. generateTableStyles() {
  982. return `
  983. /* 原有表样式保持不变 */
  984. .table-with-border {
  985. /* ... */
  986. }
  987. .table-no-border {
  988. /* ... */
  989. }
  990. /* 确保表格不会被分页符影响 */
  991. table {
  992. -webkit-column-break-inside: avoid;
  993. page-break-inside: avoid;
  994. break-inside: avoid;
  995. }
  996. `
  997. },
  998. // 添加判断表格类型的方法
  999. isInformationTable(table) {
  1000. // 获取表格第一行第一个单元格的文本
  1001. const firstCell = table.querySelector('td')
  1002. if (!firstCell) return false
  1003. // 定义信息表格的特征文本数组
  1004. const infoTableHeaders = [
  1005. 'Drafted by',
  1006. 'Reviewed by',
  1007. 'Approved by',
  1008. 'File number',
  1009. 'Revision',
  1010. 'Effective'
  1011. ]
  1012. // 检查第一个单元格的文本是否匹配特征文本
  1013. return infoTableHeaders.some(
  1014. (header) =>
  1015. firstCell.textContent.trim().toLowerCase() === header.toLowerCase()
  1016. )
  1017. },
  1018. // 添加处理普通表格的方法
  1019. processNormalTable(table) {
  1020. // 处理行和单元格
  1021. table.querySelectorAll('tr').forEach((row, rowIndex) => {
  1022. // 保持原始行高
  1023. const height = row.style.height || row.getAttribute('height')
  1024. if (height) {
  1025. row.style.height = height
  1026. }
  1027. // 处理单元格
  1028. row.querySelectorAll('td, th').forEach((cell, cellIndex) => {
  1029. // 获取原始样式
  1030. const originalStyles = cell.getAttribute('style') || ''
  1031. // 设置单元格样式
  1032. cell.style.cssText = `
  1033. padding: 6px;
  1034. word-wrap: break-word;
  1035. overflow: hidden;
  1036. ${originalStyles}
  1037. `
  1038. // 保持原始宽度
  1039. if (rowIndex === 0) {
  1040. const width = cell.style.width || cell.getAttribute('width')
  1041. if (width) {
  1042. cell.style.width = width
  1043. // 为该列的所有单元格设置相同的宽度
  1044. table
  1045. .querySelectorAll(
  1046. `tr td:nth-child(${cellIndex + 1}), tr th:nth-child(${
  1047. cellIndex + 1
  1048. })`
  1049. )
  1050. .forEach((td) => {
  1051. td.style.width = width
  1052. })
  1053. }
  1054. }
  1055. // 处理合并单元格
  1056. if (cell.hasAttribute('rowspan')) {
  1057. cell.style.verticalAlign = 'middle'
  1058. }
  1059. })
  1060. })
  1061. },
  1062. // 添加处理信息表格的方法
  1063. processInfoTable(table) {
  1064. table.querySelectorAll('tr').forEach((row) => {
  1065. row.querySelectorAll('td').forEach((cell, index) => {
  1066. // 第一列(标签列)样式
  1067. if (index === 0) {
  1068. cell.style.cssText = `
  1069. border: 1px solid black;
  1070. padding: 6px;
  1071. width: 150px;
  1072. font-weight: normal;
  1073. text-align: left;
  1074. background-color: white;
  1075. `
  1076. }
  1077. // 第二列(内容列)样式
  1078. else {
  1079. cell.style.cssText = `
  1080. border: 1px solid black;
  1081. padding: 6px;
  1082. text-align: left;
  1083. `
  1084. }
  1085. // 保持原始对齐方式
  1086. const originalAlign = cell.getAttribute('align')
  1087. if (originalAlign) {
  1088. cell.style.textAlign = originalAlign
  1089. }
  1090. })
  1091. })
  1092. },
  1093. // 判断表格类型的方法
  1094. isTableWithBorder(table) {
  1095. // 检查表格是否有边框属性
  1096. if (table.getAttribute('border') === '1') return true
  1097. // 检查表格是否有边框样式
  1098. if (table.style.border || table.classList.contains('bordered')) { return true }
  1099. // 检查第一个单格是否有边框
  1100. const firstCell = table.querySelector('td, th')
  1101. if (firstCell && firstCell.style.border) return true
  1102. // 检查是否为信息类表格
  1103. const firstCellText = firstCell?.textContent?.trim().toLowerCase() || ''
  1104. const infoHeaders = [
  1105. 'drafted by',
  1106. 'reviewed by',
  1107. 'approved by',
  1108. 'file number',
  1109. 'revision',
  1110. 'effective'
  1111. ]
  1112. return infoHeaders.some((header) =>
  1113. firstCellText.includes(header.toLowerCase())
  1114. )
  1115. },
  1116. // 处理表格的主方法
  1117. processContentForWord(content) {
  1118. const tempDiv = document.createElement('div')
  1119. tempDiv.innerHTML = content
  1120. // 添加表格分页标识
  1121. let tableCounter = 0
  1122. // 处理表格
  1123. tempDiv.querySelectorAll('table').forEach((table) => {
  1124. tableCounter++
  1125. const tableId = `table_${tableCounter}`
  1126. table.setAttribute('data-table-id', tableId)
  1127. // 判断表格类型
  1128. const isInfoTable = this.isInformationTable(table)
  1129. const isContinuousTable = this.isContinuousTable(table)
  1130. const hasTableBorder = this.isTableWithBorder(table)
  1131. // 基本表格样式
  1132. table.style.width = '100%'
  1133. table.style.borderCollapse = 'collapse'
  1134. table.style.tableLayout = 'fixed'
  1135. if (isInfoTable) {
  1136. // 信息表格处理逻辑
  1137. table.classList.add('info-table')
  1138. this.processInfoTable(table)
  1139. } else if (isContinuousTable && hasTableBorder) {
  1140. // 处理有边框的跨页连续表格
  1141. table.classList.add('continuous-table-with-border')
  1142. this.processContinuousTableWithBorder(table, tableCounter)
  1143. } else if (isContinuousTable) {
  1144. // 处理无边框的跨页连续表格
  1145. table.classList.add('continuous-table-no-border')
  1146. this.processContinuousTableNoBorder(table, tableCounter)
  1147. } else if (hasTableBorder) {
  1148. // 处理有边框的普通表格
  1149. table.classList.add('normal-table-with-border')
  1150. this.processNormalTableWithBorder(table)
  1151. } else {
  1152. // 处理无边框的普通表格
  1153. table.classList.add('normal-table-no-border')
  1154. this.processNormalTableNoBorder(table)
  1155. }
  1156. })
  1157. // 添加表格样式
  1158. const styleElement = document.createElement('style')
  1159. styleElement.textContent = this.generateTableStyles(tableCounter)
  1160. tempDiv.insertBefore(styleElement, tempDiv.firstChild)
  1161. return tempDiv.innerHTML
  1162. },
  1163. // 处理有边框的普通表格
  1164. processNormalTableWithBorder(table) {
  1165. table.style.border = '1px solid black'
  1166. table.querySelectorAll('tr').forEach((row, rowIndex) => {
  1167. // 保持原始行高
  1168. const height = row.style.height || row.getAttribute('height')
  1169. if (height) {
  1170. row.style.height = height
  1171. }
  1172. row.querySelectorAll('td, th').forEach((cell, cellIndex) => {
  1173. const originalStyles = cell.getAttribute('style') || ''
  1174. cell.style.cssText = `
  1175. border: 1px solid black;
  1176. padding: 6px;
  1177. word-wrap: break-word;
  1178. overflow: hidden;
  1179. ${originalStyles}
  1180. `
  1181. // 保持原始宽度
  1182. if (rowIndex === 0) {
  1183. const width = cell.style.width || cell.getAttribute('width')
  1184. if (width) {
  1185. this.setColumnWidth(table, cellIndex, width)
  1186. }
  1187. }
  1188. })
  1189. })
  1190. },
  1191. // 处理无边框的普通表格
  1192. processNormalTableNoBorder(table) {
  1193. table.querySelectorAll('tr').forEach((row, rowIndex) => {
  1194. // 保持原始行高
  1195. const height = row.style.height || row.getAttribute('height')
  1196. if (height) {
  1197. row.style.height = height
  1198. }
  1199. row.querySelectorAll('td, th').forEach((cell, cellIndex) => {
  1200. const originalStyles = cell.getAttribute('style') || ''
  1201. cell.style.cssText = `
  1202. padding: 6px;
  1203. word-wrap: break-word;
  1204. overflow: hidden;
  1205. ${originalStyles}
  1206. `
  1207. // 保持原始宽度
  1208. if (rowIndex === 0) {
  1209. const width = cell.style.width || cell.getAttribute('width')
  1210. if (width) {
  1211. this.setColumnWidth(table, cellIndex, width)
  1212. }
  1213. }
  1214. })
  1215. })
  1216. },
  1217. // 处理有边框的连续表格
  1218. processContinuousTableWithBorder(table, tableCounter) {
  1219. // 设置表格基本样式
  1220. table.style.borderCollapse = 'collapse'
  1221. table.style.border = '1px solid black'
  1222. // 为表格添加唯一标识
  1223. const tableId = `bordered-table-${tableCounter}`
  1224. table.id = tableId
  1225. // 处理所有行和单元格
  1226. table.querySelectorAll('tr').forEach((row, rowIndex) => {
  1227. // 设置行样式
  1228. row.style.borderTop = '1px solid black'
  1229. row.style.borderBottom = '1px solid black'
  1230. row.style.pageBreakInside = 'avoid' // 防止行内分页
  1231. // 处理单元格
  1232. row.querySelectorAll('td, th').forEach((cell, cellIndex) => {
  1233. const originalStyles = cell.getAttribute('style') || ''
  1234. cell.style.cssText = `
  1235. border: 1px solid black !important;
  1236. padding: 6px;
  1237. word-wrap: break-word;
  1238. overflow: hidden;
  1239. ${originalStyles}
  1240. `
  1241. })
  1242. })
  1243. // 添加特定样式以处理分页情况
  1244. const styleSheet = document.createElement('style')
  1245. styleSheet.textContent = `
  1246. #${tableId} {
  1247. border-collapse: collapse !important;
  1248. border: 1px solid black !important;
  1249. }
  1250. #${tableId} tr {
  1251. border-top: 1px solid black !important;
  1252. border-bottom: 1px solid black !important;
  1253. }
  1254. #${tableId} td,
  1255. #${tableId} th {
  1256. border: 1px solid black !important;
  1257. }
  1258. /* 处理分页后的表格边框 */
  1259. @media print {
  1260. #${tableId} {
  1261. border: 1px solid black !important;
  1262. }
  1263. #${tableId}[data-table-continued="true"] {
  1264. border-top: 1px solid black !important;
  1265. }
  1266. #${tableId}[data-table-start="true"] {
  1267. border-bottom: 1px solid black !important;
  1268. }
  1269. }
  1270. `
  1271. document.head.appendChild(styleSheet)
  1272. },
  1273. // 处理无边框的连续表格
  1274. processContinuousTableNoBorder(table, tableCounter) {
  1275. table.querySelectorAll('tr').forEach((row) => {
  1276. row.querySelectorAll('td, th').forEach((cell) => {
  1277. const originalStyles = cell.getAttribute('style') || ''
  1278. cell.style.cssText = `
  1279. padding: 6px;
  1280. word-wrap: break-word;
  1281. overflow: hidden;
  1282. ${originalStyles}
  1283. `
  1284. })
  1285. })
  1286. },
  1287. // 设置列宽的辅助方法
  1288. setColumnWidth(table, columnIndex, width) {
  1289. table
  1290. .querySelectorAll(
  1291. `tr td:nth-child(${columnIndex + 1}), tr th:nth-child(${
  1292. columnIndex + 1
  1293. })`
  1294. )
  1295. .forEach((td) => {
  1296. td.style.width = width
  1297. })
  1298. },
  1299. // 生成表格样式
  1300. generateTableStyles(tableCount) {
  1301. return `
  1302. /* 基础表格样式 */
  1303. table {
  1304. width: 100%;
  1305. border-collapse: collapse;
  1306. table-layout: fixed;
  1307. page-break-inside: auto;
  1308. }
  1309. /* 有边框表格的通用样式 */
  1310. table[data-has-border="true"] {
  1311. border-collapse: collapse !important;
  1312. }
  1313. /* 处理分页情况 */
  1314. @media print {
  1315. /* 确保分页后的表格边框显示 */
  1316. table[data-has-border="true"] {
  1317. border: 1px solid black !important;
  1318. }
  1319. /* 处理跨页表格的边框 */
  1320. table[data-has-border="true"][data-table-continued="true"] {
  1321. border-top: 1px solid black !important;
  1322. margin-top: 0 !important;
  1323. }
  1324. table[data-has-border="true"][data-table-start="true"] {
  1325. border-bottom: 1px solid black !important;
  1326. }
  1327. /* 确保所有单元格边框显示 */
  1328. table[data-has-border="true"] td,
  1329. table[data-has-border="true"] th {
  1330. border: 1px solid black !important;
  1331. }
  1332. }
  1333. `
  1334. },
  1335. // 判断表格是否是连续的
  1336. isTableContinued(previousTable, currentTable) {
  1337. // 检查两个表格是否紧邻
  1338. const nextElement = previousTable.nextElementSibling
  1339. if (!nextElement) return false
  1340. // 检查是否有分页标记
  1341. if (nextElement.classList.contains('page-break')) {
  1342. return true
  1343. }
  1344. // 检查表格结构是否相似
  1345. const prevCols = previousTable.querySelectorAll(
  1346. 'tr:first-child td, tr:first-child th'
  1347. ).length
  1348. const currCols = currentTable.querySelectorAll(
  1349. 'tr:first-child td, tr:first-child th'
  1350. ).length
  1351. return prevCols === currCols
  1352. },
  1353. // 处理表格表头
  1354. processTableHeaders(content) {
  1355. const tempDiv = document.createElement('div')
  1356. tempDiv.innerHTML = content
  1357. // 处理所有需要重复表头的表格
  1358. tempDiv
  1359. .querySelectorAll('table[data-repeat-header="true"]')
  1360. .forEach((table) => {
  1361. const headerHTML = table.getAttribute('data-header')
  1362. if (!headerHTML) return
  1363. // 创建thead元素
  1364. const thead = document.createElement('thead')
  1365. thead.innerHTML = headerHTML
  1366. // 将原有的表头移动到thead中
  1367. const firstRow = table.querySelector('tr')
  1368. if (firstRow) {
  1369. table.insertBefore(thead, firstRow)
  1370. }
  1371. // 确保tbody存在
  1372. if (!table.querySelector('tbody')) {
  1373. const tbody = document.createElement('tbody')
  1374. while (table.querySelector('tr')) {
  1375. tbody.appendChild(table.querySelector('tr'))
  1376. }
  1377. table.appendChild(tbody)
  1378. }
  1379. })
  1380. return tempDiv.innerHTML
  1381. },
  1382. /* */
  1383. // 添加 PDF 转 Word 的方法
  1384. async convertPDFToWord(pdfPath) {
  1385. try {
  1386. // 创建FormData对象
  1387. const formData = new FormData()
  1388. formData.append('oss_url', pdfPath)
  1389. // 调用后端转换接口
  1390. const res = await axios.post(
  1391. `${process.env.VUE_APP_BASE_API}/convert/pdftoword`,
  1392. formData,
  1393. {
  1394. headers: {
  1395. 'Content-Type': 'multipart/form-data' // 设置请求头为form-data
  1396. }
  1397. }
  1398. )
  1399. if (res.status === 200) {
  1400. // 将base64转换为Blob
  1401. const byteCharacters = atob(res.data.data.file_content)
  1402. const byteNumbers = new Array(byteCharacters.length)
  1403. for (let i = 0; i < byteCharacters.length; i++) {
  1404. byteNumbers[i] = byteCharacters.charCodeAt(i)
  1405. }
  1406. const byteArray = new Uint8Array(byteNumbers)
  1407. const blob = new Blob([byteArray], {
  1408. type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
  1409. })
  1410. // 创建下载链接
  1411. const link = document.createElement('a')
  1412. link.href = URL.createObjectURL(blob)
  1413. link.download = 'converted.docx' // 或使用服务器返回的文件名
  1414. document.body.appendChild(link)
  1415. link.click()
  1416. document.body.removeChild(link)
  1417. // 清理URL对象
  1418. URL.revokeObjectURL(link.href)
  1419. this.$message.success('转换成功')
  1420. } else {
  1421. throw new Error(res.data.message || '转换失败')
  1422. }
  1423. } catch (error) {
  1424. console.error('PDF转Word失败:', error)
  1425. this.$message.error('PDF转Word失败,请稍后重试')
  1426. }
  1427. },
  1428. // 2. 替换原有的 exportDocument 方法
  1429. async exportDocument(template) {
  1430. try {
  1431. const templateData = JSON.parse(template.data || '[]')
  1432. const contentData = `
  1433. <!DOCTYPE html>
  1434. <html>
  1435. <head>
  1436. <meta charset="UTF-8">
  1437. <style>
  1438. @page {
  1439. size: A4;
  1440. margin: 2cm;
  1441. }
  1442. body {
  1443. font-family: Arial, sans-serif;
  1444. font-size: 12pt;
  1445. line-height: 1.5;
  1446. }
  1447. /* 目录样式 */
  1448. .toc {
  1449. margin-bottom: 30px;
  1450. }
  1451. .toc-item {
  1452. display: flex;
  1453. justify-content: space-between;
  1454. margin: 5px 0;
  1455. border-bottom: 1px dotted #000;
  1456. }
  1457. .toc-text {
  1458. margin-right: 10px;
  1459. }
  1460. .toc-page {
  1461. margin-left: 10px;
  1462. }
  1463. /* 章节标题样式 */
  1464. .section-title {
  1465. font-weight: bold;
  1466. margin-top: 20px;
  1467. margin-bottom: 10px;
  1468. page-break-after: avoid;
  1469. }
  1470. /* 页码样式 */
  1471. .page-number {
  1472. position: fixed;
  1473. bottom: 20px;
  1474. right: 20px;
  1475. font-size: 10pt;
  1476. }
  1477. </style>
  1478. </head>
  1479. <body>
  1480. <!-- 添加目录 -->
  1481. <div class="toc">
  1482. <h2>Content</h2>
  1483. ${this.generateTableOfContents(templateData)}
  1484. </div>
  1485. <!-- 添加正文内容 -->
  1486. ${this.processContentWithSections(templateData)}
  1487. </body>
  1488. </html>
  1489. `
  1490. // 调用导出接口
  1491. const res = await htmlWordNew({
  1492. content: contentData,
  1493. title: template.title,
  1494. options: {
  1495. format: 'docx',
  1496. encoding: 'utf-8',
  1497. preserveStyles: true
  1498. }
  1499. })
  1500. if (res.status !== 200) {
  1501. this.$alert(res.errMsg)
  1502. return
  1503. }
  1504. // 处理文件下载
  1505. const link = document.createElement('a')
  1506. link.href = res.data.file_path
  1507. link.download = `${template.title}.docx`
  1508. document.body.appendChild(link)
  1509. link.click()
  1510. document.body.removeChild(link)
  1511. this.$message.success('导出成功')
  1512. } catch (error) {
  1513. console.error('导出Word文档失败:', error)
  1514. this.$alert('导出文档时发生错误,请稍后重试。')
  1515. }
  1516. },
  1517. /* 客户名称 */
  1518. getCusomName(phaseId) {
  1519. const product = this.customList.find((item) => item.id === phaseId)
  1520. return product ? product.name : '未知客户'
  1521. },
  1522. /* 当前阶段 */
  1523. getPhaseName(phaseId) {
  1524. const product = this.currentPhaseList.find(
  1525. (item) => item.value === phaseId
  1526. )
  1527. return product ? product.label : '未知阶段'
  1528. },
  1529. getPhaseType(phaseId) {
  1530. const product = this.currentPhaseList.find(
  1531. (item) => item.value === phaseId
  1532. )
  1533. if (!product) return 'info'
  1534. // 根据不同阶段返回同的类型
  1535. // 这里的判断逻辑需要根据实际的产品阶来调整
  1536. switch (product.status) {
  1537. case 1:
  1538. return 'success' // 已完成
  1539. case 2:
  1540. return 'warning' // 进行中
  1541. case 3:
  1542. return 'danger' // 待处理
  1543. default:
  1544. return 'info' // 其他状态
  1545. }
  1546. },
  1547. /* 目前状态 */
  1548. getStatusName(statusId) {
  1549. const status = this.getstatusList.find((item) => item.value === statusId)
  1550. return status ? status.label : '未状态'
  1551. },
  1552. getStatusType(statusId) {
  1553. // 这里可以根据不同状态返回不同的类型
  1554. // 可用的类型:success/warning/danger/info
  1555. const status = this.getstatusList.find((item) => item.value === statusId)
  1556. if (!status) return 'info'
  1557. // 根据状值返回对应的类型
  1558. switch (status.value) {
  1559. case 5:
  1560. return 'success'
  1561. case 4:
  1562. return 'warning'
  1563. case 3:
  1564. return 'danger'
  1565. default:
  1566. return 'info'
  1567. }
  1568. },
  1569. // 格式化完成度显示
  1570. formatCompletion(rate) {
  1571. if (rate === undefined || rate === null) {
  1572. return '0%'
  1573. }
  1574. return `${rate}%`
  1575. },
  1576. // 根据完成度返回对应CSS类名
  1577. getCompletionClass(rate) {
  1578. if (!rate) return 'completion-low'
  1579. if (rate >= 100) return 'completion-high'
  1580. if (rate >= 50) return 'completion-medium'
  1581. return 'completion-low'
  1582. },
  1583. handleCommand(command) {
  1584. switch (command.type) {
  1585. case 'template':
  1586. this.selTemplate(command.row)
  1587. break
  1588. case 'enter':
  1589. this.enterPro(command.row)
  1590. break
  1591. case 'edit':
  1592. this.btnEdit(command.row)
  1593. break
  1594. case 'delete':
  1595. this.btnDelete(command.row.id)
  1596. break
  1597. case 'viewTemplates':
  1598. this.viewBoundTemplates(command.row)
  1599. break
  1600. }
  1601. },
  1602. // Add new methods
  1603. // 查看已绑定模版
  1604. viewBoundTemplates(row) {
  1605. this.currentData = row
  1606. this.boundTemplateLoading = true
  1607. searchTemplateListNoData({ ids: row.documentIds.join(',') || 0 })
  1608. .then((res) => {
  1609. if (res.status === 200) {
  1610. this.boundTemplateList = res.data
  1611. this.boundTemplatesDialogVisible = true
  1612. }
  1613. })
  1614. .finally(() => {
  1615. this.boundTemplateLoading = false
  1616. })
  1617. },
  1618. async previewTemplate(template) {
  1619. console.log(template);
  1620. this.currentTemplate = template
  1621. /* this.loadDocumentVersions(template); */
  1622. const res = await searchlistDocTemplate({
  1623. /* page: 1,
  1624. pageSize: 10, */
  1625. project_id:this.currentData.id,
  1626. id: template.id
  1627. })
  1628. if (res.status === 200) {
  1629. /* this.documentVersions = res.data.dataList[0]; */
  1630. if (res.data.documents.length !== 0) {
  1631. const url = `#/document/viewCreate?articleId=${res.data.documents[0].id}&type=document`
  1632. window.open(url, '_blank')
  1633. } else {
  1634. const a = document.createElement('a')
  1635. a.href = '#/document/viewCreate?articleId=' + template.id
  1636. a.target = '_blank'
  1637. a.click()
  1638. }
  1639. /* if (this.documentVersions.length === 0) {
  1640. this.$message.warning("未找到相关文档版本");
  1641. } */
  1642. }
  1643. },
  1644. replaceTemplate(template) {
  1645. this.docAttr = template
  1646. const TemList = JSON.parse(template.data)
  1647. /* this.$confirm("确定要替换此模吗?", "提示", {
  1648. confirmButtonText: "确",
  1649. cancelButtonText: "取消",
  1650. type: "warning",
  1651. }).then(() => { */
  1652. this.replaceVisible = true
  1653. this.searchEx(TemList)
  1654. },
  1655. // 新增:处理已选定模版的选择
  1656. async handleBoundTemplateSelect(selection) {
  1657. this.selectedBoundTemplates = selection
  1658. /* try {
  1659. if (!selection || selection.length === 0) {
  1660. this.selectedBoundTemplates = [];
  1661. return;
  1662. }
  1663. // 获取选中模板的详细信息
  1664. const templatePromises = selection.map(async (item) => {
  1665. const templateId = JSON.parse(item.data)[0].id;
  1666. const res = await getTemplateInfo({ id: templateId });
  1667. if (res.status === 200 && res.data) {
  1668. return {
  1669. ...res.data,
  1670. data: res.data.data || '[]' // 确保 data 字段存在
  1671. };
  1672. }
  1673. return null;
  1674. });
  1675. // 等待所有模板信息加载完成
  1676. const templates = await Promise.all(templatePromises);
  1677. // 过滤掉无效的模板并保存
  1678. this.selectedBoundTemplates = templates.filter(template => template !== null);
  1679. console.log(this.selectedBoundTemplates);
  1680. } catch (error) {
  1681. console.error('获取模板信息失败:', error);
  1682. this.$message.error('获取模板信息失败');
  1683. this.selectedBoundTemplates = [];
  1684. } */
  1685. },
  1686. // 新增:批量替换方法
  1687. async batchReplace() {
  1688. if (this.selectedBoundTemplates.length === 0) {
  1689. this.$message.warning('请选择需要替换的模版')
  1690. return
  1691. }
  1692. this.batchReplaceLoading = true
  1693. try {
  1694. // 依次处理每个选中的模版
  1695. for (const template of this.selectedBoundTemplates) {
  1696. this.docAttr = template
  1697. const res = await searchlistDocTemplate({
  1698. /* page: 1,
  1699. pageSize: 10, */
  1700. project_id:this.currentData.id,
  1701. id: template.id,
  1702. is_template:1
  1703. })
  1704. if(res.status!== 200) return
  1705. // 检查模板数据格式
  1706. let templateData
  1707. try {
  1708. // 优先尝试使用 data 字段
  1709. if(res.data.documents.length!==0){
  1710. templateData = JSON.parse(res.data.documents[0].data || '[]')
  1711. }else{
  1712. const template_Data = await getDocumentInfo({id:template.id})
  1713. if(template_Data.status!==200) return
  1714. templateData = JSON.parse(template_Data.data.data || '[]')
  1715. }
  1716. } catch (e) {
  1717. // 如果解析失败,尝试使用 content 字段
  1718. templateData = template.content
  1719. ? [{ content: template.content }]
  1720. : []
  1721. }
  1722. if (templateData && templateData.length > 0) {
  1723. await this.searchEx(templateData)
  1724. if (this.matchResults.length > 0) {
  1725. await this.replaceAll()
  1726. }
  1727. } else {
  1728. console.warn(`模板 ${template.id} 没有有效的内容数据`)
  1729. }
  1730. }
  1731. this.$message.success('批量替换完成')
  1732. } catch (error) {
  1733. console.error('批量替换失败:', error)
  1734. this.$message.error('批量替换过程中发生错误: ' + error.message)
  1735. } finally {
  1736. this.batchReplaceLoading = false
  1737. }
  1738. },
  1739. searchEx(val) {
  1740. return new Promise((resolve) => {
  1741. const allMatches = []
  1742. this.TemList = val
  1743. this.TemList.forEach((el) => {
  1744. // 创建临时 div 来解析 HTML 内容
  1745. const tempDiv = document.createElement('div')
  1746. tempDiv.innerHTML = el.content
  1747. const plainText = tempDiv.textContent || tempDiv.innerText
  1748. // 分别匹配中文括号和英文括号
  1749. const patterns = [
  1750. /【([A-Z]{1,4}[0-9]{0,2})-([A-Z]{1,4}[0-9]{0,2})-([0-9]{2})-([0-9]{2,3})】/g,
  1751. /\[([A-Z]{1,4}[0-9]{0,2})-([A-Z]{1,4}[0-9]{0,2})-([0-9]{2})-([0-9]{2,3})\]/g
  1752. ]
  1753. patterns.forEach((pattern) => {
  1754. let match
  1755. while ((match = pattern.exec(plainText)) !== null) {
  1756. const fullMatch = match[0]
  1757. const segments = [match[1], match[2], match[3], match[4]]
  1758. // 验证个段落的格式
  1759. const isValid = segments.every((segment) => segment.length > 0)
  1760. if (isValid) {
  1761. allMatches.push(fullMatch)
  1762. }
  1763. }
  1764. })
  1765. })
  1766. this.matchResults = [...new Set(allMatches)]
  1767. // 调试用:打印每个匹配的详细信息
  1768. this.matchResults.forEach((match) => {
  1769. // 据括号类型选不同的切片方式
  1770. const content = match.startsWith('【')
  1771. ? match.slice(1, -1)
  1772. : match.slice(1, -1)
  1773. const parts = content.split('-')
  1774. })
  1775. resolve()
  1776. })
  1777. },
  1778. // 添加的辅助方法来判断文档类型
  1779. getDocumentType(code) {
  1780. if (code.includes('-RP-')) return 'template_location'
  1781. if (code.includes('-IR-')) return 'middle_layer_location'
  1782. if (code.includes('-WE-')) return 'tech_report_location'
  1783. return 'other_location'
  1784. },
  1785. // 修改式化请求参数的方法
  1786. formatRequestParams(locations) {
  1787. const locationArray = Array.isArray(locations) ? locations : [locations]
  1788. // 初始化求参数对象
  1789. const params = {
  1790. /* project_id:'', */
  1791. template_location: [],
  1792. middle_layer_location: [],
  1793. tech_report_location: [],
  1794. other_location: []
  1795. }
  1796. // 根据不同类型分类存储
  1797. locationArray.forEach((location) => {
  1798. const cleanLocation = location.replace(/[\[\]【】]/g, '')
  1799. const type = this.getDocumentType(cleanLocation)
  1800. console.log('params', type)
  1801. // 为每种类型创建对应长度的空数组
  1802. Object.keys(params).forEach((key) => {
  1803. if (type === key) {
  1804. params[key].push(cleanLocation)
  1805. } else {
  1806. params[key].push('')
  1807. }
  1808. })
  1809. })
  1810. return params
  1811. },
  1812. // 解析内容,处理特殊标记
  1813. parseContent(content) {
  1814. if (!content) return ''
  1815. let parsed = content
  1816. // 处理图片标记
  1817. parsed = parsed.replace(
  1818. /\[图片:(.*?)\|style=(.*?)\|class=(.*?)\]/g,
  1819. (match, src, style, className) => {
  1820. return `<img src="${src}" alt="图片" style="${style}" class="${className}">`
  1821. }
  1822. )
  1823. // 处理表格标记
  1824. parsed = parsed.replace(
  1825. /\[表格\|style=(.*?)\|class=(.*?)\]\n([\s\S]*?)(?=\[|$)/g,
  1826. (match, tableStyle, tableClass, tableContent) => {
  1827. const rows = tableContent.trim().split('\n')
  1828. let tableHtml = `<table style="${tableStyle}" class="${tableClass}">`
  1829. rows.forEach((row) => {
  1830. tableHtml += '<tr>'
  1831. const cells = row.split(' | ').map((cell) => {
  1832. const [content, ...styleInfo] = cell.split('[style=')
  1833. if (styleInfo.length) {
  1834. const [style, className] = styleInfo[0]
  1835. .slice(0, -1)
  1836. .split('|class=')
  1837. return `<td style="${style}" class="${className}">${content}</td>`
  1838. }
  1839. return `<td>${content}</td>`
  1840. })
  1841. tableHtml += cells.join('') + '</tr>'
  1842. })
  1843. tableHtml += '</table>'
  1844. return tableHtml
  1845. }
  1846. )
  1847. // 处理文本样式
  1848. parsed = parsed.replace(
  1849. /\[文本:(.*?)\|style=(.*?)\|class=(.*?)\]/g,
  1850. (match, content, style, className) => {
  1851. return `<span style="${style}" class="${className}">${content}</span>`
  1852. }
  1853. )
  1854. return parsed
  1855. },
  1856. // 查内容是否包含表格或图片
  1857. containsTableOrImage(content) {
  1858. return content.includes('<table') || content.includes('<img')
  1859. },
  1860. // 获取所有文本节点
  1861. getTextNodes(node) {
  1862. const textNodes = []
  1863. const walk = document.createTreeWalker(
  1864. node,
  1865. NodeFilter.SHOW_TEXT,
  1866. null,
  1867. false
  1868. )
  1869. let currentNode
  1870. while ((currentNode = walk.nextNode())) {
  1871. textNodes.push(currentNode)
  1872. }
  1873. return textNodes
  1874. },
  1875. /* 更新 */
  1876. async onUpload() {
  1877. const _this = this
  1878. if (_this.TemList.length <= 0) {
  1879. _this.$message.error('增加组件')
  1880. return
  1881. }
  1882. if (_this.docAttr.title == '') {
  1883. _this.$message.error('请填写模版标题')
  1884. return
  1885. }
  1886. const res = await searchlistDocTemplate({
  1887. /* page: 1,
  1888. pageSize: 10, */
  1889. project_id:this.currentData.id,
  1890. id: _this.docAttr.id
  1891. })
  1892. if(res.status!==200) return
  1893. if(res.data.documents.length==0){
  1894. _this.docAttr.title = _this.docAttr.title + new Date().getTime()
  1895. _this.docAttr.links = JSON.stringify(_this.docAttr.linkProduct)
  1896. _this.docAttr.projects = JSON.stringify(_this.docAttr.linkProject)
  1897. _this.docAttr.data = JSON.stringify(_this.TemList)
  1898. _this.docAttr.is_template = 0
  1899. /* _this.docAttr.id = _this.docAttr.id; */
  1900. _this.docAttr.user_name = _this.docAttr.name
  1901. _this.docAttr.user_id = _this.$store.state.user.id // _this.docAttr.id;
  1902. _this.docAttr.template_id = _this.docAttr.id
  1903. createDocument(_this.docAttr).then((res) => {
  1904. if (res.status != 200) return // 更新文档
  1905. /* _this.docAttr.id = res.data; */
  1906. _this.$message.success('文档创建成功')
  1907. searchlistDoc({
  1908. page: 1,
  1909. pageSize: 999
  1910. })
  1911. })
  1912. }else{
  1913. updateDocument({
  1914. id:res.data.documents[0].id,
  1915. data :JSON.stringify(_this.TemList),
  1916. title:res.data.documents[0].title,
  1917. status:5,
  1918. document_type:res.data.documents[0].document_type,
  1919. is_template:0,
  1920. template_id:res.data.documents[0].template_id,
  1921. user_id:res.data.documents[0].user_id
  1922. }).then(res=>{
  1923. if (res.status != 200) return
  1924. _this.$message.success('文档更新成功')
  1925. })
  1926. }
  1927. },
  1928. // 添加统一的内容处理方法
  1929. async processAndReplaceContent(content, originalMatch) {
  1930. try {
  1931. const params = this.formatRequestParams(originalMatch)
  1932. params.project_id = this.currentData.id
  1933. const response = await axios.post(
  1934. `${process.env.VUE_APP_BASE_API}/project-raw-data/batch-content`,
  1935. params,
  1936. {
  1937. headers: {
  1938. 'Content-Type': 'application/json'
  1939. }
  1940. }
  1941. )
  1942. if (response.status === 200 && response.data) {
  1943. const replacementContent =
  1944. response.data.data.matched_data[0]?.content
  1945. if (replacementContent) {
  1946. // 处富文本内容
  1947. this.TemList.forEach((el, index) => {
  1948. console.log(el, originalMatch, replacementContent)
  1949. const processedContent = this.processTableContent(
  1950. el.content,
  1951. originalMatch,
  1952. replacementContent
  1953. )
  1954. this.$set(this.TemList[index], 'content', processedContent)
  1955. })
  1956. return true
  1957. }
  1958. }
  1959. return false
  1960. } catch (error) {
  1961. console.error('处理内容失败:', error)
  1962. return false
  1963. }
  1964. },
  1965. // 修改单个替换方法
  1966. async replaceItem(item) {
  1967. try {
  1968. const success = await this.processAndReplaceContent(this.TemList, item)
  1969. if (success) {
  1970. this.matchResults = this.matchResults.filter(
  1971. (match) => match !== item
  1972. )
  1973. this.onUpload()
  1974. this.$message.success('替换成功')
  1975. } else {
  1976. this.$message.warning('未获取到替换内容')
  1977. }
  1978. } catch (error) {
  1979. console.error('替换失败:', error)
  1980. this.$message.error(error.response?.data?.message || '替换失败')
  1981. }
  1982. },
  1983. // 修改批量替换方法
  1984. async replaceAll() {
  1985. this.batchReplaceLoading = true
  1986. try {
  1987. // 收所有配
  1988. const allLocations = this.matchResults.map((item) => {
  1989. return item.replace(/[\[\]【】]/g, '')
  1990. })
  1991. // 格式化请求参数
  1992. const params = {
  1993. template_location: [],
  1994. middle_layer_location: [],
  1995. tech_report_location: [],
  1996. other_location: []
  1997. }
  1998. // 遍历所有位置并分类
  1999. allLocations.forEach((location) => {
  2000. const type = this.getDocumentType(location)
  2001. Object.keys(params).forEach((key) => {
  2002. if (type === key) {
  2003. params[key].push(location)
  2004. } else {
  2005. params[key].push('')
  2006. }
  2007. })
  2008. })
  2009. params.project_id = this.currentData.id
  2010. // 发送批量请求
  2011. const response = await axios.post(
  2012. `${process.env.VUE_APP_BASE_API}/project-raw-data/batch-content`,
  2013. params,
  2014. {
  2015. headers: {
  2016. 'Content-Type': 'application/json'
  2017. }
  2018. }
  2019. )
  2020. if (response.status === 200 && response.data) {
  2021. const replacementContents = response.data.data.matched_data
  2022. if (replacementContents && replacementContents.length > 0) {
  2023. // 更新所有内容
  2024. this.TemList.forEach((el, index) => {
  2025. let updatedContent = el.content
  2026. // 遍历所有替换内容和对应匹配项
  2027. this.matchResults.forEach((item) => {
  2028. // 提取编号的函数只需定义一
  2029. const getCodeFromItem = (searchItem) => {
  2030. const cleanItem = searchItem.replace(/[\[\]【】]/g, '')
  2031. const match = cleanItem.match(/([A-Z0-9]+-[A-Z]+-\d+-\d+)/)
  2032. return match ? match[1] : null
  2033. }
  2034. // 获取当前项的编号
  2035. const currentCode = getCodeFromItem(item)
  2036. if (!currentCode) return // 如果没有有效编号,跳过此项
  2037. // 查找匹配的替换内容
  2038. const matchedContent = replacementContents.find(
  2039. (content) => content.template_location === currentCode
  2040. )
  2041. if (matchedContent?.content) {
  2042. // 构建替换内容
  2043. const formattedReplacement = `
  2044. <span class="replacement-content" data-code="${currentCode}">
  2045. ${matchedContent.content}
  2046. </span>
  2047. `.trim()
  2048. // 处理表格和其他内容
  2049. updatedContent = this.processTableContent(
  2050. updatedContent,
  2051. item,
  2052. formattedReplacement
  2053. )
  2054. // 记录日志
  2055. if (process.env.NODE_ENV !== 'production') {
  2056. console.log('替换信息:', {
  2057. 原始编号: currentCode,
  2058. 原始内容: item,
  2059. 替换内容: formattedReplacement
  2060. })
  2061. }
  2062. }
  2063. })
  2064. // 更新内容
  2065. this.$set(this.TemList[index], 'content', updatedContent)
  2066. })
  2067. /* // 更新文档*/
  2068. await this.onUpload()
  2069. this.matchResults = []
  2070. this.$message.success('批量替换成功')
  2071. this.replaceVisible = false
  2072. } else {
  2073. this.$message.warning('未获取到替换内容')
  2074. }
  2075. }
  2076. } catch (error) {
  2077. console.error('批量替换失败:', error)
  2078. this.$message.error(error.response?.data?.message || '批量替换失败')
  2079. } finally {
  2080. this.batchReplaceLoading = false
  2081. }
  2082. },
  2083. // 辅助方法:获取文档类型
  2084. getDocumentType(code) {
  2085. if (code.includes('-RP-')) return 'template_location'
  2086. if (code.includes('-IR-')) return 'middle_layer_location'
  2087. if (code.includes('-WE-')) return 'tech_report_location'
  2088. return 'other_location'
  2089. },
  2090. // 修改处理表格内容的方法,添加对多个替换的支持
  2091. processTableContent(content, searchItem, replacement) {
  2092. if (!content || !searchItem) return content
  2093. try {
  2094. // 提取搜索项中的编号
  2095. const getCodeFromItem = (item) => {
  2096. const cleanItem = item.replace(/[\[\]【】]/g, '')
  2097. const match = cleanItem.match(/([A-Z0-9]+-[A-Z]+-\d+-\d+)/)
  2098. return match ? match[1] : null
  2099. }
  2100. const searchCode = getCodeFromItem(searchItem)
  2101. if (!searchCode) {
  2102. console.warn('无法从搜索项中提取编号:', searchItem)
  2103. return content
  2104. }
  2105. const tempDiv = document.createElement('div')
  2106. tempDiv.innerHTML = content
  2107. // 新增:递归处理HTML节点
  2108. const processNode = (node) => {
  2109. if (node.nodeType === 3) {
  2110. // 文本节点
  2111. return node.textContent
  2112. }
  2113. let html = ''
  2114. for (const child of node.childNodes) {
  2115. html += processNode(child)
  2116. }
  2117. return html
  2118. }
  2119. const handleSplitContent = () => {
  2120. // 先获取纯文本内容用于匹配
  2121. const plainText = processNode(tempDiv)
  2122. // 构建用于匹配分割标记的正则表达式
  2123. const splitRegex = new RegExp(
  2124. `(\\[|【)(.*?)(${searchCode})(.*?)(\\]|】)`,
  2125. 'g'
  2126. )
  2127. const currentHtml = tempDiv.innerHTML
  2128. // 查找所有可能的匹配
  2129. const matches = [
  2130. ...currentHtml.matchAll(/<[^>]+>|\[|\]|【|】|[^<\[\]【】]+/g)
  2131. ]
  2132. // 重建HTML,识别并替换目标编码
  2133. let rebuiltHtml = ''
  2134. let currentMatch = ''
  2135. let isCollecting = false
  2136. let bracketCount = 0
  2137. for (let i = 0; i < matches.length; i++) {
  2138. const part = matches[i][0]
  2139. if (part === '[' || part === '【') {
  2140. isCollecting = true
  2141. bracketCount++
  2142. currentMatch = part
  2143. continue
  2144. }
  2145. if (isCollecting) {
  2146. currentMatch += part
  2147. if (part === ']' || part === '】') {
  2148. bracketCount--
  2149. if (bracketCount === 0) {
  2150. // 检查收集到的内容是否包含目标编码
  2151. const cleanText = currentMatch.replace(/<[^>]+>/g, '')
  2152. if (cleanText.includes(searchCode)) {
  2153. rebuiltHtml += replacement
  2154. isCollecting = false
  2155. currentMatch = ''
  2156. } else {
  2157. rebuiltHtml += currentMatch
  2158. isCollecting = false
  2159. currentMatch = ''
  2160. }
  2161. }
  2162. }
  2163. } else {
  2164. rebuiltHtml += part
  2165. }
  2166. }
  2167. // 处理未闭合的标记
  2168. if (currentMatch) {
  2169. rebuiltHtml += currentMatch
  2170. }
  2171. // 清理可能的残留标签
  2172. rebuiltHtml = rebuiltHtml
  2173. .replace(/(<span[^>]*>)\s*(<\/span>)/g, '')
  2174. .replace(/(<\/span>)(<span[^>]*>)/g, '')
  2175. .replace(/<a[^>]*><\/a>/g, '')
  2176. .replace(/\s+/g, ' ')
  2177. tempDiv.innerHTML = rebuiltHtml
  2178. }
  2179. handleSplitContent()
  2180. return tempDiv.innerHTML
  2181. } catch (error) {
  2182. console.error('替换内容时出错:', error)
  2183. console.error('错误详情:', {
  2184. 原始内容: content.substring(0, 100) + '...',
  2185. 搜索项: searchItem,
  2186. 替换内容: replacement,
  2187. 错误信息: error.message
  2188. })
  2189. return content
  2190. }
  2191. },
  2192. // 添加样式到 convertTextToHtml 方法
  2193. convertTextToHtml(text) {
  2194. if (!text) return ''
  2195. let html = text
  2196. // 处理特定格式的表格
  2197. html = html.replace(
  2198. /\[表格\|style=(.*?)\|class=(.*?)\]([\s\S]*?)(?=\[|$)/g,
  2199. (match, tableStyle, tableClass, tableContent) => {
  2200. const rows = tableContent.trim().split('\n')
  2201. let tableHtml = `<table style="${tableStyle}; border-collapse: collapse;" class="${tableClass}">`
  2202. rows.forEach((row, index) => {
  2203. const [label, value] = row.split('|').map((item) => item.trim())
  2204. if (label) {
  2205. tableHtml += '<tr>'
  2206. if (value) {
  2207. // 如果有值,创建两列的行
  2208. tableHtml += `
  2209. <td style="border: 1px solid #000; padding: 8px;">${label}</td>
  2210. <td style="border: 1px solid #000; padding: 8px;">${value}</td>
  2211. `
  2212. } else {
  2213. // 如果只有标签,创建单列的行(可能是表头)
  2214. tableHtml += `
  2215. <td style="border: 1px solid #000; padding: 8px; font-weight: ${
  2216. index === 0 ? 'bold' : 'normal'
  2217. }">${label}</td>
  2218. `
  2219. }
  2220. tableHtml += '</tr>'
  2221. }
  2222. })
  2223. tableHtml += '</table>'
  2224. return tableHtml
  2225. }
  2226. )
  2227. // 保持原有的图片和文本处理逻辑不变
  2228. html = html.replace(
  2229. /\[图片:(.*?)\|style=(.*?)\|class=(.*?)\]/g,
  2230. (match, src, style, className) => {
  2231. return `<img src="${src}" alt="图片" style="${style}" class="${className}">`
  2232. }
  2233. )
  2234. html = html.replace(
  2235. /\[文本:(.*?)\|style=(.*?)\|class=(.*?)\]/g,
  2236. (match, content, style, className) => {
  2237. return `<span style="${style}" class="${className}">${content}</span>`
  2238. }
  2239. )
  2240. return html
  2241. },
  2242. /* 选择模版 */
  2243. selTemplate(row) {
  2244. this.currentData = row
  2245. // 重置所有状态
  2246. this.selectedTemplateIds = new Set(row.documentIds || [])
  2247. this.selectedPageIds = new Map()
  2248. this.selectionCache = new Map()
  2249. this.isAllSelected = false
  2250. this.templateQueryForm.page = 1
  2251. // 加载模板列表
  2252. this.loadTemplateList()
  2253. },
  2254. // 处理模版分页变化
  2255. async handleTemplatePageChange(page) {
  2256. try {
  2257. // 保存当前页的选择状态
  2258. const currentPage = this.templateQueryForm.page
  2259. const currentSelection = this.$refs.templateTable?.selection || []
  2260. // 更新选择缓存
  2261. this.selectionCache.set(
  2262. currentPage,
  2263. new Set(currentSelection.map((row) => row.id))
  2264. )
  2265. // 更新全局选择状态
  2266. currentSelection.forEach((row) => {
  2267. this.selectedTemplateIds.add(row.id)
  2268. })
  2269. // 更新页码
  2270. this.templateQueryForm.page = page
  2271. // 加载新页面数据
  2272. await this.loadTemplateList()
  2273. } catch (error) {
  2274. console.error('页面切换失败:', error)
  2275. this.$message.error('页面切换失败')
  2276. }
  2277. },
  2278. // 加载模版表
  2279. async loadTemplateList() {
  2280. this.templateLoading = true
  2281. try {
  2282. const res = await searchList({
  2283. ...this.templateQueryForm,
  2284. document_type: this.templateQueryForm.document_type || undefined // 只在有值时传递
  2285. })
  2286. if (res.status === 200) {
  2287. this.templateList = res.data.dataList
  2288. this.templateTotal = res.data.totalRecord
  2289. this.templateDialogVisible = true
  2290. await this.$nextTick()
  2291. if (this.$refs.templateTable) {
  2292. this.$refs.templateTable.clearSelection()
  2293. this.restoreSelectionState()
  2294. }
  2295. }
  2296. } catch (error) {
  2297. console.error('加载模板列表失败:', error)
  2298. this.$message.error('加载模板列表失败')
  2299. } finally {
  2300. this.templateLoading = false
  2301. }
  2302. },
  2303. // 添加新方法:恢复选择状态
  2304. restoreSelectionState() {
  2305. const currentPage = this.templateQueryForm.page
  2306. // 遍历当前页数据
  2307. this.templateList.forEach((row) => {
  2308. // 检查是否在全局选中集合中
  2309. if (this.selectedTemplateIds.has(row.id)) {
  2310. this.$refs.templateTable.toggleRowSelection(row, true, false)
  2311. }
  2312. })
  2313. // 更新当前页的选择记录
  2314. const selectedRows = this.templateList.filter((row) =>
  2315. this.selectedTemplateIds.has(row.id)
  2316. )
  2317. // 更新选择缓存
  2318. this.selectionCache.set(
  2319. currentPage,
  2320. new Set(selectedRows.map((row) => row.id))
  2321. )
  2322. // 检查当前页是否全选
  2323. const isCurrentPageAllSelected = this.templateList.every((row) =>
  2324. this.selectedTemplateIds.has(row.id)
  2325. )
  2326. // 更新表头的全选框状态
  2327. if (this.$refs.templateTable.store) {
  2328. this.$refs.templateTable.store.states.isAllSelected =
  2329. isCurrentPageAllSelected
  2330. }
  2331. // 触发选择变化事件
  2332. this.handleSelectionChange(selectedRows)
  2333. },
  2334. // 修改 handleSelectionChange 方法
  2335. handleSelectionChange(selection) {
  2336. if (!Array.isArray(selection)) {
  2337. console.warn('Invalid selection:', selection)
  2338. return
  2339. }
  2340. const currentPage = this.templateQueryForm.page
  2341. // 更新选择缓存
  2342. this.selectionCache.set(
  2343. currentPage,
  2344. new Set(selection.map((row) => row.id))
  2345. )
  2346. // 更新全局选择状态
  2347. const currentPageIds = new Set(this.templateList.map((row) => row.id))
  2348. // 移除当前页未选中的项
  2349. currentPageIds.forEach((id) => {
  2350. if (!selection.some((row) => row.id === id)) {
  2351. this.selectedTemplateIds.delete(id)
  2352. }
  2353. })
  2354. // 添加当前页选中的项
  2355. selection.forEach((row) => {
  2356. this.selectedTemplateIds.add(row.id)
  2357. })
  2358. // 更新全选状态
  2359. this.isAllSelected =
  2360. this.templateList.length > 0 &&
  2361. this.templateList.every((row) => this.selectedTemplateIds.has(row.id))
  2362. },
  2363. handleSelect(selection, row) {
  2364. if (!row || typeof row.id === 'undefined') {
  2365. console.warn('Invalid row data:', row)
  2366. return
  2367. }
  2368. const currentPage = this.templateQueryForm.page
  2369. const isSelected = selection.some((item) => item.id === row.id)
  2370. if (isSelected) {
  2371. this.selectedTemplateIds.add(row.id)
  2372. // 更新当前页选择记录
  2373. const currentPageSelected =
  2374. this.selectedPageIds.get(currentPage) || new Set()
  2375. currentPageSelected.add(row.id)
  2376. this.selectedPageIds.set(currentPage, currentPageSelected)
  2377. } else {
  2378. this.selectedTemplateIds.delete(row.id)
  2379. // 更新当前页选择记录
  2380. const currentPageSelected = this.selectedPageIds.get(currentPage)
  2381. if (currentPageSelected) {
  2382. currentPageSelected.delete(row.id)
  2383. }
  2384. // 取消全选状态
  2385. this.isAllSelected = false
  2386. }
  2387. },
  2388. // 修改加载方法
  2389. loadTemplateList() {
  2390. this.templateLoading = true
  2391. searchList(this.templateQueryForm)
  2392. .then((res) => {
  2393. if (res.status === 200) {
  2394. this.templateList = res.data.dataList
  2395. this.templateTotal = res.data.totalRecord
  2396. this.templateDialogVisible = true
  2397. // 在数据加载完成后设置选中状态
  2398. this.$nextTick(() => {
  2399. if (this.$refs.templateTable) {
  2400. // 清除当前选择
  2401. this.$refs.templateTable.clearSelection()
  2402. // 遍历当前页数据,根据 selectedTemplateIds 设置选中状态
  2403. this.templateList.forEach((row) => {
  2404. if (this.selectedTemplateIds.has(row.id)) {
  2405. this.$refs.templateTable.toggleRowSelection(row, true)
  2406. }
  2407. })
  2408. // 更新当前页的选择记录
  2409. const currentPage = this.templateQueryForm.page
  2410. const currentPageSelected = new Set(
  2411. this.templateList
  2412. .filter((row) => this.selectedTemplateIds.has(row.id))
  2413. .map((row) => row.id)
  2414. )
  2415. this.selectedPageIds.set(currentPage, currentPageSelected)
  2416. // 检查当前页是否全选
  2417. const isCurrentPageAllSelected = this.templateList.every(
  2418. (row) => this.selectedTemplateIds.has(row.id)
  2419. )
  2420. // 更新表头的全选框状态
  2421. if (this.$refs.templateTable.store) {
  2422. this.$refs.templateTable.store.states.isAllSelected =
  2423. isCurrentPageAllSelected
  2424. }
  2425. // 触发选择变化事件以更新UI
  2426. this.handleSelectionChange(
  2427. this.templateList.filter((row) =>
  2428. this.selectedTemplateIds.has(row.id)
  2429. )
  2430. )
  2431. }
  2432. })
  2433. }
  2434. })
  2435. .catch((error) => {
  2436. console.error('加载模板列表失败:', error)
  2437. this.$message.error('加载模板列表失败')
  2438. })
  2439. .finally(() => {
  2440. this.templateLoading = false
  2441. })
  2442. },
  2443. // 修改选择所有页方法
  2444. async selectAllPages() {
  2445. try {
  2446. this.isAllSelected = true
  2447. const allTemplates = await this.getAllTemplates()
  2448. // 清除之前的选择
  2449. this.selectedTemplateIds.clear()
  2450. this.selectedPageIds.clear()
  2451. // 添加所有模板ID到选择集合
  2452. allTemplates.forEach((template) => {
  2453. this.selectedTemplateIds.add(template.id)
  2454. })
  2455. // 更新当前页显示
  2456. if (this.$refs.templateTable) {
  2457. this.$refs.templateTable.clearSelection()
  2458. this.templateList.forEach((row) => {
  2459. this.$refs.templateTable.toggleRowSelection(row, true)
  2460. })
  2461. }
  2462. // 更新当前页的选择记录
  2463. const currentPage = this.templateQueryForm.page
  2464. this.selectedPageIds.set(
  2465. currentPage,
  2466. new Set(this.templateList.map((row) => row.id))
  2467. )
  2468. this.$message.success(`已选择全部 ${this.templateTotal} 条数据`)
  2469. } catch (error) {
  2470. console.error('选择所有页失败:', error)
  2471. this.$message.error('选择所有页失败')
  2472. this.isAllSelected = false
  2473. }
  2474. },
  2475. // 获取有模版数据
  2476. async getAllTemplates() {
  2477. const pageSize = 999 // 使用较大的页码一次性获取
  2478. const params = {
  2479. ...this.templateQueryForm,
  2480. page: 1,
  2481. pageSize
  2482. }
  2483. try {
  2484. const res = await searchList(params)
  2485. if (res.status === 200) {
  2486. return res.data.dataList
  2487. }
  2488. return []
  2489. } catch (error) {
  2490. console.error('获取所有模版失败:', error)
  2491. return []
  2492. }
  2493. },
  2494. // 确认选择
  2495. confirmTemplateSelection() {
  2496. if (this.selectedTemplateIds.size === 0) {
  2497. this.$message.warning('请至少选择一个模版')
  2498. return
  2499. }
  2500. // 转换Set为数组
  2501. const selectedIds = Array.from(this.selectedTemplateIds)
  2502. relate({
  2503. project_id: this.currentData.id,
  2504. doc_ids: JSON.stringify(selectedIds)
  2505. }).then((res) => {
  2506. if (res.status === 200) {
  2507. this.$message.success('模版关联成功')
  2508. this.closeTemplateDialog()
  2509. // 更新当前数据的documentIds
  2510. this.currentData.documentIds = selectedIds
  2511. this.search()
  2512. }
  2513. })
  2514. },
  2515. // 关对话框
  2516. closeTemplateDialog() {
  2517. this.templateDialogVisible = false
  2518. // 清除所有缓存
  2519. this.selectedTemplateIds.clear()
  2520. this.selectedPageIds.clear()
  2521. this.selectionCache.clear()
  2522. this.isAllSelected = false
  2523. if (this.$refs.templateTable) {
  2524. this.$refs.templateTable.clearSelection()
  2525. }
  2526. },
  2527. // 输入项目
  2528. enterPro(row) {
  2529. if (row.documentIds.length == 0) {
  2530. this.$message.warning('请先绑定模版!')
  2531. return
  2532. }
  2533. // 获取绑定模版替换位置信息
  2534. searchTemplateList({ ids: row.documentIds.join(',') || 0 })
  2535. .then(async(res) => {
  2536. if (res.status === 200) {
  2537. const templates = res.data
  2538. const locations = []
  2539. // 遍历所有模版,提取替换位置
  2540. templates.forEach((template) => {
  2541. if (template.data) {
  2542. const templateData = JSON.parse(template.data)
  2543. templateData.forEach((item) => {
  2544. const tempDiv = document.createElement('div')
  2545. tempDiv.innerHTML = item.content
  2546. const plainText = tempDiv.textContent || tempDiv.innerText
  2547. const patterns = [
  2548. /【([A-Z]{1,4}[0-9]{0,2})-([A-Z]{1,4}[0-9]{0,2})-([0-9]{2})-([0-9]{2,3})】/g,
  2549. /\[([A-Z]{1,4}[0-9]{0,2})-([A-Z]{1,4}[0-9]{0,2})-([0-9]{2})-([0-9]{2,3})\]/g
  2550. ]
  2551. patterns.forEach((pattern) => {
  2552. let match
  2553. while ((match = pattern.exec(plainText)) !== null) {
  2554. locations.push({
  2555. template_id: template.id,
  2556. template_title: template.title,
  2557. location: match[0],
  2558. type: this.getDocumentType(match[0])
  2559. })
  2560. }
  2561. })
  2562. })
  2563. }
  2564. })
  2565. try {
  2566. // 按类型分组位置信息
  2567. const params = {
  2568. template_location: [],
  2569. middle_layer_location: [],
  2570. tech_report_location: [],
  2571. other_location: []
  2572. }
  2573. // 遍历所有位置并分类
  2574. locations.forEach((loc) => {
  2575. Object.keys(params).forEach((key) => {
  2576. if (loc.type === key) {
  2577. params[key].push(loc.location.replace(/[\[\]【】]/g, '')) // 清除括号
  2578. } else {
  2579. params[key].push('')
  2580. }
  2581. })
  2582. })
  2583. params.project_id = row.id
  2584. // 获取替换内容
  2585. const response = await axios.post(
  2586. `${process.env.VUE_APP_BASE_API}/project-raw-data/batch-content`,
  2587. params,
  2588. {
  2589. headers: {
  2590. 'Content-Type': 'application/json'
  2591. }
  2592. }
  2593. )
  2594. if (response.status === 200 && response.data) {
  2595. const replacementContents = response.data.data.matched_data
  2596. // 将位置信息和对应的替换内容组合
  2597. const enrichedLocations = locations.map((loc, index) => ({
  2598. ...loc,
  2599. content: replacementContents[index]?.content || null,
  2600. template_location:
  2601. replacementContents[index]?.template_location || null,
  2602. tech_report_location:
  2603. replacementContents[index]?.tech_report_location || []
  2604. }))
  2605. // 对整个 enrichedLocations 进行去重
  2606. const uniqueEnrichedLocations = enrichedLocations.reduce(
  2607. (acc, current) => {
  2608. const isDuplicate = acc.find(
  2609. (item) =>
  2610. item.template_id === current.template_id &&
  2611. item.location === current.location
  2612. )
  2613. if (!isDuplicate) {
  2614. // 如果不是重复项,添加到结果数组中
  2615. return [...acc, current]
  2616. }
  2617. return acc
  2618. },
  2619. []
  2620. )
  2621. // 使用去重后的数据
  2622. this.$router.push({
  2623. path: '/project/ProjectInput',
  2624. query: {
  2625. id: row.id,
  2626. projectData: encodeURIComponent(
  2627. JSON.stringify({
  2628. locations: uniqueEnrichedLocations, // 使用去重后的数据
  2629. templates: templates.map((t) => ({
  2630. id: t.id,
  2631. title: t.title
  2632. }))
  2633. })
  2634. )
  2635. }
  2636. })
  2637. }
  2638. } catch (error) {
  2639. console.error('获取替换内容失败:', error)
  2640. this.$message.error('获取替换内容失败')
  2641. }
  2642. }
  2643. })
  2644. .catch((error) => {
  2645. console.error('获取模版位置信息失败:', error)
  2646. this.$message.error('获取模版位置信失败')
  2647. })
  2648. },
  2649. /* 产品类别名称 */
  2650. getCategoryTypeName(categoryId) {
  2651. if (!categoryId) return '未分类'
  2652. const product = this.productList.find((item) => item.id === categoryId)
  2653. return product && product.category ? product.category.name : '未知类'
  2654. },
  2655. getCategoryName(categoryId) {
  2656. if (!categoryId) return '未知产品'
  2657. const product = this.productList.find((item) => item.id === categoryId)
  2658. return product ? product.name : '未知产品'
  2659. },
  2660. searchData() {
  2661. const _this = this
  2662. _this.dialogVisible = false
  2663. _this.search()
  2664. },
  2665. btnDelete(e) {
  2666. const _this = this
  2667. const par = {
  2668. id: e
  2669. }
  2670. _this
  2671. .$confirm('您是否确认删除该记录?', '提示', {
  2672. confirmButtonText: '确认',
  2673. cancelButtonText: '取消',
  2674. type: 'warning'
  2675. })
  2676. .then((res) => {
  2677. // console.log("AAA")
  2678. deleteProject(par).then((res) => {
  2679. _this.search()
  2680. })
  2681. })
  2682. .catch(() => {})
  2683. },
  2684. // 编辑
  2685. btnEdit(e) {
  2686. const _this = this
  2687. _this.currentDataId = e.id
  2688. _this.dialogVisible = true
  2689. },
  2690. onClose(needRefresh) {
  2691. console.log('onClose triggered with needRefresh:', needRefresh) // 添加调试日志
  2692. this.currentDataId = 0
  2693. this.dialogVisible = false
  2694. if (needRefresh) {
  2695. this.search()
  2696. }
  2697. },
  2698. handleUpdate() {
  2699. this.search() // 添加额外的更新处理方法
  2700. },
  2701. // 搜索
  2702. async search() {
  2703. try {
  2704. // 1. 先加载并显示项目列表
  2705. const res = await searchProject(this.queryForm)
  2706. if (!res) return
  2707. // 2. 先用0初始化完成度,让列表可以立即渲染
  2708. const projectListFromTemplate = res.data.dataList.map((item) => ({
  2709. ...item,
  2710. calculate: 0
  2711. }))
  2712. // 3. 立即更新列表数据
  2713. this.dataList = projectListFromTemplate
  2714. this.recordCount = res.data.totalRecord
  2715. this.pageTotal = res.data.totalPage
  2716. // 4. 异步加载完成度
  2717. this.loadProjectCompletions(projectListFromTemplate)
  2718. } catch (err) {
  2719. console.error('搜索失败:', err)
  2720. this.$message.error('获取项目列表失败')
  2721. }
  2722. },
  2723. // 修改分页
  2724. ChangePage(e) {
  2725. const _this = this
  2726. _this.queryForm.page = e
  2727. _this.search()
  2728. },
  2729. init() {
  2730. searchCategory().then((res) => {
  2731. if (res.status !== 200) return
  2732. this.categoryList = res.data.dataList
  2733. })
  2734. /* 获取阶段 */
  2735. getphases().then((res) => {
  2736. this.currentPhaseList = res.data.options
  2737. })
  2738. /* 获取目前态 */
  2739. getstatus().then((res) => {
  2740. this.getstatusList = res.data.options
  2741. })
  2742. /* 获取客户列表 */
  2743. searchCustomer({ page: 1, pageSize: 999 }).then((res) => {
  2744. this.customList = res.data.dataList
  2745. })
  2746. /* 获取产品列表 */
  2747. searchProduct({ page: 1, pageSize: 999 }).then((res) => {
  2748. this.productList = res.data.dataList
  2749. })
  2750. },
  2751. // 新增:处理导出命令
  2752. async handleExport(command, row) {
  2753. switch (command) {
  2754. case 'word':
  2755. await this.exportDocumentWithHtmlDocx(row) // this.exportDocument(row);
  2756. break
  2757. case 'pdf':
  2758. await this.exportToPDF(row)
  2759. break
  2760. case 'pdf-to-word': // 添加新的命令类型
  2761. const pdfResult = await this.exportToPDF_url(row)
  2762. /* if (pdfResult && pdfResult.file_path) {
  2763. await this.convertPDFToWord(pdfResult.file_path);
  2764. } */
  2765. break
  2766. }
  2767. },
  2768. /* 过去pdf链接 */
  2769. async exportToPDF_url(template) {
  2770. try {
  2771. // 解析模版内容
  2772. const templateData = JSON.parse(template.data || '[]')
  2773. // 创建一个临时的容器来存放内容
  2774. const contentContainer = document.createElement('div')
  2775. contentContainer.className = 'content'
  2776. // 处理每个模版段落
  2777. templateData.forEach((item) => {
  2778. const templateSection = document.createElement('div')
  2779. templateSection.className = 'template-textarea'
  2780. templateSection.innerHTML = item.content
  2781. contentContainer.appendChild(templateSection)
  2782. })
  2783. // 处理所有输入框
  2784. contentContainer
  2785. .querySelectorAll('input[type="text"]')
  2786. .forEach((input) => {
  2787. const span = document.createElement('span')
  2788. span.textContent = input.value
  2789. input.parentNode.replaceChild(span, input)
  2790. })
  2791. // 构建HTML内容
  2792. const contentData = `<!DOCTYPE html>
  2793. <html>
  2794. <head>
  2795. <meta charset="UTF-8">
  2796. <style>
  2797. @page {
  2798. size: A4;
  2799. margin: 2cm;
  2800. }
  2801. body {
  2802. font-family: Arial, sans-serif;
  2803. font-size: 12pt;
  2804. line-height: 1.5;
  2805. }
  2806. /* 添加图片控制样式 */
  2807. img {
  2808. max-width: 100%;
  2809. max-height: 100vh;
  2810. object-fit: contain;
  2811. page-break-inside: avoid;
  2812. }
  2813. /* 图片容器样式 */
  2814. .image-container {
  2815. width: 100%;
  2816. text-align: center;
  2817. margin: 10px 0;
  2818. page-break-inside: avoid;
  2819. }
  2820. table {
  2821. width: 100%;
  2822. border-collapse: collapse;
  2823. }
  2824. td, th {
  2825. border: 1px solid black;
  2826. padding: 5px;
  2827. }
  2828. .template-textarea {
  2829. page-break-after: always;
  2830. }
  2831. .TOC {
  2832. display: none !important;
  2833. }
  2834. </style>
  2835. </head>
  2836. <body>
  2837. ${this.processImages(contentContainer.innerHTML)}
  2838. </body>
  2839. </html>`
  2840. /* // 处理表格样式
  2841. contentData = contentData
  2842. .replaceAll("<table ", `<table style="border-collapse: collapse;" `)
  2843. .replaceAll("<td>", `<td style="border: 1px solid black;">`); */
  2844. const formData = new FormData()
  2845. formData.append('content', contentData)
  2846. formData.append('title', template.title)
  2847. // 调用导出接口,指定导出类型为 PDF
  2848. const res = await axios.post(
  2849. `${process.env.VUE_APP_BASE_API}/document/htmlPDFWord`,
  2850. formData,
  2851. {
  2852. headers: {
  2853. 'Content-Type': 'multipart/form-data' // 设置请求头为form-data
  2854. }
  2855. }
  2856. ) /* await htmlPDFWord({
  2857. content: contentData,
  2858. title: template.title,
  2859. }); */
  2860. if (res.status !== 200) {
  2861. this.$alert(res.errMsg)
  2862. return
  2863. }
  2864. console.log(res)
  2865. // 处理文件下载
  2866. const link = document.createElement('a')
  2867. link.href = res.data.data.file_path
  2868. link.download = res.data.data.file_name
  2869. document.body.appendChild(link)
  2870. link.click()
  2871. document.body.removeChild(link)
  2872. this.$message.success('导出成功')
  2873. // 处理文件下载
  2874. return res.data // 返回结果,包含 file_path
  2875. } catch (error) {
  2876. console.error('导出PDF失败:', error)
  2877. this.$alert('导出PDF时发生错误,请稍后重试。')
  2878. }
  2879. },
  2880. // 添加新的 exportToPDF 方法
  2881. async exportToPDF(template) {
  2882. /* const res = await searchlistDocTemplate({
  2883. project_id:this.currentData.id,
  2884. id: template.id
  2885. })
  2886. if(res.status!==200) return
  2887. let templateData */
  2888. try {
  2889. // 1. 先获取文档模板数据
  2890. const resData = await searchlistDocTemplate({
  2891. project_id: this.currentData.id,
  2892. id: template.id
  2893. })
  2894. if (resData.status !== 200) {
  2895. this.$message.error('获取文档模板失败')
  2896. return
  2897. }
  2898. // 2. 获取模板数据
  2899. let templateData
  2900. if (resData.data.documents.length !== 0) {
  2901. templateData = JSON.parse(resData.data.documents[0].data || '[]')
  2902. } else {
  2903. const templateDataRes = await getDocumentInfo({id: template.id})
  2904. if (templateDataRes.status !== 200) {
  2905. this.$message.error('获取文档信息失败')
  2906. return
  2907. }
  2908. templateData = JSON.parse(templateDataRes.data.data || '[]')
  2909. }
  2910. if (!templateData || !Array.isArray(templateData)) {
  2911. this.$message.error('无效的模板数据格式')
  2912. return
  2913. }
  2914. // 创建一个临时的容器来存放内容
  2915. const contentContainer = document.createElement('div')
  2916. contentContainer.className = 'content'
  2917. // 处理每个模版段落
  2918. templateData.forEach((item) => {
  2919. const templateSection = document.createElement('div')
  2920. templateSection.className = 'template-textarea'
  2921. templateSection.innerHTML = item.content
  2922. contentContainer.appendChild(templateSection)
  2923. })
  2924. // 处理所有输入框
  2925. contentContainer
  2926. .querySelectorAll('input[type="text"]')
  2927. .forEach((input) => {
  2928. const span = document.createElement('span')
  2929. span.textContent = input.value
  2930. input.parentNode.replaceChild(span, input)
  2931. })
  2932. // 构建HTML内容
  2933. const contentData = `<!DOCTYPE html>
  2934. <html>
  2935. <head>
  2936. <meta charset="UTF-8">
  2937. <style>
  2938. @page {
  2939. size: A4;
  2940. margin: 2cm;
  2941. }
  2942. body {
  2943. font-family: Arial, sans-serif;
  2944. font-size: 12pt;
  2945. line-height: 1.5;
  2946. }
  2947. /* 添加图片控制样式 */
  2948. img {
  2949. max-width: 100%;
  2950. max-height: 100vh;
  2951. object-fit: contain;
  2952. page-break-inside: avoid;
  2953. }
  2954. /* 图片容器样式 */
  2955. .image-container {
  2956. width: 100%;
  2957. text-align: center;
  2958. margin: 10px 0;
  2959. page-break-inside: avoid;
  2960. }
  2961. table {
  2962. width: 100%;
  2963. border-collapse: collapse;
  2964. }
  2965. td, th {
  2966. border: 1px solid black;
  2967. padding: 5px;
  2968. }
  2969. .template-textarea {
  2970. page-break-after: always;
  2971. }
  2972. .TOC {
  2973. display: none !important;
  2974. }
  2975. </style>
  2976. </head>
  2977. <body>
  2978. ${this.processImages(contentContainer.innerHTML)}
  2979. </body>
  2980. </html>`
  2981. /* // 处理表格样式
  2982. contentData = contentData
  2983. .replaceAll("<table ", `<table style="border-collapse: collapse;" `)
  2984. .replaceAll("<td>", `<td style="border: 1px solid black;">`); */
  2985. // 调用导出接口,指定导出类型为 PDF
  2986. const res = await exportToPDF({
  2987. content: contentData,
  2988. title: template.title
  2989. })
  2990. if (res.status !== 200) {
  2991. this.$alert(res.errMsg)
  2992. return
  2993. }
  2994. // 处理文件下载
  2995. const link = document.createElement('a')
  2996. link.href = res.data.file_path
  2997. link.download = res.data.file_name
  2998. document.body.appendChild(link)
  2999. link.click()
  3000. document.body.removeChild(link)
  3001. this.$message.success('导出成功')
  3002. return res.data // 返回结果,包含 file_path
  3003. } catch (error) {
  3004. console.error('导出PDF失败:', error)
  3005. this.$alert('导出PDF时发生错误,请稍后重试。')
  3006. }
  3007. },
  3008. // 添加处理图片的辅助方法
  3009. processImages(content) {
  3010. const tempDiv = document.createElement('div')
  3011. tempDiv.innerHTML = content
  3012. // 处理所有图片
  3013. tempDiv.querySelectorAll('img').forEach((img) => {
  3014. // 创建图片容器
  3015. const container = document.createElement('div')
  3016. container.className = 'image-container'
  3017. // 获取所有可能的尺寸来源
  3018. let width =
  3019. img.getAttribute('width') || img.style.width || img.width || null
  3020. let height =
  3021. img.getAttribute('height') || img.style.height || img.height || null
  3022. // 移除单位并转换为数字
  3023. if (typeof width === 'string') {
  3024. width = parseInt(width.replace(/[^0-9]/g, ''))
  3025. }
  3026. if (typeof height === 'string') {
  3027. height = parseInt(height.replace(/[^0-9]/g, ''))
  3028. }
  3029. // 如果没有尺寸,创建一个临时图片来获取实际尺寸
  3030. if (!width || !height) {
  3031. const tempImg = new Image()
  3032. tempImg.src = img.src
  3033. width = tempImg.width || 595 // 默认宽度
  3034. height = tempImg.height || Math.round(width * 0.75) // 默认高度比例
  3035. }
  3036. // 确保数值有效
  3037. width = Math.max(1, parseInt(width))
  3038. height = Math.max(1, parseInt(height))
  3039. // 处理尺寸
  3040. const maxWidth = 595 // A4纸宽度(像素,减去边距)
  3041. if (width > maxWidth) {
  3042. // 等比例缩放
  3043. const aspectRatio = width / height
  3044. const newHeight = Math.round(maxWidth / aspectRatio)
  3045. width = maxWidth
  3046. height = newHeight
  3047. }
  3048. // 同时设置style和属性
  3049. img.style.width = `${width}px`
  3050. img.style.height = `${height}px`
  3051. img.setAttribute('width', width)
  3052. img.setAttribute('height', height)
  3053. // 将图片包装在容器中
  3054. img.parentNode.insertBefore(container, img)
  3055. container.appendChild(img)
  3056. })
  3057. return tempDiv.innerHTML
  3058. },
  3059. // 添加一个方法来显示选中数量
  3060. getSelectedCount() {
  3061. return this.selectedTemplateIds.size
  3062. },
  3063. // 添加处理每页条数变化的方法
  3064. handleSizeChange(val) {
  3065. // 保存当前的选择状态
  3066. const currentSelection = this.$refs.templateTable?.selection || []
  3067. currentSelection.forEach((row) => {
  3068. this.selectedTemplateIds.add(row.id)
  3069. })
  3070. // 更新每页显示条数
  3071. this.templateQueryForm.pageSize = val
  3072. // 重置为第一页
  3073. this.templateQueryForm.page = 1
  3074. // 重新加载数据
  3075. this.loadTemplateList()
  3076. },
  3077. // 处理文档类型变化
  3078. handleDocTypeChange(value) {
  3079. this.templateQueryForm.document_type = value
  3080. this.templateQueryForm.page = 1 // 重置页码
  3081. this.loadTemplateList() // 重新加载列表
  3082. },
  3083. // 新增方法:异步加载完成度
  3084. async loadProjectCompletions(projects) {
  3085. // 使用 Promise.all 和 chunk 分批处理
  3086. const chunkSize = 5 // 每批处理5个项目
  3087. const chunks = this.chunkArray(projects, chunkSize)
  3088. for (const chunk of chunks) {
  3089. const promises = chunk.map(async(project) => {
  3090. try {
  3091. const calcRes = await calculate({ id: project.id })
  3092. if (calcRes.status === 200) {
  3093. // 使用 Vue.set 更新单个项目的完成度
  3094. const index = this.dataList.findIndex(
  3095. (item) => item.id === project.id
  3096. )
  3097. if (index !== -1) {
  3098. this.$set(
  3099. this.dataList[index],
  3100. 'calculate',
  3101. calcRes.data.completionRate
  3102. )
  3103. }
  3104. }
  3105. } catch (err) {
  3106. console.error(`获取项目${project.id}完成度失败:`, err)
  3107. }
  3108. })
  3109. await Promise.all(promises)
  3110. }
  3111. },
  3112. // 辅助方法:将数组分成小块
  3113. chunkArray(array, size) {
  3114. const chunks = []
  3115. for (let i = 0; i < array.length; i += size) {
  3116. chunks.push(array.slice(i, i + size))
  3117. }
  3118. return chunks
  3119. },
  3120. // 生成目录的方法
  3121. generateTableOfContents(templateData) {
  3122. let toc = ''
  3123. let currentPage = 1
  3124. // 定义章节标题数组
  3125. const sections = [
  3126. '1 PURPOSE OF THE RISK MANAGEMENT REVIEW',
  3127. '2 SCOPE OF APPLICATION',
  3128. '3 RISK MANAGEMENT PROCESS',
  3129. '4 REFERENCES',
  3130. '5 RISK MANAGEMENT REVIEW PERSONNEL',
  3131. '6 RISK ACCEPTABILITY CRITERIA',
  3132. '7 RISK MANAGEMENT REVIEW',
  3133. '8 PRODUCTION AND POST-PRODUCTION ACTIVITIES',
  3134. '9 CONCLUSIONS OF RISK MANAGEMENT REVIEW',
  3135. '10 THE FILE REVIEW COMPLETED',
  3136. 'ANNEX A: QUESTION LIST USED TO IDENTIFY MEDICAL DEVICE SAFETY CHARACTERISTICS',
  3137. 'ANNEX B: IDENTIFICATION OF KNOWN OR FORESEEABLE HAZARDS',
  3138. 'ANNEX C: DETERMINATION OF SOFTWARE SAFETY CLASS',
  3139. 'ANNEX D: RISK ANALYSIS AND CONTROL MEASURES',
  3140. 'ANNEX E: TRACEABILITY'
  3141. ]
  3142. sections.forEach((section) => {
  3143. toc += `
  3144. <div class="toc-item">
  3145. <span class="toc-text">${section}</span>
  3146. <span class="toc-page">${currentPage}</span>
  3147. </div>
  3148. `
  3149. currentPage++ // 简单估算每个章节占一页
  3150. })
  3151. return toc
  3152. },
  3153. // 处理正文内容的方法
  3154. processContentWithSections(templateData) {
  3155. let processedContent = ''
  3156. let currentPage = 1
  3157. templateData.forEach((item, index) => {
  3158. // 添加章节标题
  3159. processedContent += `
  3160. <div class="section-title">
  3161. ${this.getSectionTitle(index + 1)}
  3162. </div>
  3163. <div class="section-content">
  3164. ${item.content}
  3165. </div>
  3166. <div class="page-number">${currentPage}</div>
  3167. `
  3168. currentPage++
  3169. })
  3170. return processedContent
  3171. },
  3172. // 获取章节标题的辅助方法
  3173. getSectionTitle(index) {
  3174. const titles = {
  3175. 1: '1 PURPOSE OF THE RISK MANAGEMENT REVIEW',
  3176. 2: '2 SCOPE OF APPLICATION',
  3177. 3: '3 RISK MANAGEMENT PROCESS',
  3178. 4: '4 REFERENCES',
  3179. 5: '5 RISK MANAGEMENT REVIEW PERSONNEL',
  3180. 6: '6 RISK ACCEPTABILITY CRITERIA',
  3181. 7: '7 RISK MANAGEMENT REVIEW',
  3182. 8: '8 PRODUCTION AND POST-PRODUCTION ACTIVITIES',
  3183. 9: '9 CONCLUSIONS OF RISK MANAGEMENT REVIEW',
  3184. 10: '10 THE FILE REVIEW COMPLETED',
  3185. 11: 'ANNEX A: QUESTION LIST USED TO IDENTIFY MEDICAL DEVICE SAFETY CHARACTERISTICS',
  3186. 12: 'ANNEX B: IDENTIFICATION OF KNOWN OR FORESEEABLE HAZARDS',
  3187. 13: 'ANNEX C: DETERMINATION OF SOFTWARE SAFETY CLASS',
  3188. 14: 'ANNEX D: RISK ANALYSIS AND CONTROL MEASURES',
  3189. 15: 'ANNEX E: TRACEABILITY'
  3190. }
  3191. return titles[index] || `${index} SECTION`
  3192. },
  3193. // 估算页数的方法
  3194. estimatePageCount(content) {
  3195. const tempDiv = document.createElement('div')
  3196. tempDiv.style.cssText = `
  3197. width: 595px; /* A4 宽度 */
  3198. font-size: 12pt;
  3199. line-height: 1.5;
  3200. visibility: hidden;
  3201. position: absolute;
  3202. `
  3203. tempDiv.innerHTML = content
  3204. document.body.appendChild(tempDiv)
  3205. const contentHeight = tempDiv.offsetHeight
  3206. const pageHeight = 842 // A4 高度
  3207. document.body.removeChild(tempDiv)
  3208. return Math.ceil(contentHeight / pageHeight)
  3209. },
  3210. // 加载文档版本列表
  3211. async loadDocumentVersions(template) {
  3212. this.documentVersionsLoading = true
  3213. this.documentSelectVisible = true
  3214. try {
  3215. const res = await searchlistDoc({
  3216. page: 1,
  3217. pageSize: 999,
  3218. template_id: template.id
  3219. })
  3220. if (res.status === 200) {
  3221. this.documentVersions = res.data.dataList
  3222. if (this.documentVersions.length === 0) {
  3223. this.$message.warning('未找到相关文档版本')
  3224. }
  3225. }
  3226. } catch (error) {
  3227. console.error('加载文档版本失败:', error)
  3228. this.$message.error('加载文档版本失败')
  3229. } finally {
  3230. this.documentVersionsLoading = false
  3231. }
  3232. },
  3233. // 处理文档预览
  3234. handleDocPreview(doc) {
  3235. const url = `#/document/viewCreate?articleId=${doc.id}&type=document`
  3236. window.open(url, '_blank')
  3237. this.documentSelectVisible = false
  3238. },
  3239. // 处理文档导出
  3240. async handleDocExport(command, doc) {
  3241. try {
  3242. switch (command) {
  3243. case 'word':
  3244. // 获取完整的文档信息
  3245. const docInfo = await getDocumentInfo({ id: doc.id })
  3246. if (docInfo.status === 200 && docInfo.data) {
  3247. // 使用 exportDocumentWithHtmlDocx 方法导出
  3248. await this.exportDocumentWithHtmlDocx(docInfo.data)
  3249. } else {
  3250. throw new Error('获取文档信息失败')
  3251. }
  3252. break
  3253. case 'pdf':
  3254. // 保持原有的 PDF 导出功能不变
  3255. const res = await exportToPDF({
  3256. content: doc.content,
  3257. title: doc.title
  3258. })
  3259. if (res.status === 200) {
  3260. const link = document.createElement('a')
  3261. link.href = res.data.file_path
  3262. link.download = `${doc.title}.pdf`
  3263. document.body.appendChild(link)
  3264. link.click()
  3265. document.body.removeChild(link)
  3266. } else {
  3267. throw new Error(res.message || '导出失败')
  3268. }
  3269. break
  3270. }
  3271. this.$message.success('导出成功')
  3272. } catch (error) {
  3273. console.error('导出失败:', error)
  3274. this.$message.error(error.message || '导出失败')
  3275. }
  3276. this.documentSelectVisible = false
  3277. }
  3278. }
  3279. }
  3280. </script>
  3281. <style lang="scss" scoped>
  3282. @import "./dataList.scss";
  3283. .match-results {
  3284. margin-top: 20px;
  3285. .match-item {
  3286. padding: 10px;
  3287. border-bottom: 1px solid #eee;
  3288. display: flex;
  3289. justify-content: space-between;
  3290. align-items: center;
  3291. .match-content {
  3292. flex: 1;
  3293. display: flex;
  3294. gap: 20px;
  3295. }
  3296. .actions {
  3297. margin-left: 10px;
  3298. }
  3299. }
  3300. }
  3301. // 完成度文字样式
  3302. .completion-high {
  3303. color: #67c23a;
  3304. font-weight: bold;
  3305. }
  3306. .completion-medium {
  3307. color: #e6a23c;
  3308. font-weight: bold;
  3309. }
  3310. .completion-low {
  3311. color: #f56c6c;
  3312. font-weight: bold;
  3313. }
  3314. // 添加PDF相关样式
  3315. .content {
  3316. background: white;
  3317. .template-textarea {
  3318. page-break-after: always;
  3319. table {
  3320. width: 100%;
  3321. border-collapse: collapse;
  3322. td,
  3323. th {
  3324. border: 1px solid black;
  3325. padding: 5px;
  3326. }
  3327. }
  3328. }
  3329. }
  3330. .header_t {
  3331. margin: 10px 0;
  3332. }
  3333. /* 添加分页样式 */
  3334. .pagination-container {
  3335. margin-top: 15px;
  3336. padding: 10px 0;
  3337. text-align: right;
  3338. }
  3339. .filter-section {
  3340. display: flex;
  3341. align-items: center;
  3342. gap: 10px;
  3343. padding: 0 20px;
  3344. }
  3345. /* 添加到已有的 style 标签中 */
  3346. .dialog-header {
  3347. display: flex;
  3348. justify-content: space-between;
  3349. align-items: center;
  3350. padding: 0 20px;
  3351. }
  3352. .header-left {
  3353. display: flex;
  3354. gap: 10px;
  3355. }
  3356. .header-right {
  3357. display: flex;
  3358. align-items: center;
  3359. }
  3360. </style>