dataList.vue 60 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916
  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 @command="handleCommand" :data="scope.row">
  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. ></el-button>
  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. >
  72. <el-dropdown-item :command="{ type: 'enter', row: scope.row }"
  73. >输入项目</el-dropdown-item
  74. >
  75. <el-dropdown-item
  76. :command="{ type: 'viewTemplates', row: scope.row }"
  77. v-if="
  78. scope.row.documentIds && scope.row.documentIds.length > 0
  79. "
  80. >
  81. 已绑定模版
  82. </el-dropdown-item>
  83. <el-dropdown-item :command="{ type: 'edit', row: scope.row }">
  84. <svg-icon icon-class="edit" />编辑
  85. </el-dropdown-item>
  86. <el-dropdown-item :command="{ type: 'delete', row: scope.row }">
  87. <svg-icon icon-class="delete" />删除
  88. </el-dropdown-item>
  89. </el-dropdown-menu>
  90. </template>
  91. </el-dropdown>
  92. </template>
  93. <!-- <template #default="scope">
  94. <div class="btns">
  95. <el-button type="primary" size="small" @click="selTemplate(scope.row)"
  96. >选择模版</el-button
  97. >
  98. <el-button type="primary" size="small" @click="enterPro(scope.row)"
  99. >输入项目</el-button
  100. >
  101. <el-button type="primary" size="small" @click="btnEdit(scope.row)"
  102. ><svg-icon icon-class="edit" />编辑</el-button
  103. >
  104. <el-button
  105. type="danger"
  106. size="small"
  107. @click="btnDelete(scope.row.id)"
  108. ><svg-icon icon-class="delete" />删除</el-button
  109. >
  110. </div>
  111. </template> -->
  112. </el-table-column>
  113. </el-table>
  114. <div class="page-info">
  115. <el-pagination
  116. :currentPage="queryForm.page"
  117. :page-size="queryForm.pageSize"
  118. :total="recordCount"
  119. :page-count="pageTotal"
  120. background
  121. layout="prev, pager, next"
  122. @current-change="ChangePage"
  123. >
  124. </el-pagination>
  125. </div>
  126. <el-dialog
  127. :visible.sync="dialogVisible"
  128. append-to-body
  129. v-el-drag-dialog
  130. custom-class="prod-verify"
  131. title="创建项目信息"
  132. >
  133. <dataInfo :id="currentDataId" @onClose="onClose"></dataInfo>
  134. </el-dialog>
  135. <!-- 选择模版 -->
  136. <el-dialog
  137. title="选择模版"
  138. :visible.sync="templateDialogVisible"
  139. width="50%"
  140. append-to-body
  141. >
  142. <el-table
  143. :data="templateList"
  144. style="width: 100%"
  145. @selection-change="handleTemplateSelect"
  146. @select-all="handleSelectAll"
  147. ref="templateTable"
  148. >
  149. <el-table-column type="selection" width="55"></el-table-column>
  150. <el-table-column prop="id" label="ID" width="80" align="center" />
  151. <el-table-column prop="title" label="模版名称" align="left" />
  152. <el-table-column prop="create_time" label="创建时间" align="center" />
  153. </el-table>
  154. <div class="pagination-container">
  155. <el-pagination
  156. :current-page="templateQueryForm.page"
  157. :page-size="templateQueryForm.pageSize"
  158. :total="templateTotal"
  159. background
  160. layout="prev, pager, next"
  161. @current-change="handleTemplatePageChange"
  162. >
  163. </el-pagination>
  164. </div>
  165. <div class="dialog-footer" slot="footer">
  166. <el-button @click="templateDialogVisible = false">取 消</el-button>
  167. <el-button type="primary" @click="confirmTemplateSelection"
  168. >确 定</el-button
  169. >
  170. </div>
  171. </el-dialog>
  172. <el-dialog
  173. title="已绑定模版"
  174. :visible.sync="boundTemplatesDialogVisible"
  175. width="50%"
  176. append-to-body
  177. >
  178. <el-table
  179. :data="boundTemplateList"
  180. style="width: 100%"
  181. @selection-change="handleBoundTemplateSelect"
  182. ref="boundTemplateTable"
  183. >
  184. <el-table-column type="selection" width="55"></el-table-column>
  185. <el-table-column prop="id" label="ID" width="80" align="center" />
  186. <el-table-column prop="title" label="模版名称" align="left" />
  187. <el-table-column prop="createTime" label="绑定时间" align="center" />
  188. <el-table-column label="操作" align="center" width="320">
  189. <template #default="scope">
  190. <el-button type="primary" @click="previewTemplate(scope.row)"
  191. >预览</el-button
  192. >
  193. <el-button type="primary" @click="replaceTemplate(scope.row)"
  194. >替换</el-button
  195. >
  196. <el-dropdown
  197. style="margin-left: 10px"
  198. trigger="click"
  199. @command="(command) => handleExport(command, scope.row)"
  200. >
  201. <el-button type="success">
  202. 导出文档<i class="el-icon-arrow-down el-icon--right"></i>
  203. </el-button>
  204. <template #dropdown>
  205. <el-dropdown-menu>
  206. <el-dropdown-item command="word">Word格式</el-dropdown-item>
  207. <el-dropdown-item command="pdf">PDF格式</el-dropdown-item>
  208. <!-- <el-dropdown-item command="pdf-to-word"
  209. >PDF转Word</el-dropdown-item
  210. > -->
  211. </el-dropdown-menu>
  212. </template>
  213. </el-dropdown>
  214. </template>
  215. </el-table-column>
  216. </el-table>
  217. <div class="dialog-footer" style="margin-top: 20px; text-align: right">
  218. <el-button
  219. type="primary"
  220. @click="batchReplace"
  221. :loading="batchReplaceLoading"
  222. :disabled="selectedBoundTemplates.length === 0"
  223. >
  224. 批量替换
  225. </el-button>
  226. </div>
  227. </el-dialog>
  228. <el-dialog
  229. title=""
  230. :visible.sync="replaceVisible"
  231. width="50%"
  232. append-to-body
  233. >
  234. <!-- 显示匹配到的结果 -->
  235. <div v-if="matchResults.length" class="match-results">
  236. <div
  237. v-for="(item, index) in matchResults"
  238. :key="index"
  239. class="match-item"
  240. >
  241. <div class="match-content">
  242. <span>原始值: {{ item }}</span>
  243. <!-- <span>替换后: {{ item }}</span> -->
  244. </div>
  245. <div class="actions">
  246. <el-button size="small" @click="replaceItem(item)">替换</el-button>
  247. </div>
  248. </div>
  249. <div style="display: flex; justify-content: flex-end; margin-top: 20px">
  250. <el-button type="primary" @click="replaceAll">批量替换</el-button>
  251. </div>
  252. </div>
  253. </el-dialog>
  254. </div>
  255. </template>
  256. <script>
  257. import {
  258. searchProject,
  259. deleteProject,
  260. searchCategory,
  261. relate,
  262. searchTemplateList,
  263. getstatus,
  264. calculate,
  265. getphases,
  266. } from "@/api/project";
  267. import {
  268. searchList,
  269. updateDocument,
  270. exportDocument,
  271. exportToPDF,
  272. } from "@/api/document";
  273. import { searchCustomer } from "@/api/customer";
  274. import { searchProduct } from "@/api/product";
  275. /* import {searchTemplate} from "@/api/template" */
  276. import dataInfo from "./dataInfo.vue";
  277. import elDragDialog from "@/directive/el-drag-dialog";
  278. import axios from "axios";
  279. import jsPDF from "jspdf";
  280. import html2canvas from "html2canvas";
  281. export default {
  282. components: {
  283. dataInfo,
  284. },
  285. directives: { elDragDialog },
  286. props: {
  287. queryForm: {
  288. type: Object,
  289. default: () => {
  290. return {
  291. page: 1,
  292. pageSize: 10,
  293. name: "",
  294. category_id: "",
  295. status: "",
  296. };
  297. },
  298. },
  299. },
  300. watch: {
  301. queryForm: {
  302. handler: function (val) {
  303. this.search();
  304. },
  305. // immediate: true,//立即执行
  306. deep: true,
  307. },
  308. },
  309. data() {
  310. return {
  311. templateQueryForm: {
  312. page: 1,
  313. pageSize: 10,
  314. },
  315. templateTotal: 0, // 模版总数
  316. dialogVisible: false, //控制产品审核
  317. currentDataId: 0,
  318. recordCount: 0,
  319. pageTotal: 1,
  320. dataList: [],
  321. currentData: {},
  322. categoryList: [],
  323. templateDialogVisible: false,
  324. templateList: [],
  325. selectedTemplates: [], // 改为数组以支持多选
  326. boundTemplatesDialogVisible: false,
  327. previewDialogVisible: false,
  328. boundTemplateList: [],
  329. templatePreviewContent: "",
  330. currentTemplateId: null,
  331. replaceVisible: false,
  332. loading: false, // 增加 loading 状态
  333. searchResult: {},
  334. matchResults: [], // 存储匹配到的结果
  335. TemList: [],
  336. docAttr: {},
  337. templateLoading: false, // 添加模版加载状态
  338. boundTemplateLoading: false, // 添加已绑定模版加载状态
  339. selectedBoundTemplates: [], // 新增:已选择的绑定模版
  340. batchReplaceLoading: false, // 新增:批量替换loading状态
  341. getstatusList: [], //状态列表
  342. customList: [], //客户列表
  343. productList: [], //产品列表
  344. currentPhaseList: [], //阶段表
  345. selectedTemplateIds: new Set(), // 新增: 用于存储所有已选择的模版ID
  346. isAllSelected: false, // 新增: 标记是否全选所有页
  347. selectedPageIds: new Map(), // 新增: 存储每页选中的ID
  348. };
  349. },
  350. mounted() {
  351. this.search();
  352. this.init();
  353. },
  354. methods: {
  355. // 添加 PDF 转 Word 的方法
  356. async convertPDFToWord(pdfPath) {
  357. try {
  358. // 创建FormData对象
  359. const formData = new FormData();
  360. formData.append("oss_url", pdfPath);
  361. // 调用后端转换接口
  362. const res = await axios.post(
  363. `${process.env.VUE_APP_BASE_API}/convert/pdftoword`,
  364. formData,
  365. {
  366. headers: {
  367. "Content-Type": "multipart/form-data", // 设置请求头为form-data
  368. },
  369. }
  370. );
  371. if (res.status === 200) {
  372. // 将base64转换为Blob
  373. const byteCharacters = atob(res.data.data.file_content);
  374. const byteNumbers = new Array(byteCharacters.length);
  375. for (let i = 0; i < byteCharacters.length; i++) {
  376. byteNumbers[i] = byteCharacters.charCodeAt(i);
  377. }
  378. const byteArray = new Uint8Array(byteNumbers);
  379. const blob = new Blob([byteArray], {
  380. type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
  381. });
  382. // 创建下载链接
  383. const link = document.createElement("a");
  384. link.href = URL.createObjectURL(blob);
  385. link.download = "converted.docx"; // 或使用服务器返回的文件名
  386. document.body.appendChild(link);
  387. link.click();
  388. document.body.removeChild(link);
  389. // 清理URL对象
  390. URL.revokeObjectURL(link.href);
  391. this.$message.success("转换成功");
  392. } else {
  393. throw new Error(res.data.message || "转换失败");
  394. }
  395. } catch (error) {
  396. console.error("PDF转Word失败:", error);
  397. this.$message.error("PDF转Word失败,请稍后重试");
  398. }
  399. },
  400. // 修改后的导出文档方法
  401. async exportDocument(template) {
  402. try {
  403. // 解析模版内容
  404. const templateData = JSON.parse(template.data || "[]");
  405. // 创建一个临时的容器来存放内容
  406. const contentContainer = document.createElement("div");
  407. contentContainer.className = "content";
  408. // 处理每个模版段落
  409. templateData.forEach((item) => {
  410. const templateSection = document.createElement("div");
  411. templateSection.className = "template-textarea";
  412. // 创建临时div来解析HTML
  413. const tempDiv = document.createElement("div");
  414. tempDiv.innerHTML = item.content;
  415. // 处理标题格式
  416. tempDiv
  417. .querySelectorAll("h1, h2, h3, h4, h5, h6")
  418. .forEach((heading) => {
  419. heading.style.fontWeight = "bold";
  420. heading.style.margin = "1em 0";
  421. switch (heading.tagName.toLowerCase()) {
  422. case "h1":
  423. heading.style.fontSize = "24pt";
  424. break;
  425. case "h2":
  426. heading.style.fontSize = "18pt";
  427. break;
  428. case "h3":
  429. heading.style.fontSize = "14pt";
  430. break;
  431. case "h4":
  432. case "h5":
  433. case "h6":
  434. heading.style.fontSize = "12pt";
  435. break;
  436. }
  437. });
  438. // 处理表格
  439. tempDiv.querySelectorAll("table").forEach((table) => {
  440. // 设置表格属性
  441. table.setAttribute("border", "1");
  442. table.setAttribute("cellspacing", "0");
  443. table.setAttribute("cellpadding", "5");
  444. table.style.borderCollapse = "collapse";
  445. table.style.width = "100%";
  446. table.style.border = "1px solid black";
  447. // 处理所有单元格
  448. const cells = table.querySelectorAll("td, th");
  449. cells.forEach((cell) => {
  450. cell.style.border = "1px solid black";
  451. cell.style.padding = "5px";
  452. if (cell.tagName === "TH") {
  453. cell.style.backgroundColor = "#f0f0f0";
  454. cell.style.fontWeight = "bold";
  455. }
  456. });
  457. });
  458. templateSection.innerHTML = tempDiv.innerHTML;
  459. contentContainer.appendChild(templateSection);
  460. });
  461. // 构建HTML内容
  462. let contentData = `
  463. <!DOCTYPE html>
  464. <html xmlns:o='urn:schemas-microsoft-com:office:office'
  465. xmlns:w='urn:schemas-microsoft-com:office:word'
  466. xmlns='http://www.w3.org/TR/REC-html40'>
  467. <head>
  468. <meta charset="UTF-8">
  469. <title>${template.title}</title>
  470. <xml>
  471. <w:WordDocument>
  472. <w:View>Print</w:View>
  473. <w:Zoom>100</w:Zoom>
  474. <w:DoNotOptimizeForBrowser/>
  475. </w:WordDocument>
  476. </xml>
  477. <style>
  478. /* 重置样式 */
  479. * {
  480. margin: 0;
  481. padding: 0;
  482. box-sizing: border-box;
  483. }
  484. /* 页面设置 */
  485. @page {
  486. size: A4;
  487. margin: 2cm;
  488. }
  489. /* 基本样式 */
  490. body {
  491. font-family: "Times New Roman", SimSun, serif;
  492. font-size: 12pt;
  493. line-height: 1.5;
  494. }
  495. /* 标题样式 */
  496. h1 { font-size: 24pt; margin: 1em 0; }
  497. h2 { font-size: 18pt; margin: 1em 0; }
  498. h3 { font-size: 14pt; margin: 1em 0; }
  499. h4, h5, h6 { font-size: 12pt; margin: 1em 0; }
  500. /* 表格样式 */
  501. table {
  502. width: 100%;
  503. border-collapse: collapse;
  504. border: 1px solid black;
  505. margin: 1em 0;
  506. mso-table-lspace: 0pt;
  507. mso-table-rspace: 0pt;
  508. mso-border-alt: solid windowtext .5pt;
  509. }
  510. td, th {
  511. border: 1px solid black;
  512. padding: 5px;
  513. mso-border-alt: solid windowtext .5pt;
  514. }
  515. th {
  516. background-color: #f0f0f0;
  517. font-weight: bold;
  518. }
  519. /* Word特定样式 */
  520. .MsoNormal {
  521. margin-bottom: .0001pt;
  522. text-align: justify;
  523. text-justify: inter-ideograph;
  524. }
  525. /* 分页控制 */
  526. .template-textarea {
  527. page-break-after: always;
  528. }
  529. /* 隐藏目录 */
  530. .TOC {
  531. display: none !important;
  532. }
  533. </style>
  534. <!--[if gte mso 9]>
  535. <xml>
  536. <w:WordDocument>
  537. <w:View>Print</w:View>
  538. <w:Zoom>100</w:Zoom>
  539. <w:DoNotOptimizeForBrowser/>
  540. </w:WordDocument>
  541. </xml>
  542. <![endif]-->
  543. </head>
  544. <body>
  545. ${contentContainer.innerHTML}
  546. </body>
  547. </html>`;
  548. // 调用导出接口
  549. const res = await exportDocument({
  550. content: contentData,
  551. title: template.title,
  552. options: {
  553. format: "docx",
  554. encoding: "utf-8",
  555. preserveStyles: true,
  556. },
  557. });
  558. if (res.status !== 200) {
  559. this.$alert(res.errMsg);
  560. return;
  561. }
  562. // 处理文件下载
  563. const link = document.createElement("a");
  564. link.href = res.data.file_path;
  565. link.download = `${template.title}.docx`;
  566. document.body.appendChild(link);
  567. link.click();
  568. document.body.removeChild(link);
  569. this.$message.success("导出成功");
  570. } catch (error) {
  571. console.error("导出文档失败:", error);
  572. this.$alert("导出文档时发生错误,请稍后重试。");
  573. }
  574. },
  575. /* 客户名称 */
  576. getCusomName(phaseId) {
  577. const product = this.customList.find((item) => item.id === phaseId);
  578. return product ? product.name : "未知客户";
  579. },
  580. /* 当前阶段 */
  581. getPhaseName(phaseId) {
  582. const product = this.currentPhaseList.find(
  583. (item) => item.value === phaseId
  584. );
  585. return product ? product.label : "未知阶段";
  586. },
  587. getPhaseType(phaseId) {
  588. const product = this.currentPhaseList.find(
  589. (item) => item.value === phaseId
  590. );
  591. if (!product) return "info";
  592. // 根据不同阶段返回不同的类型
  593. // 这里的判断逻辑需要根据实际的产品阶来调整
  594. switch (product.status) {
  595. case 1:
  596. return "success"; // 已完成
  597. case 2:
  598. return "warning"; // 进行中
  599. case 3:
  600. return "danger"; // 待处理
  601. default:
  602. return "info"; // 其他状态
  603. }
  604. },
  605. /*目前状态 */
  606. getStatusName(statusId) {
  607. const status = this.getstatusList.find((item) => item.value === statusId);
  608. return status ? status.label : "未知状态";
  609. },
  610. getStatusType(statusId) {
  611. // 这里可以根据不同状态返回不同的类型
  612. // 可用的类型有:success/warning/danger/info
  613. const status = this.getstatusList.find((item) => item.value === statusId);
  614. if (!status) return "info";
  615. // 根据状态值返回对应的类型
  616. switch (status.value) {
  617. case 5:
  618. return "success";
  619. case 4:
  620. return "warning";
  621. case 3:
  622. return "danger";
  623. default:
  624. return "info";
  625. }
  626. },
  627. // 格式化完成度显示
  628. formatCompletion(rate) {
  629. if (rate === undefined || rate === null) {
  630. return "0%";
  631. }
  632. return `${rate}%`;
  633. },
  634. // 根据完成度返回对应的CSS类名
  635. getCompletionClass(rate) {
  636. if (!rate) return "completion-low";
  637. if (rate >= 100) return "completion-high";
  638. if (rate >= 50) return "completion-medium";
  639. return "completion-low";
  640. },
  641. handleCommand(command) {
  642. switch (command.type) {
  643. case "template":
  644. this.selTemplate(command.row);
  645. break;
  646. case "enter":
  647. this.enterPro(command.row);
  648. break;
  649. case "edit":
  650. this.btnEdit(command.row);
  651. break;
  652. case "delete":
  653. this.btnDelete(command.row.id);
  654. break;
  655. case "viewTemplates":
  656. this.viewBoundTemplates(command.row);
  657. break;
  658. }
  659. },
  660. // Add new methods
  661. // 查看已绑定模版
  662. viewBoundTemplates(row) {
  663. this.currentData = row;
  664. this.boundTemplateLoading = true;
  665. searchTemplateList({ ids: row.documentIds.join(",") || 0 })
  666. .then((res) => {
  667. if (res.status === 200) {
  668. this.boundTemplateList = res.data;
  669. this.boundTemplatesDialogVisible = true;
  670. }
  671. })
  672. .finally(() => {
  673. this.boundTemplateLoading = false;
  674. });
  675. },
  676. previewTemplate(template) {
  677. /* this.currentTemplateId = template.id; */
  678. let _this = this;
  679. let a = document.createElement("a");
  680. a.href = "#/document/viewCreate?articleId=" + template.id;
  681. a.target = "_blank";
  682. a.click();
  683. /* getTemplateContent(template.id).then((res) => {
  684. if (res.status === 200) {
  685. this.templatePreviewContent = res.data.content;
  686. this.previewDialogVisible = true;
  687. }
  688. }); */
  689. },
  690. replaceTemplate(template) {
  691. this.docAttr = template;
  692. const TemList = JSON.parse(template.data);
  693. /* this.$confirm("确定要替换此模版吗?", "提示", {
  694. confirmButtonText: "确定",
  695. cancelButtonText: "取消",
  696. type: "warning",
  697. }).then(() => { */
  698. this.replaceVisible = true;
  699. this.searchEx(TemList);
  700. /* this.closeTemplateDialog();
  701. this.selTemplate(this.currentData); */
  702. /* }); */
  703. },
  704. // 新增:处理已绑定模版的选择
  705. handleBoundTemplateSelect(selection) {
  706. this.selectedBoundTemplates = selection;
  707. },
  708. // 新增:批量替换方法
  709. async batchReplace() {
  710. if (this.selectedBoundTemplates.length === 0) {
  711. this.$message.warning("请选择需要替换的模版");
  712. return;
  713. }
  714. this.batchReplaceLoading = true;
  715. try {
  716. // 依次处理每个选中的模版
  717. for (const template of this.selectedBoundTemplates) {
  718. this.docAttr = template;
  719. const TemList = JSON.parse(template.data);
  720. await this.searchEx(TemList);
  721. if (this.matchResults.length > 0) {
  722. await this.replaceAll();
  723. }
  724. }
  725. this.$message.success("批量替换完成");
  726. this.boundTemplatesDialogVisible = false;
  727. } catch (error) {
  728. console.error("批量替换失败:", error);
  729. this.$message.error("批量替换过程中发生错误");
  730. } finally {
  731. this.batchReplaceLoading = false;
  732. }
  733. },
  734. searchEx(val) {
  735. return new Promise((resolve) => {
  736. let allMatches = [];
  737. this.TemList = val;
  738. this.TemList.forEach((el) => {
  739. // 创建临时 div 来解析 HTML 内容
  740. const tempDiv = document.createElement("div");
  741. tempDiv.innerHTML = el.content;
  742. const plainText = tempDiv.textContent || tempDiv.innerText;
  743. // 分别匹配中文括号和英文括号
  744. const patterns = [
  745. /【([A-Z]{1,4}[0-9]{0,2})-([A-Z]{1,4}[0-9]{0,2})-([0-9]{2})-([0-9]{2,3})】/g,
  746. /\[([A-Z]{1,4}[0-9]{0,2})-([A-Z]{1,4}[0-9]{0,2})-([0-9]{2})-([0-9]{2,3})\]/g,
  747. ];
  748. patterns.forEach((pattern) => {
  749. let match;
  750. while ((match = pattern.exec(plainText)) !== null) {
  751. const fullMatch = match[0];
  752. const segments = [match[1], match[2], match[3], match[4]];
  753. // 验证每个段落的格式
  754. const isValid = segments.every((segment) => segment.length > 0);
  755. if (isValid) {
  756. allMatches.push(fullMatch);
  757. }
  758. }
  759. });
  760. });
  761. this.matchResults = [...new Set(allMatches)];
  762. // 调试用:打印每个匹配的详细信息
  763. this.matchResults.forEach((match) => {
  764. // 根据括号类型选择不同的切片方式
  765. const content = match.startsWith("【")
  766. ? match.slice(1, -1)
  767. : match.slice(1, -1);
  768. const parts = content.split("-");
  769. });
  770. resolve();
  771. });
  772. },
  773. // 添加新的辅助方法来判断文档类型
  774. getDocumentType(code) {
  775. if (code.includes("-RP-")) return "template_location";
  776. if (code.includes("-IR-")) return "middle_layer_location";
  777. if (code.includes("-WE-")) return "tech_report_location";
  778. return "other_location";
  779. },
  780. // 修改格式化请求参数的方法
  781. formatRequestParams(locations) {
  782. const locationArray = Array.isArray(locations) ? locations : [locations];
  783. // 初始化请求参数对象
  784. const params = {
  785. /* project_id:'', */
  786. template_location: [],
  787. middle_layer_location: [],
  788. tech_report_location: [],
  789. other_location: [],
  790. };
  791. // 根据不同类型分类存储
  792. locationArray.forEach((location) => {
  793. const cleanLocation = location.replace(/[\[\]]/g, "");
  794. const type = this.getDocumentType(cleanLocation);
  795. console.log("params", type);
  796. // 为每种类型创建对应长度的空数组
  797. Object.keys(params).forEach((key) => {
  798. if (type === key) {
  799. params[key].push(cleanLocation);
  800. } else {
  801. params[key].push("");
  802. }
  803. });
  804. });
  805. return params;
  806. },
  807. // 解析内容,处理特殊标记
  808. parseContent(content) {
  809. if (!content) return "";
  810. let parsed = content;
  811. // 处理图片标记
  812. parsed = parsed.replace(
  813. /\[图片:(.*?)\|style=(.*?)\|class=(.*?)\]/g,
  814. (match, src, style, className) => {
  815. return `<img src="${src}" alt="图片" style="${style}" class="${className}">`;
  816. }
  817. );
  818. // 处理表格标记
  819. parsed = parsed.replace(
  820. /\[表格\|style=(.*?)\|class=(.*?)\]\n([\s\S]*?)(?=\[|$)/g,
  821. (match, tableStyle, tableClass, tableContent) => {
  822. const rows = tableContent.trim().split("\n");
  823. let tableHtml = `<table style="${tableStyle}" class="${tableClass}">`;
  824. rows.forEach((row) => {
  825. tableHtml += "<tr>";
  826. const cells = row.split(" | ").map((cell) => {
  827. const [content, ...styleInfo] = cell.split("[style=");
  828. if (styleInfo.length) {
  829. const [style, className] = styleInfo[0]
  830. .slice(0, -1)
  831. .split("|class=");
  832. return `<td style="${style}" class="${className}">${content}</td>`;
  833. }
  834. return `<td>${content}</td>`;
  835. });
  836. tableHtml += cells.join("") + "</tr>";
  837. });
  838. tableHtml += "</table>";
  839. return tableHtml;
  840. }
  841. );
  842. // 处理文本样式
  843. parsed = parsed.replace(
  844. /\[文本:(.*?)\|style=(.*?)\|class=(.*?)\]/g,
  845. (match, content, style, className) => {
  846. return `<span style="${style}" class="${className}">${content}</span>`;
  847. }
  848. );
  849. return parsed;
  850. },
  851. // 检查内容是否包含表格或图片
  852. containsTableOrImage(content) {
  853. return content.includes("<table") || content.includes("<img");
  854. },
  855. // 获取所有文本节点
  856. getTextNodes(node) {
  857. const textNodes = [];
  858. const walk = document.createTreeWalker(
  859. node,
  860. NodeFilter.SHOW_TEXT,
  861. null,
  862. false
  863. );
  864. let currentNode;
  865. while ((currentNode = walk.nextNode())) {
  866. textNodes.push(currentNode);
  867. }
  868. return textNodes;
  869. },
  870. /*更新 */
  871. onUpload() {
  872. let _this = this;
  873. if (_this.TemList.length <= 0) {
  874. _this.$message.error("增加组件");
  875. return;
  876. }
  877. if (_this.docAttr.title == "") {
  878. _this.$message.error("请填写模版标题");
  879. return;
  880. }
  881. _this.docAttr.links = JSON.stringify(_this.docAttr.linkProduct);
  882. _this.docAttr.projects = JSON.stringify(_this.docAttr.linkProject);
  883. _this.docAttr.data = JSON.stringify(_this.TemList);
  884. _this.docAttr.is_template = 1;
  885. _this.docAttr.id = _this.docAttr.id;
  886. _this.docAttr.user_name = _this.docAttr.name;
  887. _this.docAttr.user_id = _this.docAttr.id;
  888. updateDocument(_this.docAttr).then((res) => {
  889. if (res.status != 200) return; //更新文档
  890. _this.docAttr.id = res.data;
  891. _this.$message.success("模版更新成功");
  892. });
  893. },
  894. // 添加统一的内容处理方法
  895. async processAndReplaceContent(content, originalMatch) {
  896. try {
  897. const params = this.formatRequestParams(originalMatch);
  898. params.project_id = this.currentData.id;
  899. const response = await axios.post(
  900. `${process.env.VUE_APP_BASE_API}/project-raw-data/batch-content`,
  901. params,
  902. {
  903. headers: {
  904. "Content-Type": "application/json",
  905. },
  906. }
  907. );
  908. if (response.status === 200 && response.data) {
  909. const replacementContent =
  910. response.data.data.matched_data[0]?.content;
  911. if (replacementContent) {
  912. // 处理富文本内容
  913. this.TemList.forEach((el, index) => {
  914. console.log(el, originalMatch, replacementContent);
  915. const processedContent = this.processTableContent(
  916. el.content,
  917. originalMatch,
  918. replacementContent
  919. );
  920. this.$set(this.TemList[index], "content", processedContent);
  921. });
  922. return true;
  923. }
  924. }
  925. return false;
  926. } catch (error) {
  927. console.error("处理内容失败:", error);
  928. return false;
  929. }
  930. },
  931. // 修改单个替换方法
  932. async replaceItem(item) {
  933. try {
  934. const success = await this.processAndReplaceContent(this.TemList, item);
  935. if (success) {
  936. this.matchResults = this.matchResults.filter(
  937. (match) => match !== item
  938. );
  939. this.onUpload();
  940. this.$message.success("替换成功");
  941. } else {
  942. this.$message.warning("未获取到替换内容");
  943. }
  944. } catch (error) {
  945. console.error("替换失败:", error);
  946. this.$message.error(error.response?.data?.message || "替换失败");
  947. }
  948. },
  949. // 修改批量替换方法
  950. async replaceAll() {
  951. this.batchReplaceLoading = true;
  952. try {
  953. // 收集所有匹配
  954. const allLocations = this.matchResults.map((item) => {
  955. // 清理括号
  956. return item.replace(/[\[\]【】]/g, "");
  957. });
  958. // 格式化所有位置的请求参数
  959. const params = {
  960. template_location: [],
  961. middle_layer_location: [],
  962. tech_report_location: [],
  963. other_location: [],
  964. };
  965. // 遍历所有位置并分类
  966. allLocations.forEach((location) => {
  967. // 为每种类型创建对应长度的数组
  968. const type = this.getDocumentType(location);
  969. Object.keys(params).forEach((key) => {
  970. if (type === key) {
  971. params[key].push(location);
  972. } else {
  973. params[key].push("");
  974. }
  975. });
  976. });
  977. params.project_id = this.currentData.id;
  978. // 发送单个批量请求
  979. const response = await axios.post(
  980. `${process.env.VUE_APP_BASE_API}/project-raw-data/batch-content`,
  981. params,
  982. {
  983. headers: {
  984. "Content-Type": "application/json",
  985. },
  986. }
  987. );
  988. if (response.status === 200 && response.data) {
  989. const replacementContents = response.data.data.matched_data;
  990. if (replacementContents && replacementContents.length > 0) {
  991. // 更新所有内容
  992. this.TemList.forEach((el, index) => {
  993. let updatedContent = el.content;
  994. // 遍历所有替内容和对应的匹配项
  995. this.matchResults.forEach((item, idx) => {
  996. const replacement = replacementContents[idx]?.content;
  997. if (replacement) {
  998. // 处理表格和其他内容
  999. updatedContent = this.processTableContent(
  1000. updatedContent,
  1001. item,
  1002. replacement
  1003. );
  1004. }
  1005. });
  1006. // 更新内容
  1007. this.$set(this.TemList[index], "content", updatedContent);
  1008. });
  1009. // 更新文档
  1010. await this.onUpload();
  1011. this.matchResults = [];
  1012. this.$message.success("批量替换成功");
  1013. this.replaceVisible = false;
  1014. } else {
  1015. this.$message.warning("未获取到替换内容");
  1016. }
  1017. }
  1018. } catch (error) {
  1019. console.error("批量替换失败:", error);
  1020. this.$message.error(error.response?.data?.message || "批量替换失败");
  1021. } finally {
  1022. this.batchReplaceLoading = false;
  1023. }
  1024. },
  1025. // 辅助方法:获取文档类型
  1026. getDocumentType(code) {
  1027. if (code.includes("-RP-")) return "template_location";
  1028. if (code.includes("-IR-")) return "middle_layer_location";
  1029. if (code.includes("-WE-")) return "tech_report_location";
  1030. return "other_location";
  1031. },
  1032. // 修改处理表格内容的方法,添加对多个替换的支持
  1033. processTableContent(content, searchItem, replacement) {
  1034. // 直接使用字符串替换,不处理 DOM
  1035. if (!content || !searchItem) return content;
  1036. try {
  1037. const regex = new RegExp(this.escapeRegExp(searchItem), "g");
  1038. return content.replace(regex, replacement);
  1039. } catch (error) {
  1040. console.error("替换内容时出错:", error);
  1041. return content;
  1042. }
  1043. },
  1044. // 添加样式到 convertTextToHtml 方法
  1045. convertTextToHtml(text) {
  1046. if (!text) return "";
  1047. let html = text;
  1048. // 处理特定格式的表格
  1049. html = html.replace(
  1050. /\[表格\|style=(.*?)\|class=(.*?)\]([\s\S]*?)(?=\[|$)/g,
  1051. (match, tableStyle, tableClass, tableContent) => {
  1052. const rows = tableContent.trim().split("\n");
  1053. let tableHtml = `<table style="${tableStyle}; border-collapse: collapse;" class="${tableClass}">`;
  1054. rows.forEach((row, index) => {
  1055. const [label, value] = row.split("|").map((item) => item.trim());
  1056. if (label) {
  1057. tableHtml += "<tr>";
  1058. if (value) {
  1059. // 如果有值,创建两列的行
  1060. tableHtml += `
  1061. <td style="border: 1px solid #000; padding: 8px;">${label}</td>
  1062. <td style="border: 1px solid #000; padding: 8px;">${value}</td>
  1063. `;
  1064. } else {
  1065. // 如果只有标签,创建单列的行(可能是表头)
  1066. tableHtml += `
  1067. <td style="border: 1px solid #000; padding: 8px; font-weight: ${
  1068. index === 0 ? "bold" : "normal"
  1069. }">${label}</td>
  1070. `;
  1071. }
  1072. tableHtml += "</tr>";
  1073. }
  1074. });
  1075. tableHtml += "</table>";
  1076. return tableHtml;
  1077. }
  1078. );
  1079. // 保持原有的图片和文本处理逻辑不变
  1080. html = html.replace(
  1081. /\[图片:(.*?)\|style=(.*?)\|class=(.*?)\]/g,
  1082. (match, src, style, className) => {
  1083. return `<img src="${src}" alt="图片" style="${style}" class="${className}">`;
  1084. }
  1085. );
  1086. html = html.replace(
  1087. /\[文本:(.*?)\|style=(.*?)\|class=(.*?)\]/g,
  1088. (match, content, style, className) => {
  1089. return `<span style="${style}" class="${className}">${content}</span>`;
  1090. }
  1091. );
  1092. return html;
  1093. },
  1094. // 更新 escapeRegExp 方法以支持两种括号类型
  1095. escapeRegExp(string) {
  1096. const isChinese = string.startsWith("【");
  1097. const content = isChinese
  1098. ? string.replace(/^【|】$/g, "")
  1099. : string.replace(/^\[|\]$/g, "");
  1100. const escaped = content.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&");
  1101. return isChinese ? `\\【${escaped}\\】` : `\\[${escaped}\\]`;
  1102. },
  1103. /* 选择模版 */
  1104. selTemplate(row) {
  1105. this.currentData = row;
  1106. this.selectedTemplates = []; // 重置选择
  1107. this.selectedTemplateIds = new Set(row.documentIds || []); // 初始化已绑定的模版ID
  1108. this.templateQueryForm.page = 1; // 重置���码
  1109. this.loadTemplateList();
  1110. },
  1111. // 处理模版分页变化
  1112. handleTemplatePageChange(page) {
  1113. // 保存当前页的选择状态
  1114. const currentSelection = this.$refs.templateTable.selection;
  1115. if (currentSelection.length > 0) {
  1116. this.selectedPageIds.set(
  1117. this.templateQueryForm.page,
  1118. new Set(currentSelection.map((item) => item.id))
  1119. );
  1120. }
  1121. this.templateQueryForm.page = page;
  1122. this.loadTemplateList();
  1123. },
  1124. // 加载模版列表
  1125. loadTemplateList() {
  1126. this.templateLoading = true;
  1127. searchList(this.templateQueryForm)
  1128. .then((res) => {
  1129. if (res.status === 200) {
  1130. this.templateList = res.data.dataList;
  1131. this.templateTotal = res.data.totalRecord;
  1132. this.templateDialogVisible = true;
  1133. // 恢复选中状态
  1134. this.$nextTick(() => {
  1135. const currentPage = this.templateQueryForm.page;
  1136. const currentPageSelected = this.selectedPageIds.get(currentPage);
  1137. this.templateList.forEach((row) => {
  1138. const shouldSelect =
  1139. this.isAllSelected ||
  1140. (currentPageSelected && currentPageSelected.has(row.id)) ||
  1141. this.selectedTemplateIds.has(row.id) ||
  1142. (this.currentData.documentIds &&
  1143. this.currentData.documentIds.includes(row.id));
  1144. if (shouldSelect) {
  1145. this.$refs.templateTable.toggleRowSelection(row, true);
  1146. }
  1147. });
  1148. });
  1149. }
  1150. })
  1151. .finally(() => {
  1152. this.templateLoading = false;
  1153. });
  1154. },
  1155. handleTemplateSelect(selection) {
  1156. const currentPage = this.templateQueryForm.page;
  1157. const currentPageIds = selection.map((item) => item.id);
  1158. // 更新当前页的选择状态
  1159. if (currentPageIds.length > 0) {
  1160. this.selectedPageIds.set(currentPage, new Set(currentPageIds));
  1161. } else {
  1162. this.selectedPageIds.delete(currentPage);
  1163. }
  1164. // 如果之前是全选状态,现在取消了某些选择
  1165. if (this.isAllSelected && selection.length < this.templateList.length) {
  1166. this.isAllSelected = false;
  1167. // 保留其他页面的选择和已绑定的模版
  1168. const allSelectedIds = new Set(this.currentData.documentIds || []);
  1169. this.selectedPageIds.forEach((pageIds, page) => {
  1170. if (page !== currentPage) {
  1171. pageIds.forEach((id) => allSelectedIds.add(id));
  1172. }
  1173. });
  1174. // 添加当前页选中的ID
  1175. currentPageIds.forEach((id) => allSelectedIds.add(id));
  1176. this.selectedTemplateIds = allSelectedIds;
  1177. } else {
  1178. // 正常选择模式,保留已绑定的模版
  1179. const existingIds = new Set(this.currentData.documentIds || []);
  1180. this.templateList.forEach((item) => {
  1181. if (currentPageIds.includes(item.id)) {
  1182. this.selectedTemplateIds.add(item.id);
  1183. } else if (!existingIds.has(item.id)) {
  1184. // 只删除非已绑定的模版
  1185. this.selectedTemplateIds.delete(item.id);
  1186. }
  1187. });
  1188. }
  1189. },
  1190. // 处理全选按点击
  1191. handleSelectAll(selection) {
  1192. const currentPage = this.templateQueryForm.page;
  1193. if (selection.length > 0) {
  1194. this.$confirm("是否选择所有页的模版?", "提示", {
  1195. confirmButtonText: "全部选择",
  1196. cancelButtonText: "仅选择当前页",
  1197. type: "info",
  1198. })
  1199. .then(() => {
  1200. // 选择所有页
  1201. this.selectAllPages();
  1202. })
  1203. .catch(() => {
  1204. // 仅选择当前页
  1205. this.selectCurrentPage(selection);
  1206. // 更新当前页的选择记录
  1207. this.selectedPageIds.set(
  1208. currentPage,
  1209. new Set(selection.map((item) => item.id))
  1210. );
  1211. });
  1212. } else {
  1213. // 取消全选
  1214. this.isAllSelected = false;
  1215. this.selectedTemplateIds.clear();
  1216. this.selectedPageIds.clear();
  1217. this.$refs.templateTable.clearSelection();
  1218. }
  1219. },
  1220. // 选择所有页
  1221. async selectAllPages() {
  1222. this.isAllSelected = true;
  1223. this.selectedTemplateIds.clear();
  1224. // 获取所有模版ID
  1225. try {
  1226. const allTemplates = await this.getAllTemplates();
  1227. allTemplates.forEach((template) => {
  1228. this.selectedTemplateIds.add(template.id);
  1229. });
  1230. // 选中当前页显示的所有行
  1231. this.templateList.forEach((row) => {
  1232. this.$refs.templateTable.toggleRowSelection(row, true);
  1233. });
  1234. this.$message.success(`已选择全部 ${this.templateTotal} 条数据`);
  1235. } catch (error) {
  1236. console.error("获取所有模版失败:", error);
  1237. this.$message.error("获取所有模版失败");
  1238. }
  1239. },
  1240. // 仅选择当前页
  1241. selectCurrentPage(selection) {
  1242. this.isAllSelected = false;
  1243. // 清除之前的选择
  1244. this.selectedTemplateIds.clear();
  1245. // 添加当前页选中项
  1246. selection.forEach((item) => {
  1247. this.selectedTemplateIds.add(item.id);
  1248. });
  1249. },
  1250. // 获取所有模版数据
  1251. async getAllTemplates() {
  1252. const pageSize = 999; // 使用较大的页码一次性获取
  1253. const params = {
  1254. ...this.templateQueryForm,
  1255. page: 1,
  1256. pageSize,
  1257. };
  1258. try {
  1259. const res = await searchList(params);
  1260. if (res.status === 200) {
  1261. return res.data.dataList;
  1262. }
  1263. return [];
  1264. } catch (error) {
  1265. console.error("获取所有模版失败:", error);
  1266. return [];
  1267. }
  1268. },
  1269. // 确认选择
  1270. confirmTemplateSelection() {
  1271. if (this.selectedTemplateIds.size === 0) {
  1272. this.$message.warning("请至少选择一个模版");
  1273. return;
  1274. }
  1275. // 合并已有的文档ID和新选择的ID
  1276. const allDocIds = new Set([
  1277. ...(this.currentData.documentIds || []),
  1278. ...this.selectedTemplateIds,
  1279. ]);
  1280. relate({
  1281. project_id: this.currentData.id,
  1282. doc_ids: JSON.stringify([...allDocIds]),
  1283. }).then((res) => {
  1284. if (res.status !== 200) return;
  1285. this.$message.success("模版关联成功");
  1286. this.closeTemplateDialog();
  1287. this.search();
  1288. });
  1289. },
  1290. // 关对话框
  1291. closeTemplateDialog() {
  1292. this.templateDialogVisible = false;
  1293. this.selectedTemplateIds.clear();
  1294. this.selectedPageIds.clear();
  1295. this.isAllSelected = false;
  1296. this.$refs.templateTable.clearSelection();
  1297. },
  1298. // 输入项目
  1299. enterPro(row) {
  1300. if (row.documentIds.length == 0) {
  1301. this.$message.warning("请先绑定模版!");
  1302. return;
  1303. }
  1304. // 获取已绑定模版的替换位置信息
  1305. searchTemplateList({ ids: row.documentIds.join(",") || 0 })
  1306. .then(async (res) => {
  1307. if (res.status === 200) {
  1308. const templates = res.data;
  1309. const locations = [];
  1310. // 遍历所有模版,提取替换位置
  1311. templates.forEach((template) => {
  1312. if (template.data) {
  1313. const templateData = JSON.parse(template.data);
  1314. templateData.forEach((item) => {
  1315. const tempDiv = document.createElement("div");
  1316. tempDiv.innerHTML = item.content;
  1317. const plainText = tempDiv.textContent || tempDiv.innerText;
  1318. const patterns = [
  1319. /【([A-Z]{1,4}[0-9]{0,2})-([A-Z]{1,4}[0-9]{0,2})-([0-9]{2})-([0-9]{2,3})】/g,
  1320. /\[([A-Z]{1,4}[0-9]{0,2})-([A-Z]{1,4}[0-9]{0,2})-([0-9]{2})-([0-9]{2,3})\]/g,
  1321. ];
  1322. patterns.forEach((pattern) => {
  1323. let match;
  1324. while ((match = pattern.exec(plainText)) !== null) {
  1325. locations.push({
  1326. template_id: template.id,
  1327. template_title: template.title,
  1328. location: match[0],
  1329. type: this.getDocumentType(match[0]),
  1330. });
  1331. }
  1332. });
  1333. });
  1334. }
  1335. });
  1336. try {
  1337. // 按类型分组位置信息
  1338. const params = {
  1339. template_location: [],
  1340. middle_layer_location: [],
  1341. tech_report_location: [],
  1342. other_location: [],
  1343. };
  1344. // 遍历所有位置并分类
  1345. locations.forEach((loc) => {
  1346. Object.keys(params).forEach((key) => {
  1347. if (loc.type === key) {
  1348. params[key].push(loc.location.replace(/[\[\]【】]/g, "")); // 清除括号
  1349. } else {
  1350. params[key].push("");
  1351. }
  1352. });
  1353. });
  1354. params.project_id = row.id;
  1355. // 获取替换内容
  1356. const response = await axios.post(
  1357. `${process.env.VUE_APP_BASE_API}/project-raw-data/batch-content`,
  1358. params,
  1359. {
  1360. headers: {
  1361. "Content-Type": "application/json",
  1362. },
  1363. }
  1364. );
  1365. if (response.status === 200 && response.data) {
  1366. const replacementContents = response.data.data.matched_data;
  1367. // 将位置信息和对应的替换内容组合
  1368. const enrichedLocations = locations.map((loc, index) => ({
  1369. ...loc,
  1370. content: replacementContents[index]?.content || null,
  1371. template_location:
  1372. replacementContents[index]?.template_location || null,
  1373. tech_report_location:
  1374. replacementContents[index]?.tech_report_location || [],
  1375. }));
  1376. // 对整个 enrichedLocations 进行去重
  1377. const uniqueEnrichedLocations = enrichedLocations.reduce(
  1378. (acc, current) => {
  1379. const isDuplicate = acc.find(
  1380. (item) =>
  1381. item.template_id === current.template_id &&
  1382. item.location === current.location
  1383. );
  1384. if (!isDuplicate) {
  1385. // 如果不是重复项,则添加到结果数组中
  1386. return [...acc, current];
  1387. }
  1388. return acc;
  1389. },
  1390. []
  1391. );
  1392. // 使用去重后的数据
  1393. this.$router.push({
  1394. path: "/project/ProjectInput",
  1395. query: {
  1396. id: row.id,
  1397. projectData: encodeURIComponent(
  1398. JSON.stringify({
  1399. locations: uniqueEnrichedLocations, // 使用去重后的数据
  1400. templates: templates.map((t) => ({
  1401. id: t.id,
  1402. title: t.title,
  1403. })),
  1404. })
  1405. ),
  1406. },
  1407. });
  1408. }
  1409. } catch (error) {
  1410. console.error("获取替换内容失败:", error);
  1411. this.$message.error("获取替换内容失败");
  1412. }
  1413. }
  1414. })
  1415. .catch((error) => {
  1416. console.error("获取模版位置信息失败:", error);
  1417. this.$message.error("获取模版位置信息失败");
  1418. });
  1419. },
  1420. /* 产品分类名称 */
  1421. getCategoryTypeName(categoryId) {
  1422. if (!categoryId) return "未分类";
  1423. const product = this.productList.find((item) => item.id === categoryId);
  1424. return product && product.category ? product.category.name : "未知分类";
  1425. },
  1426. getCategoryName(categoryId) {
  1427. if (!categoryId) return "未知产品";
  1428. const product = this.productList.find((item) => item.id === categoryId);
  1429. return product ? product.name : "未知产品";
  1430. },
  1431. searchData() {
  1432. let _this = this;
  1433. _this.dialogVisible = false;
  1434. _this.search();
  1435. },
  1436. btnDelete(e) {
  1437. let _this = this;
  1438. let par = {
  1439. id: e,
  1440. };
  1441. _this
  1442. .$confirm("您是否确认删除该记录?", "提示", {
  1443. confirmButtonText: "确认",
  1444. cancelButtonText: "取消",
  1445. type: "warning",
  1446. })
  1447. .then((res) => {
  1448. // console.log("AAA")
  1449. deleteProject(par).then((res) => {
  1450. _this.search();
  1451. });
  1452. })
  1453. .catch(() => {});
  1454. },
  1455. //编辑
  1456. btnEdit(e) {
  1457. let _this = this;
  1458. _this.currentDataId = e.id;
  1459. _this.dialogVisible = true;
  1460. },
  1461. onClose(needRefresh) {
  1462. console.log("onClose triggered with needRefresh:", needRefresh); // 添加调试日志
  1463. this.currentDataId = 0;
  1464. this.dialogVisible = false;
  1465. if (needRefresh) {
  1466. this.search();
  1467. }
  1468. },
  1469. handleUpdate() {
  1470. this.search(); // 添加额外的更新处理方法
  1471. },
  1472. //搜索
  1473. async search() {
  1474. try {
  1475. const res = await searchProject(this.queryForm);
  1476. if (!res) return;
  1477. // 获取所有项目的完成度
  1478. const calculatePromises = res.data.dataList.map(async (item) => {
  1479. try {
  1480. const calcRes = await calculate({ id: item.id });
  1481. if (calcRes.status === 200) {
  1482. item.calculate = calcRes.data.completionRate;
  1483. }
  1484. } catch (err) {
  1485. console.error(`获取项目${item.id}完成度失败:`, err);
  1486. item.calculate = 0;
  1487. }
  1488. });
  1489. await Promise.all(calculatePromises);
  1490. this.dataList = res.data.dataList;
  1491. this.recordCount = res.data.totalRecord;
  1492. this.pageTotal = res.data.totalPage;
  1493. } catch (err) {
  1494. console.error("搜索失败:", err);
  1495. this.$message.error("获取项目列表失");
  1496. }
  1497. },
  1498. //修改分页
  1499. ChangePage(e) {
  1500. let _this = this;
  1501. _this.queryForm.page = e;
  1502. _this.search();
  1503. },
  1504. init() {
  1505. searchCategory().then((res) => {
  1506. if (res.status !== 200) return;
  1507. this.categoryList = res.data.dataList;
  1508. });
  1509. /* 获取阶段 */
  1510. getphases().then((res) => {
  1511. this.currentPhaseList = res.data.options;
  1512. });
  1513. /* 获取目前态 */
  1514. getstatus().then((res) => {
  1515. this.getstatusList = res.data.options;
  1516. });
  1517. /* 获取客户列表 */
  1518. searchCustomer({ page: 1, pageSize: 999 }).then((res) => {
  1519. this.customList = res.data.dataList;
  1520. });
  1521. /* 获取产品列表 */
  1522. searchProduct({ page: 1, pageSize: 999 }).then((res) => {
  1523. this.productList = res.data.dataList;
  1524. });
  1525. },
  1526. // 新增:处理导出命令
  1527. async handleExport(command, row) {
  1528. switch (command) {
  1529. case "word":
  1530. await this.exportDocument(row);
  1531. break;
  1532. case "pdf":
  1533. await this.exportToPDF(row);
  1534. break;
  1535. case "pdf-to-word": // 添加新的命令类型
  1536. const pdfResult = await this.exportToPDF_url(row);
  1537. if (pdfResult && pdfResult.file_path) {
  1538. await this.convertPDFToWord(pdfResult.file_path);
  1539. }
  1540. break;
  1541. }
  1542. },
  1543. /* 过去pdf链接 */
  1544. async exportToPDF_url(template) {
  1545. try {
  1546. // 解析模版内容
  1547. const templateData = JSON.parse(template.data || "[]");
  1548. // 创建一个临时的容器来存放内容
  1549. const contentContainer = document.createElement("div");
  1550. contentContainer.className = "content";
  1551. // 处理每个模版段落
  1552. templateData.forEach((item) => {
  1553. const templateSection = document.createElement("div");
  1554. templateSection.className = "template-textarea";
  1555. templateSection.innerHTML = item.content;
  1556. contentContainer.appendChild(templateSection);
  1557. });
  1558. // 处理所有输入框
  1559. contentContainer
  1560. .querySelectorAll('input[type="text"]')
  1561. .forEach((input) => {
  1562. const span = document.createElement("span");
  1563. span.textContent = input.value;
  1564. input.parentNode.replaceChild(span, input);
  1565. });
  1566. // 构建HTML内容
  1567. let contentData = `<!DOCTYPE html>
  1568. <html>
  1569. <head>
  1570. <meta charset="UTF-8">
  1571. <style>
  1572. @page {
  1573. size: A4;
  1574. margin: 2cm;
  1575. }
  1576. body {
  1577. font-family: Arial, sans-serif;
  1578. font-size: 12pt;
  1579. line-height: 1.5;
  1580. }
  1581. table {
  1582. width: 100%;
  1583. border-collapse: collapse;
  1584. }
  1585. td, th {
  1586. border: 1px solid black;
  1587. padding: 5px;
  1588. }
  1589. .template-textarea {
  1590. page-break-after: always;
  1591. }
  1592. .TOC {
  1593. display: none !important;
  1594. }
  1595. </style>
  1596. </head>
  1597. <body>
  1598. ${contentContainer.innerHTML}
  1599. </body>
  1600. </html>`;
  1601. /* // 处理表格样式
  1602. contentData = contentData
  1603. .replaceAll("<table ", `<table style="border-collapse: collapse;" `)
  1604. .replaceAll("<td>", `<td style="border: 1px solid black;">`); */
  1605. // 调用导出接口,指定导出类型为 PDF
  1606. const res = await exportToPDF({
  1607. content: contentData,
  1608. title: template.title,
  1609. });
  1610. if (res.status !== 200) {
  1611. this.$alert(res.errMsg);
  1612. return;
  1613. }
  1614. // 处理文件下载
  1615. return res.data; // 返回结果,包含 file_path
  1616. } catch (error) {
  1617. console.error("导出PDF失败:", error);
  1618. this.$alert("导出PDF时发生错误,请稍后重试。");
  1619. }
  1620. },
  1621. // 添加新的 exportToPDF 方法
  1622. async exportToPDF(template) {
  1623. try {
  1624. // 解析模版内容
  1625. const templateData = JSON.parse(template.data || "[]");
  1626. // 创建一个临时的容器来存放内容
  1627. const contentContainer = document.createElement("div");
  1628. contentContainer.className = "content";
  1629. // 处理每个模版段落
  1630. templateData.forEach((item) => {
  1631. const templateSection = document.createElement("div");
  1632. templateSection.className = "template-textarea";
  1633. templateSection.innerHTML = item.content;
  1634. contentContainer.appendChild(templateSection);
  1635. });
  1636. // 处理所有输入框
  1637. contentContainer
  1638. .querySelectorAll('input[type="text"]')
  1639. .forEach((input) => {
  1640. const span = document.createElement("span");
  1641. span.textContent = input.value;
  1642. input.parentNode.replaceChild(span, input);
  1643. });
  1644. // 构建HTML内容
  1645. let contentData = `<!DOCTYPE html>
  1646. <html>
  1647. <head>
  1648. <meta charset="UTF-8">
  1649. <style>
  1650. @page {
  1651. size: A4;
  1652. margin: 2cm;
  1653. }
  1654. body {
  1655. font-family: Arial, sans-serif;
  1656. font-size: 12pt;
  1657. line-height: 1.5;
  1658. }
  1659. table {
  1660. width: 100%;
  1661. border-collapse: collapse;
  1662. }
  1663. td, th {
  1664. border: 1px solid black;
  1665. padding: 5px;
  1666. }
  1667. .template-textarea {
  1668. page-break-after: always;
  1669. }
  1670. .TOC {
  1671. display: none !important;
  1672. }
  1673. </style>
  1674. </head>
  1675. <body>
  1676. ${contentContainer.innerHTML}
  1677. </body>
  1678. </html>`;
  1679. /* // 处理表格样式
  1680. contentData = contentData
  1681. .replaceAll("<table ", `<table style="border-collapse: collapse;" `)
  1682. .replaceAll("<td>", `<td style="border: 1px solid black;">`); */
  1683. // 调用导出接口,指定导出类型为 PDF
  1684. const res = await exportToPDF({
  1685. content: contentData,
  1686. title: template.title,
  1687. });
  1688. if (res.status !== 200) {
  1689. this.$alert(res.errMsg);
  1690. return;
  1691. }
  1692. // 处理文件下载
  1693. const link = document.createElement("a");
  1694. link.href = res.data.file_path;
  1695. link.download = res.data.file_name;
  1696. document.body.appendChild(link);
  1697. link.click();
  1698. document.body.removeChild(link);
  1699. this.$message.success("导出成功");
  1700. return res.data; // 返回结果,包含 file_path
  1701. } catch (error) {
  1702. console.error("导出PDF失败:", error);
  1703. this.$alert("导出PDF时发生错误,请稍后重试。");
  1704. }
  1705. },
  1706. },
  1707. };
  1708. </script>
  1709. <style lang="scss" scoped>
  1710. @import "./dataList.scss";
  1711. .match-results {
  1712. margin-top: 20px;
  1713. .match-item {
  1714. padding: 10px;
  1715. border-bottom: 1px solid #eee;
  1716. display: flex;
  1717. justify-content: space-between;
  1718. align-items: center;
  1719. .match-content {
  1720. flex: 1;
  1721. display: flex;
  1722. gap: 20px;
  1723. }
  1724. .actions {
  1725. margin-left: 10px;
  1726. }
  1727. }
  1728. }
  1729. // 完成度文字样式
  1730. .completion-high {
  1731. color: #67c23a;
  1732. font-weight: bold;
  1733. }
  1734. .completion-medium {
  1735. color: #e6a23c;
  1736. font-weight: bold;
  1737. }
  1738. .completion-low {
  1739. color: #f56c6c;
  1740. font-weight: bold;
  1741. }
  1742. // 添加PDF导出相关样式
  1743. .content {
  1744. background: white;
  1745. .template-textarea {
  1746. page-break-after: always;
  1747. table {
  1748. width: 100%;
  1749. border-collapse: collapse;
  1750. td,
  1751. th {
  1752. border: 1px solid black;
  1753. padding: 5px;
  1754. }
  1755. }
  1756. }
  1757. }
  1758. </style>