1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724 |
- <template>
- <div class="data-list">
- <el-table
- :data="dataList"
- style="width: 100%"
- header-row-class-name="headerBg"
- empty-text="没有项目信息"
- >
- <el-table-column prop="id" label="项目编号" align="center" width="80" />
- <el-table-column prop="customer_id" label="客户名称" align="left">
- <template #default="scope">
- <span>{{ getCusomName(scope.row.customer_id) }}</span>
- </template>
- </el-table-column>
- <el-table-column prop="name" label="项目名称" align="left" />
- <el-table-column prop="categoryId" label="产品类别" align="left">
- <template #default="scope">
- <span>{{ getCategoryTypeName(scope.row.product_id) }}</span>
- </template>
- </el-table-column>
- <el-table-column prop="productName" label="产品名称" align="left">
- <template #default="scope">
- <span>{{ getCategoryName(scope.row.product_id) }}</span>
- </template>
- </el-table-column>
- <el-table-column prop="service_content" label="服务内容" align="left" />
- <el-table-column prop="start_date" label="项目开始日期" align="center" />
- <el-table-column prop="updateTime" label="最后更新日期" align="center" />
- <el-table-column prop="calculate" label="完成度" align="center">
- <template #default="scope">
- <span :class="getCompletionClass(scope.row.calculate)">
- {{ formatCompletion(scope.row.calculate) }}
- </span>
- </template>
- </el-table-column>
- <el-table-column prop="current_phase" label="当前阶段" align="left">
- <template #default="scope">
- <el-tag :type="getPhaseType(scope.row.current_phase)" effect="plain">
- {{ getPhaseName(scope.row.current_phase) }}
- </el-tag>
- </template>
- </el-table-column>
- <el-table-column prop="project_status" label="目前状态" align="center">
- <template #default="scope">
- <el-tag
- :type="getStatusType(scope.row.project_status)"
- effect="plain"
- >
- {{ getStatusName(scope.row.project_status) }}
- </el-tag>
- </template>
- </el-table-column>
- <el-table-column label="操作" align="center" fixed="right">
- <template #default="scope">
- <el-dropdown :data="scope.row" @command="handleCommand">
- <span class="el-dropdown-link">
- <el-button
- type="text"
- size="small"
- circle
- class="operation-button"
- style="font-size: 15px; margin-left: 10px"
- icon="el-icon-more"
- />
- </span>
- <template #dropdown>
- <el-dropdown-menu>
- <el-dropdown-item
- :command="{ type: 'template', row: scope.row }"
- >选择模版</el-dropdown-item>
- <el-dropdown-item
- :command="{ type: 'enter', row: scope.row }"
- >输入项目</el-dropdown-item>
- <el-dropdown-item
- v-if="
- scope.row.documentIds && scope.row.documentIds.length > 0
- "
- :command="{ type: 'viewTemplates', row: scope.row }"
- >
- 已绑定模版
- </el-dropdown-item>
- <el-dropdown-item :command="{ type: 'edit', row: scope.row }">
- <svg-icon icon-class="edit" />编辑
- </el-dropdown-item>
- <el-dropdown-item :command="{ type: 'delete', row: scope.row }">
- <svg-icon icon-class="delete" />删除
- </el-dropdown-item>
- </el-dropdown-menu>
- </template>
- </el-dropdown>
- </template>
- <!-- <template #default="scope">
- <div class="btns">
- <el-button type="primary" size="small" @click="selTemplate(scope.row)"
- >选择模版</el-button
- >
- <el-button type="primary" size="small" @click="enterPro(scope.row)"
- >输入项目</el-button
- >
- <el-button type="primary" size="small" @click="btnEdit(scope.row)"
- ><svg-icon icon-class="edit" />编辑</el-button
- >
- <el-button
- type="danger"
- size="small"
- @click="btnDelete(scope.row.id)"
- ><svg-icon icon-class="delete" />删除</el-button
- >
- </div>
- </template> -->
- </el-table-column>
- </el-table>
- <div class="page-info">
- <el-pagination
- :current-page="queryForm.page"
- :page-size="queryForm.pageSize"
- :total="recordCount"
- :page-count="pageTotal"
- background
- layout="prev, pager, next"
- @current-change="ChangePage"
- />
- </div>
- <el-dialog
- v-el-drag-dialog
- :visible.sync="dialogVisible"
- append-to-body
- custom-class="prod-verify"
- title="创建项目信息"
- >
- <dataInfo :id="currentDataId" @onClose="onClose" />
- </el-dialog>
- <!-- 选择模版 -->
- <el-dialog
- title="选择模版"
- :visible.sync="templateDialogVisible"
- width="50%"
- append-to-body
- :close-on-click-modal="false"
- top="5%"
- >
- <div class="dialog-header" style="margin-bottom: 20px">
- <!-- 左侧按钮 -->
- <div class="header-left">
- <el-button
- type="primary"
- @click="confirmTemplateSelection"
- >确 定</el-button>
- <el-button @click="closeTemplateDialog">取 消</el-button>
- </div>
- <!-- 右侧选择框 -->
- <div class="header-right">
- <el-select
- v-model="templateQueryForm.document_type"
- placeholder="请选择模板类型"
- style="width: 200px"
- @change="handleDocTypeChange"
- >
- <el-option
- v-for="item in documentTypes"
- :key="item.value"
- :label="item.label"
- :value="item.value"
- />
- </el-select>
- </div>
- </div>
- <!-- 添加文档类型筛选 -->
- <!-- <div class="filter-section" style="margin-bottom: 20px;">
- <el-select
- v-model="templateQueryForm.document_type"
- placeholder="请选择文档类型"
- @change="handleDocTypeChange"
- style="width: 200px;"
- >
- <el-option
- v-for="item in documentTypes"
- :key="item.value"
- :label="item.label"
- :value="item.value"
- />
- </el-select>
- </div> -->
- <el-table
- ref="templateTable"
- v-loading="templateLoading"
- :data="templateList"
- :max-height="600"
- element-loading-text="加载中..."
- element-loading-spinner="el-icon-loading"
- element-loading-background="rgba(255, 255, 255, 0.8)"
- @selection-change="handleSelectionChange"
- @select="handleSelect"
- >
- <el-table-column type="selection" width="55" />
- <el-table-column prop="id" label="ID" width="80" align="center" />
- <el-table-column prop="title" label="模版名称" align="left" />
- <el-table-column prop="create_time" label="创建时间" align="center" />
- </el-table>
- <div class="pagination-container">
- <el-pagination
- :current-page="templateQueryForm.page"
- :page-sizes="pageSizes"
- :page-size="templateQueryForm.pageSize"
- layout="total, sizes, prev, pager, next, jumper"
- :total="templateTotal"
- @size-change="handleSizeChange"
- @current-change="handleTemplatePageChange"
- />
- </div>
- <div slot="footer" class="dialog-footer">
- <!-- <span style="float: left; line-height: 32px;">
- 已选择 {{ getSelectedCount() }} 项
- </span> -->
- </div>
- </el-dialog>
- <el-dialog
- v-el-drag-dialog
- title="已绑定模版"
- :visible.sync="boundTemplatesDialogVisible"
- width="50%"
- append-to-body
- :close-on-click-modal="false"
- >
- <el-table
- ref="boundTemplateTable"
- :data="boundTemplateList"
- style="width: 100%"
- @selection-change="handleBoundTemplateSelect"
- >
- <el-table-column type="selection" width="55" />
- <el-table-column prop="id" label="ID" width="80" align="center" />
- <el-table-column prop="title" label="模版名称" align="left" />
- <el-table-column prop="createTime" label="绑定时间" align="center" />
- <el-table-column label="操作" align="center" width="320">
- <template #default="scope">
- <el-button
- type="primary"
- @click="previewTemplate(scope.row)"
- >预览</el-button>
- <!-- <el-button
- type="primary"
- @click="replaceTemplate(scope.row)"
- >替换</el-button> -->
- <el-dropdown
- style="margin-left: 10px"
- trigger="click"
- @command="(command) => handleExport(command, scope.row)"
- >
- <el-button type="success">
- 导出文档<i class="el-icon-arrow-down el-icon--right" />
- </el-button>
- <template #dropdown>
- <el-dropdown-menu>
- <el-dropdown-item command="word">Word格式</el-dropdown-item>
- <el-dropdown-item command="pdf">PDF格式</el-dropdown-item>
- </el-dropdown-menu> </template><!-- word -->
- <!-- <el-dropdown-item command="pdf-to-word"
- >Word</el-dropdown-item
- > -->
- </el-dropdown>
- </template>
- </el-table-column>
- </el-table>
- <div class="dialog-footer" style="margin-top: 20px; text-align: right">
- <el-button
- type="primary"
- :loading="batchReplaceLoading"
- :disabled="selectedBoundTemplates.length === 0"
- @click="batchReplace"
- >
- 批量替换
- </el-button>
- </div>
- </el-dialog>
- <el-dialog
- title=""
- :visible.sync="replaceVisible"
- width="50%"
- append-to-body
- >
- <!-- 显示匹配 -->
- <div v-if="matchResults.length" class="match-results">
- <div
- v-for="(item, index) in matchResults"
- :key="index"
- class="match-item"
- >
- <div class="match-content">
- <span>原始值: {{ item }}</span>
- <!-- <span>替换后: {{ item }}</span> -->
- </div>
- <div class="actions">
- <el-button size="small" @click="replaceItem(item)">替换</el-button>
- </div>
- </div>
- <div style="display: flex; justify-content: flex-end; margin-top: 20px">
- <el-button type="primary" @click="replaceAll">批量替换</el-button>
- </div>
- </div>
- </el-dialog>
- <!-- 添加选择文档弹窗 -->
- <el-dialog
- title="选择文档版本"
- :visible.sync="documentSelectVisible"
- width="50%"
- append-to-body
- :close-on-click-modal="false"
- >
- <el-table
- v-loading="documentVersionsLoading"
- :data="documentVersions"
- style="width: 100%"
- >
- <el-table-column prop="id" label="ID" width="80" align="center" />
- <el-table-column prop="title" label="文档标题" align="left" />
- <el-table-column prop="createTime" label="创建时间" align="center" />
- <el-table-column label="操作" align="center" width="250">
- <template #default="scope">
- <el-button
- type="primary"
- size="small"
- style="margin-right: 10px"
- @click="handleDocPreview(scope.row)"
- >
- 预览
- </el-button>
- <el-dropdown
- trigger="click"
- @command="(command) => handleDocExport(command, scope.row)"
- >
- <el-button type="success" size="small">
- 导出<i class="el-icon-arrow-down el-icon--right" />
- </el-button>
- <template #dropdown>
- <el-dropdown-menu>
- <el-dropdown-item command="word">Word格式</el-dropdown-item>
- <el-dropdown-item command="pdf">PDF格式</el-dropdown-item>
- </el-dropdown-menu>
- </template>
- </el-dropdown>
- </template>
- </el-table-column>
- </el-table>
- </el-dialog>
- </div>
- </template>
- <script>
- import {
- searchProject,
- deleteProject,
- searchCategory,
- relate,
- searchTemplateList,
- searchTemplateListNoData,
- getstatus,
- calculate,
- getphases
- } from '@/api/project'
- import {
- searchList,
- exportToPDF,
- htmlWordNew,
- createDocument,
- searchlistDoc,
- getDocumentInfo,
- updateDocument,
- searchlistDocTemplate
- } from '@/api/document'
- import { searchCustomer } from '@/api/customer'
- import { searchProduct } from '@/api/product'
- /* import {searchTemplate} from "@/api/template" */
- import dataInfo from './dataInfo.vue'
- import elDragDialog from '@/directive/el-drag-dialog'
- import axios from 'axios'
- import htmlDocx from 'html-docx-js/dist/html-docx' // 需要先安装: npm install html-docx-js
- import { saveAs } from 'file-saver' // 需要先安装: npm install file-saver
- export default {
- components: {
- dataInfo
- },
- directives: { elDragDialog },
- props: {
- queryForm: {
- type: Object,
- default: () => {
- return {
- page: 1,
- pageSize: 10,
- name: '',
- category_id: '',
- status: ''
- }
- }
- }
- },
- data() {
- return {
- templateQueryForm: {
- page: 1,
- pageSize: 10, // 默认每页显示10条
- document_type: '' // 添加文档类型字段
- },
- pageSizes: [10, 20, 50, 100], // 每页显示条数选项
- templateTotal: 0, // 模版总数
- dialogVisible: false, // 控制产品审核
- currentDataId: 0,
- recordCount: 0,
- pageTotal: 1,
- dataList: [],
- currentData: {},
- categoryList: [],
- templateDialogVisible: false,
- templateList: [],
- selectedTemplates: [], // 改为数组以支持多选
- boundTemplatesDialogVisible: false,
- previewDialogVisible: false,
- boundTemplateList: [],
- templatePreviewContent: '',
- currentTemplateId: null,
- replaceVisible: false,
- loading: false, // 增加 loading 状态
- searchResult: {},
- matchResults: [], // 存储匹配到的结果
- TemList: [],
- docAttr: {},
- templateLoading: false, // 添加模版加载状态
- boundTemplateLoading: false, // 添加已绑定模版加载状态
- selectedBoundTemplates: [], // 新增:已选择的绑定模版
- batchReplaceLoading: false, // 新增:批量替换loading状态
- getstatusList: [], // 状态列表
- customList: [], // 客户列表
- productList: [], // 产品列表
- currentPhaseList: [], // 阶段列表
- selectedTemplateIds: new Set(), // 存储所有选中的模板ID
- selectedPageIds: new Map(), // 存储每页的选择状态
- isAllSelected: false, // 是否全选
- selectionCache: new Map(), // 添加选择缓存
- // 添加文档类型选项
- documentTypes: [
- { label: '全部', value: '' },
- { label: '临床', value: 'Clinical' },
- { label: '非临床', value: 'NonClinical' }
- ],
- documentSelectVisible: false, // 文档选择弹窗显示状态
- documentVersions: [], // 文档版本列表
- documentVersionsLoading: false, // 加载状态
- currentTemplate: null // 当前选中的模板
- }
- },
- computed: {
- tableColumns() {
- return [
- // ... 其他列配置 ...
- {
- prop: 'calculate',
- label: '完成度',
- width: '100',
- render: (h, { row }) => {
- // 使用过渡效果显示完成度
- return h(
- 'el-tooltip',
- {
- props: {
- content: '加载中...',
- disabled: row.calculate !== 0
- }
- },
- [
- h(
- 'span',
- {
- class: [
- this.getCompletionClass(row.calculate),
- 'completion-text'
- ]
- },
- [
- row.calculate === 0
- ? h('i', { class: 'el-icon-loading' })
- : this.formatCompletion(row.calculate)
- ]
- )
- ]
- )
- }
- }
- ]
- }
- },
- watch: {
- queryForm: {
- handler: function(val) {
- this.search()
- },
- // immediate: true,//立即执行
- deep: true
- }
- },
- mounted() {
- this.search()
- this.init()
- },
- methods: {
- /* 使用html-docx-js方法替换 */
- // 预处理内容,处理分页和表格
- preprocessContent(content) {
- if (!content) return ''
- const tempDiv = document.createElement('div')
- tempDiv.innerHTML = content
- // 处理分页符
- tempDiv
- .querySelectorAll('div[style*="page-break-after"]')
- .forEach((pageBreak) => {
- const newPageBreak = document.createElement('br')
- newPageBreak.style.pageBreakBefore = 'always'
- newPageBreak.style.clear = 'both'
- newPageBreak.className = 'WordSection'
- pageBreak.parentNode.replaceChild(newPageBreak, pageBreak)
- })
- // 处理图片
- tempDiv.querySelectorAll('img').forEach((img) => {
- // 保持原有尺寸或设置默认尺寸
- const width = img.getAttribute('width') || img.style.width || '595px'
- const height = img.getAttribute('height') || img.style.height || 'auto'
- img.style.width = width
- img.style.height = height
- })
- // 处理表格
- tempDiv.querySelectorAll('table').forEach((table) => {
- if (this.isTableWithBorder(table)) {
- table.setAttribute('data-code', 'active')
- } else {
- table.setAttribute('data-code', 'noactive')
- }
- })
- return tempDiv.innerHTML
- },
- // 更新导出方法
- async exportDocumentWithHtmlDocx(template) {
- const res = await searchlistDocTemplate({
- project_id: this.currentData.id,
- id: template.id
- })
- if(res.status !== 200) return
- let templateData
- try {
- if(res.data.documents.length !== 0) {
- templateData = JSON.parse(res.data.documents[0].data || '[]')
- } else {
- const template_Data = await getDocumentInfo({id:template.id})
- if(template_Data.status !== 200) return
- templateData = JSON.parse(template_Data.data.data || '[]')
- }
- const contentContainer = document.createElement('div')
- contentContainer.className = 'content'
- // 处理每个模版段落
- templateData.forEach((item) => {
- const templateSection = document.createElement('div')
- templateSection.className = 'template-textarea'
- templateSection.innerHTML = this.preprocessContent(item.content)
- contentContainer.appendChild(templateSection)
- })
- let contentData = `
- <!DOCTYPE html>
- <html xmlns:v="urn:schemas-microsoft-com:vml"
- xmlns:o="urn:schemas-microsoft-com:office:office"
- xmlns:w="urn:schemas-microsoft-com:office:word">
- <head>
- <meta charset="UTF-8">
- <xml>
- <w:WordDocument>
- <w:View>Print</w:View>
- <w:Zoom>100</w:Zoom>
- <w:DoNotOptimizeForBrowser/>
- <w:SpaceForUL/>
- <w:BrowserLevel>MicrosoftInternetExplorer4</w:BrowserLevel>
- </w:WordDocument>
- </xml>
- <style>
- /* 基础样式 */
- body {
- font-family: "Times New Roman", SimSun;
- line-height: 1.0; /* 单倍行距 */
- }
-
- /* 段落样式 */
- p {
- margin-top: 0.5em; /* 段前0.5行 */
- margin-bottom: 0.5em; /* 段后0.5行 */
- line-height: 1.0 !important; /* 强制单倍行距 */
- mso-line-height-rule: exactly; /* Word特定的行距规则 */
- }
-
- /* 保持表格内容行距正常 */
- table p {
- margin: 0;
- line-height: inherit;
- }
-
- /* 列表样式 */
- ul, ol {
- margin-top: 0.5em;
- margin-bottom: 0.5em;
- line-height: 1.0;
- }
-
- li {
- line-height: 1.0;
- margin: 0;
- }
-
- /* 保持原有表格样式 */
- table {
- width: 100%;
- border-collapse: collapse !important;
- margin: 8px 0;
- }
-
- /* 有边框表格样式 */
- table[data-code="active"] {
- border: 1px solid black !important;
- border-collapse: collapse !important;
- }
-
- table[data-code="active"] td,
- table[data-code="active"] th {
- border: 1px solid black !important;
- padding: 6px !important;
- mso-border-alt: solid black .5pt !important;
- mso-border-insideh: .5pt solid black !important;
- mso-border-insidev: .5pt solid black !important;
- }
-
- /* 无边框表格样式 */
- table[data-code="noactive"] {
- border: none !important;
- border-collapse: collapse !important;
- }
-
- table[data-code="noactive"] td,
- table[data-code="noactive"] th {
- border: none !important;
- padding: 6px !important;
- }
-
- /* Word特定的表格边框样式 */
- .MsoNormalTable {
- mso-style-name: "普通表格";
- mso-tstyle-rowband-size: 0;
- mso-tstyle-colband-size: 0;
- mso-padding-alt: 0cm 5.4pt 0cm 5.4pt;
- }
- </style>
- </head>
- <body>
- ${this.processContentForWord(contentContainer.innerHTML)}
- </body>
- </html>
- `
- // 添加Word特定的分页处理
- contentData = contentData.replace(
- /<br class="WordSection"[^>]*>/g,
- '<br clear="all" style="page-break-before:always; mso-break-type:section-break">'
- )
- const converted = htmlDocx.asBlob(contentData, {
- orientation: 'portrait',
- margins: {
- top: '2.54cm',
- right: '2.54cm',
- bottom: '2.54cm',
- left: '2.54cm'
- },
- // 添加Word特定的文档属性
- docProperties: {
- 'w:sectPr': {
- 'w:type': { _attr: { 'w:val': 'nextPage' }}
- }
- }
- })
- saveAs(converted, `${template.title}.docx`)
- } catch (error) {
- console.error('导出Word文档失败:', error)
- throw new Error('导出文档时发生错误,请稍后重试')
- }
- },
- /* 处理图片 */
- htmlProcessImages(content) {
- const tempDiv = document.createElement('div')
- tempDiv.innerHTML = content
- // 处理所有图片
- tempDiv.querySelectorAll('img').forEach((img) => {
- // 创建图片容器
- const container = document.createElement('div')
- container.className = 'image-container'
- // 获取所有可能的尺寸来源
- let width =
- img.getAttribute('width') || img.style.width || img.width || null
- let height =
- img.getAttribute('height') || img.style.height || img.height || null
- // 移除单位并转换为数字
- if (typeof width === 'string') {
- width = parseInt(width.replace(/[^0-9]/g, ''))
- }
- if (typeof height === 'string') {
- height = parseInt(height.replace(/[^0-9]/g, ''))
- }
- // 如果没有尺寸,创建一个临时图片来获取实际尺寸
- if (!width || !height) {
- const tempImg = new Image()
- tempImg.src = img.src
- width = tempImg.width || 595 // 默认宽度
- height = tempImg.height || Math.round(width * 0.75) // 默认高度比例
- }
- // 确保数值有效
- width = Math.max(1, parseInt(width))
- height = Math.max(1, parseInt(height))
- // 处理尺寸
- const maxWidth = 595 // A4纸宽度(像素,减去边距)
- if (width > maxWidth) {
- // 等比例缩放
- const aspectRatio = width / height
- const newHeight = Math.round(maxWidth / aspectRatio)
- width = maxWidth
- height = newHeight
- }
- // 同时设置style和属性
- img.style.width = `${width}px`
- img.style.height = `${height}px`
- img.setAttribute('width', width)
- img.setAttribute('height', height)
- // 将图片包装在容器中
- img.parentNode.insertBefore(container, img)
- container.appendChild(img)
- })
- return tempDiv.innerHTML
- },
- // 预处理表格
- preprocessTables(content) {
- const tempDiv = document.createElement('div')
- tempDiv.innerHTML = content
- // 处理所有表格
- tempDiv.querySelectorAll('table').forEach((table, index) => {
- if (this.isTableWithBorder(table)) {
- // 处理有边框表格
- this.processTableWithBorder(table, index)
- } else {
- // 处理无边框表格
- this.processTableNoBorder(table, index)
- }
- })
- return tempDiv.innerHTML
- },
- // 处理有边框表格
- processTableWithBorder(table, index) {
- // 创建表格容器
- const container = document.createElement('div')
- container.className = 'table-container'
- // 获取表头行
- const headerRow = table.querySelector('tr')
- const hasHeader = headerRow && headerRow.querySelector('th')
- // 复制原始表格并添加必要的类和属性
- const clonedTable = table.cloneNode(true)
- clonedTable.className = 'table-with-border continuous-table'
- clonedTable.setAttribute('data-table-id', `table-${index}`)
- if (hasHeader) {
- // 保存表头信息
- const headerHTML = headerRow.outerHTML
- clonedTable.setAttribute('data-header', headerHTML)
- clonedTable.setAttribute('data-repeat-header', 'true')
- }
- // 处理表格的每一行
- clonedTable.querySelectorAll('tr').forEach((row, rowIndex) => {
- row.setAttribute('data-row-index', rowIndex)
- row.style.pageBreakInside = 'avoid'
- })
- // 将处理后的表格放入容器
- container.appendChild(clonedTable)
- table.parentNode.replaceChild(container, table)
- },
- // 处理无边框表格
- processTableNoBorder(table, index) {
- // 创建表格容器
- const container = document.createElement('div')
- container.className = 'table-container-no-border'
- // 复制原始表格并添加必要的类和属性
- const clonedTable = table.cloneNode(true)
- clonedTable.className = 'table-no-border'
- clonedTable.setAttribute('data-table-id', `table-no-border-${index}`)
- // 处理表格的每一行,保持原有样式
- clonedTable.querySelectorAll('tr').forEach((row) => {
- const originalRowStyles = row.getAttribute('style') || ''
- row.style.cssText = `
- page-break-inside: avoid;
- ${originalRowStyles}
- `
- // 处理单元格,保持原有样式
- row.querySelectorAll('td, th').forEach((cell) => {
- const originalStyles = cell.getAttribute('style') || ''
- cell.style.cssText = `
- border: none !important;
- padding: 6px;
- word-wrap: break-word;
- overflow: hidden;
- ${originalStyles}
- `
- })
- })
- // 将处理后的表格放入容器
- container.appendChild(clonedTable)
- table.parentNode.replaceChild(container, table)
- },
- // 处理表格内容
- processContentForWord(content) {
- const tempDiv = document.createElement('div')
- tempDiv.innerHTML = content
- // 处理所有表格
- tempDiv.querySelectorAll('table').forEach((table) => {
- // 检查表格是否有边框
- const hasBorder = this.detectTableBorder(table)
- if (hasBorder) {
- // 为有边框的表格添加必要的属性和类
- table.setAttribute('data-code', 'active')
- table.setAttribute('class', 'MsoNormalTable')
- table.setAttribute('border', '1')
- table.style.borderCollapse = 'collapse'
- // 处理所有单元格
- table.querySelectorAll('td, th').forEach((cell) => {
- // 保存原有样式
- const originalStyles = cell.getAttribute('style') || ''
- // 添加Word特定的边框样式
- cell.style.cssText = `
- border: 1px solid black !important;
- mso-border-alt: solid black .5pt !important;
- padding: 6px !important;
- ${originalStyles}
- `
- })
- } else {
- // 处理无边框表格
- table.setAttribute('data-code', 'noactive')
- table.removeAttribute('border')
- table.style.border = 'none'
- // 移除所有单元格边框
- table.querySelectorAll('td, th').forEach((cell) => {
- cell.style.border = 'none'
- cell.style.padding = '6px'
- })
- }
- })
- return tempDiv.innerHTML
- },
- // 添加检测表格边框的方法
- detectTableBorder(table) {
- // 1. 检查表格本身的边框属性
- if (
- table.getAttribute('border') ||
- /border(?!-collapse)/.test(table.style.cssText) ||
- table.classList.contains('bordered') ||
- table.hasAttribute('rules')
- ) {
- return true
- }
- // 2. 检查计算样式
- const computedStyle = window.getComputedStyle(table)
- if (
- computedStyle.border !== 'none' ||
- computedStyle.borderCollapse === 'collapse'
- ) {
- return true
- }
- // 3. 检查第一个单元格的边框
- const firstCell = table.querySelector('td, th')
- if (firstCell) {
- const cellStyle = window.getComputedStyle(firstCell)
- return (
- cellStyle.border !== 'none' ||
- cellStyle.borderTop !== 'none' ||
- cellStyle.borderBottom !== 'none' ||
- cellStyle.borderLeft !== 'none' ||
- cellStyle.borderRight !== 'none'
- )
- }
- return false
- },
- // 添加处理分的辅助方法
- addPageBreaks(content) {
- // 查找需要分页的位置(可以根据实际需求调整)
- const pageBreakMarkers = ['<h1', '<h2', '<!-- pagebreak -->']
- pageBreakMarkers.forEach((marker) => {
- content = content.replace(
- new RegExp(marker, 'g'),
- `<div class="page-break"></div>${marker}`
- )
- })
- return content
- },
- // 处理表格边框的辅助方法
- processTableBorders(table) {
- const isInfoTable = this.isInformationTable(table)
- if (isInfoTable) {
- table.classList.add('info-table')
- } else {
- // 检查表格是否需要边框
- const needsBorder = this.checkTableNeedsBorder(table)
- if (needsBorder) {
- table.style.border = '1px solid black'
- table.querySelectorAll('td, th').forEach((cell) => {
- cell.style.border = '1px solid black'
- })
- }
- }
- },
- // 检查表格是否需要边框
- checkTableNeedsBorder(table) {
- // 检查表格第一行是否包含特定标记或样式
- const firstRow = table.querySelector('tr')
- if (!firstRow) return false
- // 可以根据实际需求添加更多的判断条件
- return (
- firstRow.querySelector('td[style*="border"]') !== null ||
- table.getAttribute('border') === '1' ||
- table.classList.contains('bordered')
- )
- },
- // 添加内容处方法
- processContentForWord(content) {
- const tempDiv = document.createElement('div')
- tempDiv.innerHTML = content
- // 添加表格分页标识
- let tableCounter = 0
- // 处理表格
- tempDiv.querySelectorAll('table').forEach((table) => {
- tableCounter++
- const tableId = `table_${tableCounter}`
- table.setAttribute('data-table-id', tableId)
- // 判断表格类型
- const isInfoTable = this.isInformationTable(table)
- const isContinuousTable = this.isContinuousTable(table)
- // 基本表格样式
- table.style.width = '100%'
- table.style.borderCollapse = 'collapse'
- table.style.tableLayout = 'fixed'
- if (isInfoTable) {
- // 信息表格处理逻辑保持不变
- table.classList.add('info-table')
- this.processInfoTable(table)
- } else if (isContinuousTable) {
- // 处理跨页连续表格
- table.classList.add('continuous-table')
- this.processContinuousTable(table, tableCounter)
- } else {
- // 普通表格处理逻辑保持不变
- this.processNormalTable(table)
- }
- })
- // 添加表格连续性样式
- const styleElement = document.createElement('style')
- styleElement.textContent = this.generateTableStyles(tableCounter)
- tempDiv.insertBefore(styleElement, tempDiv.firstChild)
- return tempDiv.innerHTML
- },
- // 判断是否为连续表格
- isContinuousTable(table) {
- // 检查表格是否有分页标记
- const prevElement = table.previousElementSibling
- return prevElement && prevElement.classList.contains('page-break')
- },
- // 处理连续表格
- processContinuousTable(table, tableCounter) {
- // 设置表格基本样式
- table.style.borderCollapse = 'collapse'
- table.style.border = '1px solid black'
- // 处理所有单元格
- table.querySelectorAll('tr').forEach((row, rowIndex) => {
- row.style.borderTop = '1px solid black'
- row.style.borderBottom = '1px solid black'
- row.querySelectorAll('td, th').forEach((cell) => {
- const originalStyles = cell.getAttribute('style') || ''
- cell.style.cssText = `
- border: 1px solid black !important;
- padding: 6px;
- word-wrap: break-word;
- overflow: hidden;
- ${originalStyles}
- `
- })
- })
- },
- // 生成表格样式
- generateTableStyles() {
- return `
- /* 原有表样式保持不变 */
- .table-with-border {
- /* ... */
- }
-
- .table-no-border {
- /* ... */
- }
-
- /* 确保表格不会被分页符影响 */
- table {
- -webkit-column-break-inside: avoid;
- page-break-inside: avoid;
- break-inside: avoid;
- }
- `
- },
- // 添加判断表格类型的方法
- isInformationTable(table) {
- // 获取表格第一行第一个单元格的文本
- const firstCell = table.querySelector('td')
- if (!firstCell) return false
- // 定义信息表格的特征文本数组
- const infoTableHeaders = [
- 'Drafted by',
- 'Reviewed by',
- 'Approved by',
- 'File number',
- 'Revision',
- 'Effective'
- ]
- // 检查第一个单元格的文本是否匹配特征文本
- return infoTableHeaders.some(
- (header) =>
- firstCell.textContent.trim().toLowerCase() === header.toLowerCase()
- )
- },
- // 添加处理普通表格的方法
- processNormalTable(table) {
- // 处理行和单元格
- table.querySelectorAll('tr').forEach((row, rowIndex) => {
- // 保持原始行高
- const height = row.style.height || row.getAttribute('height')
- if (height) {
- row.style.height = height
- }
- // 处理单元格
- row.querySelectorAll('td, th').forEach((cell, cellIndex) => {
- // 获取原始样式
- const originalStyles = cell.getAttribute('style') || ''
- // 设置单元格样式
- cell.style.cssText = `
- padding: 6px;
- word-wrap: break-word;
- overflow: hidden;
- ${originalStyles}
- `
- // 保持原始宽度
- if (rowIndex === 0) {
- const width = cell.style.width || cell.getAttribute('width')
- if (width) {
- cell.style.width = width
- // 为该列的所有单元格设置相同的宽度
- table
- .querySelectorAll(
- `tr td:nth-child(${cellIndex + 1}), tr th:nth-child(${
- cellIndex + 1
- })`
- )
- .forEach((td) => {
- td.style.width = width
- })
- }
- }
- // 处理合并单元格
- if (cell.hasAttribute('rowspan')) {
- cell.style.verticalAlign = 'middle'
- }
- })
- })
- },
- // 添加处理信息表格的方法
- processInfoTable(table) {
- table.querySelectorAll('tr').forEach((row) => {
- row.querySelectorAll('td').forEach((cell, index) => {
- // 第一列(标签列)样式
- if (index === 0) {
- cell.style.cssText = `
- border: 1px solid black;
- padding: 6px;
- width: 150px;
- font-weight: normal;
- text-align: left;
- background-color: white;
- `
- }
- // 第二列(内容列)样式
- else {
- cell.style.cssText = `
- border: 1px solid black;
- padding: 6px;
- text-align: left;
- `
- }
- // 保持原始对齐方式
- const originalAlign = cell.getAttribute('align')
- if (originalAlign) {
- cell.style.textAlign = originalAlign
- }
- })
- })
- },
- // 判断表格类型的方法
- isTableWithBorder(table) {
- // 检查表格是否有边框属性
- if (table.getAttribute('border') === '1') return true
- // 检查表格是否有边框样式
- if (table.style.border || table.classList.contains('bordered')) { return true }
- // 检查第一个单格是否有边框
- const firstCell = table.querySelector('td, th')
- if (firstCell && firstCell.style.border) return true
- // 检查是否为信息类表格
- const firstCellText = firstCell?.textContent?.trim().toLowerCase() || ''
- const infoHeaders = [
- 'drafted by',
- 'reviewed by',
- 'approved by',
- 'file number',
- 'revision',
- 'effective'
- ]
- return infoHeaders.some((header) =>
- firstCellText.includes(header.toLowerCase())
- )
- },
- // 处理表格的主方法
- processContentForWord(content) {
- const tempDiv = document.createElement('div')
- tempDiv.innerHTML = content
- // 添加表格分页标识
- let tableCounter = 0
- // 处理表格
- tempDiv.querySelectorAll('table').forEach((table) => {
- tableCounter++
- const tableId = `table_${tableCounter}`
- table.setAttribute('data-table-id', tableId)
- // 判断表格类型
- const isInfoTable = this.isInformationTable(table)
- const isContinuousTable = this.isContinuousTable(table)
- const hasTableBorder = this.isTableWithBorder(table)
- // 基本表格样式
- table.style.width = '100%'
- table.style.borderCollapse = 'collapse'
- table.style.tableLayout = 'fixed'
- if (isInfoTable) {
- // 信息表格处理逻辑
- table.classList.add('info-table')
- this.processInfoTable(table)
- } else if (isContinuousTable && hasTableBorder) {
- // 处理有边框的跨页连续表格
- table.classList.add('continuous-table-with-border')
- this.processContinuousTableWithBorder(table, tableCounter)
- } else if (isContinuousTable) {
- // 处理无边框的跨页连续表格
- table.classList.add('continuous-table-no-border')
- this.processContinuousTableNoBorder(table, tableCounter)
- } else if (hasTableBorder) {
- // 处理有边框的普通表格
- table.classList.add('normal-table-with-border')
- this.processNormalTableWithBorder(table)
- } else {
- // 处理无边框的普通表格
- table.classList.add('normal-table-no-border')
- this.processNormalTableNoBorder(table)
- }
- })
- // 添加表格样式
- const styleElement = document.createElement('style')
- styleElement.textContent = this.generateTableStyles(tableCounter)
- tempDiv.insertBefore(styleElement, tempDiv.firstChild)
- return tempDiv.innerHTML
- },
- // 处理有边框的普通表格
- processNormalTableWithBorder(table) {
- table.style.border = '1px solid black'
- table.querySelectorAll('tr').forEach((row, rowIndex) => {
- // 保持原始行高
- const height = row.style.height || row.getAttribute('height')
- if (height) {
- row.style.height = height
- }
- row.querySelectorAll('td, th').forEach((cell, cellIndex) => {
- const originalStyles = cell.getAttribute('style') || ''
- cell.style.cssText = `
- border: 1px solid black;
- padding: 6px;
- word-wrap: break-word;
- overflow: hidden;
- ${originalStyles}
- `
- // 保持原始宽度
- if (rowIndex === 0) {
- const width = cell.style.width || cell.getAttribute('width')
- if (width) {
- this.setColumnWidth(table, cellIndex, width)
- }
- }
- })
- })
- },
- // 处理无边框的普通表格
- processNormalTableNoBorder(table) {
- table.querySelectorAll('tr').forEach((row, rowIndex) => {
- // 保持原始行高
- const height = row.style.height || row.getAttribute('height')
- if (height) {
- row.style.height = height
- }
- row.querySelectorAll('td, th').forEach((cell, cellIndex) => {
- const originalStyles = cell.getAttribute('style') || ''
- cell.style.cssText = `
- padding: 6px;
- word-wrap: break-word;
- overflow: hidden;
- ${originalStyles}
- `
- // 保持原始宽度
- if (rowIndex === 0) {
- const width = cell.style.width || cell.getAttribute('width')
- if (width) {
- this.setColumnWidth(table, cellIndex, width)
- }
- }
- })
- })
- },
- // 处理有边框的连续表格
- processContinuousTableWithBorder(table, tableCounter) {
- // 设置表格基本样式
- table.style.borderCollapse = 'collapse'
- table.style.border = '1px solid black'
- // 为表格添加唯一标识
- const tableId = `bordered-table-${tableCounter}`
- table.id = tableId
- // 处理所有行和单元格
- table.querySelectorAll('tr').forEach((row, rowIndex) => {
- // 设置行样式
- row.style.borderTop = '1px solid black'
- row.style.borderBottom = '1px solid black'
- row.style.pageBreakInside = 'avoid' // 防止行内分页
- // 处理单元格
- row.querySelectorAll('td, th').forEach((cell, cellIndex) => {
- const originalStyles = cell.getAttribute('style') || ''
- cell.style.cssText = `
- border: 1px solid black !important;
- padding: 6px;
- word-wrap: break-word;
- overflow: hidden;
- ${originalStyles}
- `
- })
- })
- // 添加特定样式以处理分页情况
- const styleSheet = document.createElement('style')
- styleSheet.textContent = `
- #${tableId} {
- border-collapse: collapse !important;
- border: 1px solid black !important;
- }
-
- #${tableId} tr {
- border-top: 1px solid black !important;
- border-bottom: 1px solid black !important;
- }
-
- #${tableId} td,
- #${tableId} th {
- border: 1px solid black !important;
- }
-
- /* 处理分页后的表格边框 */
- @media print {
- #${tableId} {
- border: 1px solid black !important;
- }
-
- #${tableId}[data-table-continued="true"] {
- border-top: 1px solid black !important;
- }
-
- #${tableId}[data-table-start="true"] {
- border-bottom: 1px solid black !important;
- }
- }
- `
- document.head.appendChild(styleSheet)
- },
- // 处理无边框的连续表格
- processContinuousTableNoBorder(table, tableCounter) {
- table.querySelectorAll('tr').forEach((row) => {
- row.querySelectorAll('td, th').forEach((cell) => {
- const originalStyles = cell.getAttribute('style') || ''
- cell.style.cssText = `
- padding: 6px;
- word-wrap: break-word;
- overflow: hidden;
- ${originalStyles}
- `
- })
- })
- },
- // 设置列宽的辅助方法
- setColumnWidth(table, columnIndex, width) {
- table
- .querySelectorAll(
- `tr td:nth-child(${columnIndex + 1}), tr th:nth-child(${
- columnIndex + 1
- })`
- )
- .forEach((td) => {
- td.style.width = width
- })
- },
- // 生成表格样式
- generateTableStyles(tableCount) {
- return `
- /* 基础表格样式 */
- table {
- width: 100%;
- border-collapse: collapse;
- table-layout: fixed;
- page-break-inside: auto;
- }
-
- /* 有边框表格的通用样式 */
- table[data-has-border="true"] {
- border-collapse: collapse !important;
- }
-
- /* 处理分页情况 */
- @media print {
- /* 确保分页后的表格边框显示 */
- table[data-has-border="true"] {
- border: 1px solid black !important;
- }
-
- /* 处理跨页表格的边框 */
- table[data-has-border="true"][data-table-continued="true"] {
- border-top: 1px solid black !important;
- margin-top: 0 !important;
- }
-
- table[data-has-border="true"][data-table-start="true"] {
- border-bottom: 1px solid black !important;
- }
-
- /* 确保所有单元格边框显示 */
- table[data-has-border="true"] td,
- table[data-has-border="true"] th {
- border: 1px solid black !important;
- }
- }
- `
- },
- // 判断表格是否是连续的
- isTableContinued(previousTable, currentTable) {
- // 检查两个表格是否紧邻
- const nextElement = previousTable.nextElementSibling
- if (!nextElement) return false
- // 检查是否有分页标记
- if (nextElement.classList.contains('page-break')) {
- return true
- }
- // 检查表格结构是否相似
- const prevCols = previousTable.querySelectorAll(
- 'tr:first-child td, tr:first-child th'
- ).length
- const currCols = currentTable.querySelectorAll(
- 'tr:first-child td, tr:first-child th'
- ).length
- return prevCols === currCols
- },
- // 处理表格表头
- processTableHeaders(content) {
- const tempDiv = document.createElement('div')
- tempDiv.innerHTML = content
- // 处理所有需要重复表头的表格
- tempDiv
- .querySelectorAll('table[data-repeat-header="true"]')
- .forEach((table) => {
- const headerHTML = table.getAttribute('data-header')
- if (!headerHTML) return
- // 创建thead元素
- const thead = document.createElement('thead')
- thead.innerHTML = headerHTML
- // 将原有的表头移动到thead中
- const firstRow = table.querySelector('tr')
- if (firstRow) {
- table.insertBefore(thead, firstRow)
- }
- // 确保tbody存在
- if (!table.querySelector('tbody')) {
- const tbody = document.createElement('tbody')
- while (table.querySelector('tr')) {
- tbody.appendChild(table.querySelector('tr'))
- }
- table.appendChild(tbody)
- }
- })
- return tempDiv.innerHTML
- },
- /* */
- // 添加 PDF 转 Word 的方法
- async convertPDFToWord(pdfPath) {
- try {
- // 创建FormData对象
- const formData = new FormData()
- formData.append('oss_url', pdfPath)
- // 调用后端转换接口
- const res = await axios.post(
- `${process.env.VUE_APP_BASE_API}/convert/pdftoword`,
- formData,
- {
- headers: {
- 'Content-Type': 'multipart/form-data' // 设置请求头为form-data
- }
- }
- )
- if (res.status === 200) {
- // 将base64转换为Blob
- const byteCharacters = atob(res.data.data.file_content)
- const byteNumbers = new Array(byteCharacters.length)
- for (let i = 0; i < byteCharacters.length; i++) {
- byteNumbers[i] = byteCharacters.charCodeAt(i)
- }
- const byteArray = new Uint8Array(byteNumbers)
- const blob = new Blob([byteArray], {
- type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
- })
- // 创建下载链接
- const link = document.createElement('a')
- link.href = URL.createObjectURL(blob)
- link.download = 'converted.docx' // 或使用服务器返回的文件名
- document.body.appendChild(link)
- link.click()
- document.body.removeChild(link)
- // 清理URL对象
- URL.revokeObjectURL(link.href)
- this.$message.success('转换成功')
- } else {
- throw new Error(res.data.message || '转换失败')
- }
- } catch (error) {
- console.error('PDF转Word失败:', error)
- this.$message.error('PDF转Word失败,请稍后重试')
- }
- },
- // 2. 替换原有的 exportDocument 方法
- async exportDocument(template) {
- try {
- const templateData = JSON.parse(template.data || '[]')
- const contentData = `
- <!DOCTYPE html>
- <html>
- <head>
- <meta charset="UTF-8">
- <style>
- @page {
- size: A4;
- margin: 2cm;
- }
- body {
- font-family: Arial, sans-serif;
- font-size: 12pt;
- line-height: 1.5;
- }
- /* 目录样式 */
- .toc {
- margin-bottom: 30px;
- }
- .toc-item {
- display: flex;
- justify-content: space-between;
- margin: 5px 0;
- border-bottom: 1px dotted #000;
- }
- .toc-text {
- margin-right: 10px;
- }
- .toc-page {
- margin-left: 10px;
- }
- /* 章节标题样式 */
- .section-title {
- font-weight: bold;
- margin-top: 20px;
- margin-bottom: 10px;
- page-break-after: avoid;
- }
- /* 页码样式 */
- .page-number {
- position: fixed;
- bottom: 20px;
- right: 20px;
- font-size: 10pt;
- }
- </style>
- </head>
- <body>
- <!-- 添加目录 -->
- <div class="toc">
- <h2>Content</h2>
- ${this.generateTableOfContents(templateData)}
- </div>
-
- <!-- 添加正文内容 -->
- ${this.processContentWithSections(templateData)}
- </body>
- </html>
- `
- // 调用导出接口
- const res = await htmlWordNew({
- content: contentData,
- title: template.title,
- options: {
- format: 'docx',
- encoding: 'utf-8',
- preserveStyles: true
- }
- })
- if (res.status !== 200) {
- this.$alert(res.errMsg)
- return
- }
- // 处理文件下载
- const link = document.createElement('a')
- link.href = res.data.file_path
- link.download = `${template.title}.docx`
- document.body.appendChild(link)
- link.click()
- document.body.removeChild(link)
- this.$message.success('导出成功')
- } catch (error) {
- console.error('导出Word文档失败:', error)
- this.$alert('导出文档时发生错误,请稍后重试。')
- }
- },
- /* 客户名称 */
- getCusomName(phaseId) {
- const product = this.customList.find((item) => item.id === phaseId)
- return product ? product.name : '未知客户'
- },
- /* 当前阶段 */
- getPhaseName(phaseId) {
- const product = this.currentPhaseList.find(
- (item) => item.value === phaseId
- )
- return product ? product.label : '未知阶段'
- },
- getPhaseType(phaseId) {
- const product = this.currentPhaseList.find(
- (item) => item.value === phaseId
- )
- if (!product) return 'info'
- // 根据不同阶段返回同的类型
- // 这里的判断逻辑需要根据实际的产品阶来调整
- switch (product.status) {
- case 1:
- return 'success' // 已完成
- case 2:
- return 'warning' // 进行中
- case 3:
- return 'danger' // 待处理
- default:
- return 'info' // 其他状态
- }
- },
- /* 目前状态 */
- getStatusName(statusId) {
- const status = this.getstatusList.find((item) => item.value === statusId)
- return status ? status.label : '未状态'
- },
- getStatusType(statusId) {
- // 这里可以根据不同状态返回不同的类型
- // 可用的类型:success/warning/danger/info
- const status = this.getstatusList.find((item) => item.value === statusId)
- if (!status) return 'info'
- // 根据状值返回对应的类型
- switch (status.value) {
- case 5:
- return 'success'
- case 4:
- return 'warning'
- case 3:
- return 'danger'
- default:
- return 'info'
- }
- },
- // 格式化完成度显示
- formatCompletion(rate) {
- if (rate === undefined || rate === null) {
- return '0%'
- }
- return `${rate}%`
- },
- // 根据完成度返回对应CSS类名
- getCompletionClass(rate) {
- if (!rate) return 'completion-low'
- if (rate >= 100) return 'completion-high'
- if (rate >= 50) return 'completion-medium'
- return 'completion-low'
- },
- handleCommand(command) {
- switch (command.type) {
- case 'template':
- this.selTemplate(command.row)
- break
- case 'enter':
- this.enterPro(command.row)
- break
- case 'edit':
- this.btnEdit(command.row)
- break
- case 'delete':
- this.btnDelete(command.row.id)
- break
- case 'viewTemplates':
- this.viewBoundTemplates(command.row)
- break
- }
- },
- // Add new methods
- // 查看已绑定模版
- viewBoundTemplates(row) {
- this.currentData = row
- this.boundTemplateLoading = true
- searchTemplateListNoData({ ids: row.documentIds.join(',') || 0 })
- .then((res) => {
- if (res.status === 200) {
- this.boundTemplateList = res.data
- this.boundTemplatesDialogVisible = true
- }
- })
- .finally(() => {
- this.boundTemplateLoading = false
- })
- },
- async previewTemplate(template) {
- console.log(template);
- this.currentTemplate = template
- /* this.loadDocumentVersions(template); */
- const res = await searchlistDocTemplate({
- /* page: 1,
- pageSize: 10, */
- project_id:this.currentData.id,
- id: template.id
- })
- if (res.status === 200) {
- /* this.documentVersions = res.data.dataList[0]; */
- if (res.data.documents.length !== 0) {
- const url = `#/document/viewCreate?articleId=${res.data.documents[0].id}&type=document`
- window.open(url, '_blank')
- } else {
- const a = document.createElement('a')
- a.href = '#/document/viewCreate?articleId=' + template.id
- a.target = '_blank'
- a.click()
- }
- /* if (this.documentVersions.length === 0) {
- this.$message.warning("未找到相关文档版本");
- } */
- }
- },
- replaceTemplate(template) {
- this.docAttr = template
- const TemList = JSON.parse(template.data)
- /* this.$confirm("确定要替换此模吗?", "提示", {
- confirmButtonText: "确",
- cancelButtonText: "取消",
- type: "warning",
- }).then(() => { */
- this.replaceVisible = true
- this.searchEx(TemList)
- },
- // 新增:处理已选定模版的选择
- async handleBoundTemplateSelect(selection) {
- this.selectedBoundTemplates = selection
- /* try {
- if (!selection || selection.length === 0) {
- this.selectedBoundTemplates = [];
- return;
- }
- // 获取选中模板的详细信息
- const templatePromises = selection.map(async (item) => {
- const templateId = JSON.parse(item.data)[0].id;
- const res = await getTemplateInfo({ id: templateId });
- if (res.status === 200 && res.data) {
- return {
- ...res.data,
- data: res.data.data || '[]' // 确保 data 字段存在
- };
- }
- return null;
- });
- // 等待所有模板信息加载完成
- const templates = await Promise.all(templatePromises);
- // 过滤掉无效的模板并保存
- this.selectedBoundTemplates = templates.filter(template => template !== null);
- console.log(this.selectedBoundTemplates);
- } catch (error) {
- console.error('获取模板信息失败:', error);
- this.$message.error('获取模板信息失败');
- this.selectedBoundTemplates = [];
- } */
- },
- // 新增:批量替换方法
- async batchReplace() {
- if (this.selectedBoundTemplates.length === 0) {
- this.$message.warning('请选择需要替换的模版')
- return
- }
- this.batchReplaceLoading = true
- try {
- // 依次处理每个选中的模版
- for (const template of this.selectedBoundTemplates) {
- this.docAttr = template
- const res = await searchlistDocTemplate({
- /* page: 1,
- pageSize: 10, */
- project_id:this.currentData.id,
- id: template.id,
- is_template:1
- })
- if(res.status!== 200) return
- // 检查模板数据格式
- let templateData
- try {
- // 优先尝试使用 data 字段
- if(res.data.documents.length!==0){
- templateData = JSON.parse(res.data.documents[0].data || '[]')
- }else{
- const template_Data = await getDocumentInfo({id:template.id})
- if(template_Data.status!==200) return
- templateData = JSON.parse(template_Data.data.data || '[]')
- }
-
- } catch (e) {
- // 如果解析失败,尝试使用 content 字段
- templateData = template.content
- ? [{ content: template.content }]
- : []
- }
- if (templateData && templateData.length > 0) {
- await this.searchEx(templateData)
- if (this.matchResults.length > 0) {
- await this.replaceAll()
- }
- } else {
- console.warn(`模板 ${template.id} 没有有效的内容数据`)
- }
- }
- this.$message.success('批量替换完成')
- } catch (error) {
- console.error('批量替换失败:', error)
- this.$message.error('批量替换过程中发生错误: ' + error.message)
- } finally {
- this.batchReplaceLoading = false
- }
- },
- searchEx(val) {
- return new Promise((resolve) => {
- const allMatches = []
- this.TemList = val
- this.TemList.forEach((el) => {
- // 创建临时 div 来解析 HTML 内容
- const tempDiv = document.createElement('div')
- tempDiv.innerHTML = el.content
- const plainText = tempDiv.textContent || tempDiv.innerText
- // 分别匹配中文括号和英文括号
- const patterns = [
- /【([A-Z]{1,4}[0-9]{0,2})-([A-Z]{1,4}[0-9]{0,2})-([0-9]{2})-([0-9]{2,3})】/g,
- /\[([A-Z]{1,4}[0-9]{0,2})-([A-Z]{1,4}[0-9]{0,2})-([0-9]{2})-([0-9]{2,3})\]/g
- ]
- patterns.forEach((pattern) => {
- let match
- while ((match = pattern.exec(plainText)) !== null) {
- const fullMatch = match[0]
- const segments = [match[1], match[2], match[3], match[4]]
- // 验证个段落的格式
- const isValid = segments.every((segment) => segment.length > 0)
- if (isValid) {
- allMatches.push(fullMatch)
- }
- }
- })
- })
- this.matchResults = [...new Set(allMatches)]
- // 调试用:打印每个匹配的详细信息
- this.matchResults.forEach((match) => {
- // 据括号类型选不同的切片方式
- const content = match.startsWith('【')
- ? match.slice(1, -1)
- : match.slice(1, -1)
- const parts = content.split('-')
- })
- resolve()
- })
- },
- // 添加的辅助方法来判断文档类型
- getDocumentType(code) {
- if (code.includes('-RP-')) return 'template_location'
- if (code.includes('-IR-')) return 'middle_layer_location'
- if (code.includes('-WE-')) return 'tech_report_location'
- return 'other_location'
- },
- // 修改式化请求参数的方法
- formatRequestParams(locations) {
- const locationArray = Array.isArray(locations) ? locations : [locations]
- // 初始化求参数对象
- const params = {
- /* project_id:'', */
- template_location: [],
- middle_layer_location: [],
- tech_report_location: [],
- other_location: []
- }
- // 根据不同类型分类存储
- locationArray.forEach((location) => {
- const cleanLocation = location.replace(/[\[\]【】]/g, '')
- const type = this.getDocumentType(cleanLocation)
- console.log('params', type)
- // 为每种类型创建对应长度的空数组
- Object.keys(params).forEach((key) => {
- if (type === key) {
- params[key].push(cleanLocation)
- } else {
- params[key].push('')
- }
- })
- })
- return params
- },
- // 解析内容,处理特殊标记
- parseContent(content) {
- if (!content) return ''
- let parsed = content
- // 处理图片标记
- parsed = parsed.replace(
- /\[图片:(.*?)\|style=(.*?)\|class=(.*?)\]/g,
- (match, src, style, className) => {
- return `<img src="${src}" alt="图片" style="${style}" class="${className}">`
- }
- )
- // 处理表格标记
- parsed = parsed.replace(
- /\[表格\|style=(.*?)\|class=(.*?)\]\n([\s\S]*?)(?=\[|$)/g,
- (match, tableStyle, tableClass, tableContent) => {
- const rows = tableContent.trim().split('\n')
- let tableHtml = `<table style="${tableStyle}" class="${tableClass}">`
- rows.forEach((row) => {
- tableHtml += '<tr>'
- const cells = row.split(' | ').map((cell) => {
- const [content, ...styleInfo] = cell.split('[style=')
- if (styleInfo.length) {
- const [style, className] = styleInfo[0]
- .slice(0, -1)
- .split('|class=')
- return `<td style="${style}" class="${className}">${content}</td>`
- }
- return `<td>${content}</td>`
- })
- tableHtml += cells.join('') + '</tr>'
- })
- tableHtml += '</table>'
- return tableHtml
- }
- )
- // 处理文本样式
- parsed = parsed.replace(
- /\[文本:(.*?)\|style=(.*?)\|class=(.*?)\]/g,
- (match, content, style, className) => {
- return `<span style="${style}" class="${className}">${content}</span>`
- }
- )
- return parsed
- },
- // 查内容是否包含表格或图片
- containsTableOrImage(content) {
- return content.includes('<table') || content.includes('<img')
- },
- // 获取所有文本节点
- getTextNodes(node) {
- const textNodes = []
- const walk = document.createTreeWalker(
- node,
- NodeFilter.SHOW_TEXT,
- null,
- false
- )
- let currentNode
- while ((currentNode = walk.nextNode())) {
- textNodes.push(currentNode)
- }
- return textNodes
- },
- /* 更新 */
- async onUpload() {
- const _this = this
- if (_this.TemList.length <= 0) {
- _this.$message.error('增加组件')
- return
- }
- if (_this.docAttr.title == '') {
- _this.$message.error('请填写模版标题')
- return
- }
- const res = await searchlistDocTemplate({
- /* page: 1,
- pageSize: 10, */
- project_id:this.currentData.id,
- id: _this.docAttr.id
- })
- if(res.status!==200) return
- if(res.data.documents.length==0){
- _this.docAttr.title = _this.docAttr.title + new Date().getTime()
- _this.docAttr.links = JSON.stringify(_this.docAttr.linkProduct)
- _this.docAttr.projects = JSON.stringify(_this.docAttr.linkProject)
- _this.docAttr.data = JSON.stringify(_this.TemList)
- _this.docAttr.is_template = 0
- /* _this.docAttr.id = _this.docAttr.id; */
- _this.docAttr.user_name = _this.docAttr.name
- _this.docAttr.user_id = _this.$store.state.user.id // _this.docAttr.id;
- _this.docAttr.template_id = _this.docAttr.id
- createDocument(_this.docAttr).then((res) => {
- if (res.status != 200) return // 更新文档
- /* _this.docAttr.id = res.data; */
- _this.$message.success('文档创建成功')
- searchlistDoc({
- page: 1,
- pageSize: 999
- })
- })
- }else{
- updateDocument({
- id:res.data.documents[0].id,
- data :JSON.stringify(_this.TemList),
- title:res.data.documents[0].title,
- status:5,
- document_type:res.data.documents[0].document_type,
- is_template:0,
- template_id:res.data.documents[0].template_id,
- user_id:res.data.documents[0].user_id
- }).then(res=>{
- if (res.status != 200) return
- _this.$message.success('文档更新成功')
- })
- }
-
- },
- // 添加统一的内容处理方法
- async processAndReplaceContent(content, originalMatch) {
- try {
- const params = this.formatRequestParams(originalMatch)
- params.project_id = this.currentData.id
- const response = await axios.post(
- `${process.env.VUE_APP_BASE_API}/project-raw-data/batch-content`,
- params,
- {
- headers: {
- 'Content-Type': 'application/json'
- }
- }
- )
- if (response.status === 200 && response.data) {
- const replacementContent =
- response.data.data.matched_data[0]?.content
- if (replacementContent) {
- // 处富文本内容
- this.TemList.forEach((el, index) => {
- console.log(el, originalMatch, replacementContent)
- const processedContent = this.processTableContent(
- el.content,
- originalMatch,
- replacementContent
- )
- this.$set(this.TemList[index], 'content', processedContent)
- })
- return true
- }
- }
- return false
- } catch (error) {
- console.error('处理内容失败:', error)
- return false
- }
- },
- // 修改单个替换方法
- async replaceItem(item) {
- try {
- const success = await this.processAndReplaceContent(this.TemList, item)
- if (success) {
- this.matchResults = this.matchResults.filter(
- (match) => match !== item
- )
- this.onUpload()
- this.$message.success('替换成功')
- } else {
- this.$message.warning('未获取到替换内容')
- }
- } catch (error) {
- console.error('替换失败:', error)
- this.$message.error(error.response?.data?.message || '替换失败')
- }
- },
- // 修改批量替换方法
- async replaceAll() {
- this.batchReplaceLoading = true
- try {
- // 收所有配
- const allLocations = this.matchResults.map((item) => {
- return item.replace(/[\[\]【】]/g, '')
- })
- // 格式化请求参数
- const params = {
- template_location: [],
- middle_layer_location: [],
- tech_report_location: [],
- other_location: []
- }
- // 遍历所有位置并分类
- allLocations.forEach((location) => {
- const type = this.getDocumentType(location)
- Object.keys(params).forEach((key) => {
- if (type === key) {
- params[key].push(location)
- } else {
- params[key].push('')
- }
- })
- })
- params.project_id = this.currentData.id
- // 发送批量请求
- const response = await axios.post(
- `${process.env.VUE_APP_BASE_API}/project-raw-data/batch-content`,
- params,
- {
- headers: {
- 'Content-Type': 'application/json'
- }
- }
- )
- if (response.status === 200 && response.data) {
- const replacementContents = response.data.data.matched_data
- if (replacementContents && replacementContents.length > 0) {
- // 更新所有内容
- this.TemList.forEach((el, index) => {
- let updatedContent = el.content
- // 遍历所有替换内容和对应匹配项
- this.matchResults.forEach((item) => {
- // 提取编号的函数只需定义一
- const getCodeFromItem = (searchItem) => {
- const cleanItem = searchItem.replace(/[\[\]【】]/g, '')
- const match = cleanItem.match(/([A-Z0-9]+-[A-Z]+-\d+-\d+)/)
- return match ? match[1] : null
- }
- // 获取当前项的编号
- const currentCode = getCodeFromItem(item)
- if (!currentCode) return // 如果没有有效编号,跳过此项
- // 查找匹配的替换内容
- const matchedContent = replacementContents.find(
- (content) => content.template_location === currentCode
- )
- if (matchedContent?.content) {
- // 构建替换内容
- const formattedReplacement = `
- <span class="replacement-content" data-code="${currentCode}">
- ${matchedContent.content}
- </span>
- `.trim()
- // 处理表格和其他内容
- updatedContent = this.processTableContent(
- updatedContent,
- item,
- formattedReplacement
- )
- // 记录日志
- if (process.env.NODE_ENV !== 'production') {
- console.log('替换信息:', {
- 原始编号: currentCode,
- 原始内容: item,
- 替换内容: formattedReplacement
- })
- }
- }
- })
- // 更新内容
- this.$set(this.TemList[index], 'content', updatedContent)
- })
- /* // 更新文档*/
- await this.onUpload()
- this.matchResults = []
- this.$message.success('批量替换成功')
- this.replaceVisible = false
- } else {
- this.$message.warning('未获取到替换内容')
- }
- }
- } catch (error) {
- console.error('批量替换失败:', error)
- this.$message.error(error.response?.data?.message || '批量替换失败')
- } finally {
- this.batchReplaceLoading = false
- }
- },
- // 辅助方法:获取文档类型
- getDocumentType(code) {
- if (code.includes('-RP-')) return 'template_location'
- if (code.includes('-IR-')) return 'middle_layer_location'
- if (code.includes('-WE-')) return 'tech_report_location'
- return 'other_location'
- },
- // 修改处理表格内容的方法,添加对多个替换的支持
- processTableContent(content, searchItem, replacement) {
- if (!content || !searchItem) return content
- try {
- // 提取搜索项中的编号
- const getCodeFromItem = (item) => {
- const cleanItem = item.replace(/[\[\]【】]/g, '')
- const match = cleanItem.match(/([A-Z0-9]+-[A-Z]+-\d+-\d+)/)
- return match ? match[1] : null
- }
- const searchCode = getCodeFromItem(searchItem)
- if (!searchCode) {
- console.warn('无法从搜索项中提取编号:', searchItem)
- return content
- }
- const tempDiv = document.createElement('div')
- tempDiv.innerHTML = content
- // 新增:递归处理HTML节点
- const processNode = (node) => {
- if (node.nodeType === 3) {
- // 文本节点
- return node.textContent
- }
- let html = ''
- for (const child of node.childNodes) {
- html += processNode(child)
- }
- return html
- }
- const handleSplitContent = () => {
- // 先获取纯文本内容用于匹配
- const plainText = processNode(tempDiv)
- // 构建用于匹配分割标记的正则表达式
- const splitRegex = new RegExp(
- `(\\[|【)(.*?)(${searchCode})(.*?)(\\]|】)`,
- 'g'
- )
- const currentHtml = tempDiv.innerHTML
- // 查找所有可能的匹配
- const matches = [
- ...currentHtml.matchAll(/<[^>]+>|\[|\]|【|】|[^<\[\]【】]+/g)
- ]
- // 重建HTML,识别并替换目标编码
- let rebuiltHtml = ''
- let currentMatch = ''
- let isCollecting = false
- let bracketCount = 0
- for (let i = 0; i < matches.length; i++) {
- const part = matches[i][0]
- if (part === '[' || part === '【') {
- isCollecting = true
- bracketCount++
- currentMatch = part
- continue
- }
- if (isCollecting) {
- currentMatch += part
- if (part === ']' || part === '】') {
- bracketCount--
- if (bracketCount === 0) {
- // 检查收集到的内容是否包含目标编码
- const cleanText = currentMatch.replace(/<[^>]+>/g, '')
- if (cleanText.includes(searchCode)) {
- rebuiltHtml += replacement
- isCollecting = false
- currentMatch = ''
- } else {
- rebuiltHtml += currentMatch
- isCollecting = false
- currentMatch = ''
- }
- }
- }
- } else {
- rebuiltHtml += part
- }
- }
- // 处理未闭合的标记
- if (currentMatch) {
- rebuiltHtml += currentMatch
- }
- // 清理可能的残留标签
- rebuiltHtml = rebuiltHtml
- .replace(/(<span[^>]*>)\s*(<\/span>)/g, '')
- .replace(/(<\/span>)(<span[^>]*>)/g, '')
- .replace(/<a[^>]*><\/a>/g, '')
- .replace(/\s+/g, ' ')
- tempDiv.innerHTML = rebuiltHtml
- }
- handleSplitContent()
- return tempDiv.innerHTML
- } catch (error) {
- console.error('替换内容时出错:', error)
- console.error('错误详情:', {
- 原始内容: content.substring(0, 100) + '...',
- 搜索项: searchItem,
- 替换内容: replacement,
- 错误信息: error.message
- })
- return content
- }
- },
- // 添加样式到 convertTextToHtml 方法
- convertTextToHtml(text) {
- if (!text) return ''
- let html = text
- // 处理特定格式的表格
- html = html.replace(
- /\[表格\|style=(.*?)\|class=(.*?)\]([\s\S]*?)(?=\[|$)/g,
- (match, tableStyle, tableClass, tableContent) => {
- const rows = tableContent.trim().split('\n')
- let tableHtml = `<table style="${tableStyle}; border-collapse: collapse;" class="${tableClass}">`
- rows.forEach((row, index) => {
- const [label, value] = row.split('|').map((item) => item.trim())
- if (label) {
- tableHtml += '<tr>'
- if (value) {
- // 如果有值,创建两列的行
- tableHtml += `
- <td style="border: 1px solid #000; padding: 8px;">${label}</td>
- <td style="border: 1px solid #000; padding: 8px;">${value}</td>
- `
- } else {
- // 如果只有标签,创建单列的行(可能是表头)
- tableHtml += `
- <td style="border: 1px solid #000; padding: 8px; font-weight: ${
- index === 0 ? 'bold' : 'normal'
- }">${label}</td>
- `
- }
- tableHtml += '</tr>'
- }
- })
- tableHtml += '</table>'
- return tableHtml
- }
- )
- // 保持原有的图片和文本处理逻辑不变
- html = html.replace(
- /\[图片:(.*?)\|style=(.*?)\|class=(.*?)\]/g,
- (match, src, style, className) => {
- return `<img src="${src}" alt="图片" style="${style}" class="${className}">`
- }
- )
- html = html.replace(
- /\[文本:(.*?)\|style=(.*?)\|class=(.*?)\]/g,
- (match, content, style, className) => {
- return `<span style="${style}" class="${className}">${content}</span>`
- }
- )
- return html
- },
- /* 选择模版 */
- selTemplate(row) {
- this.currentData = row
- // 重置所有状态
- this.selectedTemplateIds = new Set(row.documentIds || [])
- this.selectedPageIds = new Map()
- this.selectionCache = new Map()
- this.isAllSelected = false
- this.templateQueryForm.page = 1
- // 加载模板列表
- this.loadTemplateList()
- },
- // 处理模版分页变化
- async handleTemplatePageChange(page) {
- try {
- // 保存当前页的选择状态
- const currentPage = this.templateQueryForm.page
- const currentSelection = this.$refs.templateTable?.selection || []
- // 更新选择缓存
- this.selectionCache.set(
- currentPage,
- new Set(currentSelection.map((row) => row.id))
- )
- // 更新全局选择状态
- currentSelection.forEach((row) => {
- this.selectedTemplateIds.add(row.id)
- })
- // 更新页码
- this.templateQueryForm.page = page
- // 加载新页面数据
- await this.loadTemplateList()
- } catch (error) {
- console.error('页面切换失败:', error)
- this.$message.error('页面切换失败')
- }
- },
- // 加载模版表
- async loadTemplateList() {
- this.templateLoading = true
- try {
- const res = await searchList({
- ...this.templateQueryForm,
- document_type: this.templateQueryForm.document_type || undefined // 只在有值时传递
- })
- if (res.status === 200) {
- this.templateList = res.data.dataList
- this.templateTotal = res.data.totalRecord
- this.templateDialogVisible = true
- await this.$nextTick()
- if (this.$refs.templateTable) {
- this.$refs.templateTable.clearSelection()
- this.restoreSelectionState()
- }
- }
- } catch (error) {
- console.error('加载模板列表失败:', error)
- this.$message.error('加载模板列表失败')
- } finally {
- this.templateLoading = false
- }
- },
- // 添加新方法:恢复选择状态
- restoreSelectionState() {
- const currentPage = this.templateQueryForm.page
- // 遍历当前页数据
- this.templateList.forEach((row) => {
- // 检查是否在全局选中集合中
- if (this.selectedTemplateIds.has(row.id)) {
- this.$refs.templateTable.toggleRowSelection(row, true, false)
- }
- })
- // 更新当前页的选择记录
- const selectedRows = this.templateList.filter((row) =>
- this.selectedTemplateIds.has(row.id)
- )
- // 更新选择缓存
- this.selectionCache.set(
- currentPage,
- new Set(selectedRows.map((row) => row.id))
- )
- // 检查当前页是否全选
- const isCurrentPageAllSelected = this.templateList.every((row) =>
- this.selectedTemplateIds.has(row.id)
- )
- // 更新表头的全选框状态
- if (this.$refs.templateTable.store) {
- this.$refs.templateTable.store.states.isAllSelected =
- isCurrentPageAllSelected
- }
- // 触发选择变化事件
- this.handleSelectionChange(selectedRows)
- },
- // 修改 handleSelectionChange 方法
- handleSelectionChange(selection) {
- if (!Array.isArray(selection)) {
- console.warn('Invalid selection:', selection)
- return
- }
- const currentPage = this.templateQueryForm.page
- // 更新选择缓存
- this.selectionCache.set(
- currentPage,
- new Set(selection.map((row) => row.id))
- )
- // 更新全局选择状态
- const currentPageIds = new Set(this.templateList.map((row) => row.id))
- // 移除当前页未选中的项
- currentPageIds.forEach((id) => {
- if (!selection.some((row) => row.id === id)) {
- this.selectedTemplateIds.delete(id)
- }
- })
- // 添加当前页选中的项
- selection.forEach((row) => {
- this.selectedTemplateIds.add(row.id)
- })
- // 更新全选状态
- this.isAllSelected =
- this.templateList.length > 0 &&
- this.templateList.every((row) => this.selectedTemplateIds.has(row.id))
- },
- handleSelect(selection, row) {
- if (!row || typeof row.id === 'undefined') {
- console.warn('Invalid row data:', row)
- return
- }
- const currentPage = this.templateQueryForm.page
- const isSelected = selection.some((item) => item.id === row.id)
- if (isSelected) {
- this.selectedTemplateIds.add(row.id)
- // 更新当前页选择记录
- const currentPageSelected =
- this.selectedPageIds.get(currentPage) || new Set()
- currentPageSelected.add(row.id)
- this.selectedPageIds.set(currentPage, currentPageSelected)
- } else {
- this.selectedTemplateIds.delete(row.id)
- // 更新当前页选择记录
- const currentPageSelected = this.selectedPageIds.get(currentPage)
- if (currentPageSelected) {
- currentPageSelected.delete(row.id)
- }
- // 取消全选状态
- this.isAllSelected = false
- }
- },
- // 修改加载方法
- loadTemplateList() {
- this.templateLoading = true
- searchList(this.templateQueryForm)
- .then((res) => {
- if (res.status === 200) {
- this.templateList = res.data.dataList
- this.templateTotal = res.data.totalRecord
- this.templateDialogVisible = true
- // 在数据加载完成后设置选中状态
- this.$nextTick(() => {
- if (this.$refs.templateTable) {
- // 清除当前选择
- this.$refs.templateTable.clearSelection()
- // 遍历当前页数据,根据 selectedTemplateIds 设置选中状态
- this.templateList.forEach((row) => {
- if (this.selectedTemplateIds.has(row.id)) {
- this.$refs.templateTable.toggleRowSelection(row, true)
- }
- })
- // 更新当前页的选择记录
- const currentPage = this.templateQueryForm.page
- const currentPageSelected = new Set(
- this.templateList
- .filter((row) => this.selectedTemplateIds.has(row.id))
- .map((row) => row.id)
- )
- this.selectedPageIds.set(currentPage, currentPageSelected)
- // 检查当前页是否全选
- const isCurrentPageAllSelected = this.templateList.every(
- (row) => this.selectedTemplateIds.has(row.id)
- )
- // 更新表头的全选框状态
- if (this.$refs.templateTable.store) {
- this.$refs.templateTable.store.states.isAllSelected =
- isCurrentPageAllSelected
- }
- // 触发选择变化事件以更新UI
- this.handleSelectionChange(
- this.templateList.filter((row) =>
- this.selectedTemplateIds.has(row.id)
- )
- )
- }
- })
- }
- })
- .catch((error) => {
- console.error('加载模板列表失败:', error)
- this.$message.error('加载模板列表失败')
- })
- .finally(() => {
- this.templateLoading = false
- })
- },
- // 修改选择所有页方法
- async selectAllPages() {
- try {
- this.isAllSelected = true
- const allTemplates = await this.getAllTemplates()
- // 清除之前的选择
- this.selectedTemplateIds.clear()
- this.selectedPageIds.clear()
- // 添加所有模板ID到选择集合
- allTemplates.forEach((template) => {
- this.selectedTemplateIds.add(template.id)
- })
- // 更新当前页显示
- if (this.$refs.templateTable) {
- this.$refs.templateTable.clearSelection()
- this.templateList.forEach((row) => {
- this.$refs.templateTable.toggleRowSelection(row, true)
- })
- }
- // 更新当前页的选择记录
- const currentPage = this.templateQueryForm.page
- this.selectedPageIds.set(
- currentPage,
- new Set(this.templateList.map((row) => row.id))
- )
- this.$message.success(`已选择全部 ${this.templateTotal} 条数据`)
- } catch (error) {
- console.error('选择所有页失败:', error)
- this.$message.error('选择所有页失败')
- this.isAllSelected = false
- }
- },
- // 获取有模版数据
- async getAllTemplates() {
- const pageSize = 999 // 使用较大的页码一次性获取
- const params = {
- ...this.templateQueryForm,
- page: 1,
- pageSize
- }
- try {
- const res = await searchList(params)
- if (res.status === 200) {
- return res.data.dataList
- }
- return []
- } catch (error) {
- console.error('获取所有模版失败:', error)
- return []
- }
- },
- // 确认选择
- confirmTemplateSelection() {
- if (this.selectedTemplateIds.size === 0) {
- this.$message.warning('请至少选择一个模版')
- return
- }
- // 转换Set为数组
- const selectedIds = Array.from(this.selectedTemplateIds)
- relate({
- project_id: this.currentData.id,
- doc_ids: JSON.stringify(selectedIds)
- }).then((res) => {
- if (res.status === 200) {
- this.$message.success('模版关联成功')
- this.closeTemplateDialog()
- // 更新当前数据的documentIds
- this.currentData.documentIds = selectedIds
- this.search()
- }
- })
- },
- // 关对话框
- closeTemplateDialog() {
- this.templateDialogVisible = false
- // 清除所有缓存
- this.selectedTemplateIds.clear()
- this.selectedPageIds.clear()
- this.selectionCache.clear()
- this.isAllSelected = false
- if (this.$refs.templateTable) {
- this.$refs.templateTable.clearSelection()
- }
- },
- // 输入项目
- enterPro(row) {
- if (row.documentIds.length == 0) {
- this.$message.warning('请先绑定模版!')
- return
- }
- // 获取绑定模版替换位置信息
- searchTemplateList({ ids: row.documentIds.join(',') || 0 })
- .then(async(res) => {
- if (res.status === 200) {
- const templates = res.data
- const locations = []
- // 遍历所有模版,提取替换位置
- templates.forEach((template) => {
- if (template.data) {
- const templateData = JSON.parse(template.data)
- templateData.forEach((item) => {
- const tempDiv = document.createElement('div')
- tempDiv.innerHTML = item.content
- const plainText = tempDiv.textContent || tempDiv.innerText
- const patterns = [
- /【([A-Z]{1,4}[0-9]{0,2})-([A-Z]{1,4}[0-9]{0,2})-([0-9]{2})-([0-9]{2,3})】/g,
- /\[([A-Z]{1,4}[0-9]{0,2})-([A-Z]{1,4}[0-9]{0,2})-([0-9]{2})-([0-9]{2,3})\]/g
- ]
- patterns.forEach((pattern) => {
- let match
- while ((match = pattern.exec(plainText)) !== null) {
- locations.push({
- template_id: template.id,
- template_title: template.title,
- location: match[0],
- type: this.getDocumentType(match[0])
- })
- }
- })
- })
- }
- })
- try {
- // 按类型分组位置信息
- const params = {
- template_location: [],
- middle_layer_location: [],
- tech_report_location: [],
- other_location: []
- }
- // 遍历所有位置并分类
- locations.forEach((loc) => {
- Object.keys(params).forEach((key) => {
- if (loc.type === key) {
- params[key].push(loc.location.replace(/[\[\]【】]/g, '')) // 清除括号
- } else {
- params[key].push('')
- }
- })
- })
- params.project_id = row.id
- // 获取替换内容
- const response = await axios.post(
- `${process.env.VUE_APP_BASE_API}/project-raw-data/batch-content`,
- params,
- {
- headers: {
- 'Content-Type': 'application/json'
- }
- }
- )
- if (response.status === 200 && response.data) {
- const replacementContents = response.data.data.matched_data
- // 将位置信息和对应的替换内容组合
- const enrichedLocations = locations.map((loc, index) => ({
- ...loc,
- content: replacementContents[index]?.content || null,
- template_location:
- replacementContents[index]?.template_location || null,
- tech_report_location:
- replacementContents[index]?.tech_report_location || []
- }))
- // 对整个 enrichedLocations 进行去重
- const uniqueEnrichedLocations = enrichedLocations.reduce(
- (acc, current) => {
- const isDuplicate = acc.find(
- (item) =>
- item.template_id === current.template_id &&
- item.location === current.location
- )
- if (!isDuplicate) {
- // 如果不是重复项,添加到结果数组中
- return [...acc, current]
- }
- return acc
- },
- []
- )
- // 使用去重后的数据
- this.$router.push({
- path: '/project/ProjectInput',
- query: {
- id: row.id,
- projectData: encodeURIComponent(
- JSON.stringify({
- locations: uniqueEnrichedLocations, // 使用去重后的数据
- templates: templates.map((t) => ({
- id: t.id,
- title: t.title
- }))
- })
- )
- }
- })
- }
- } catch (error) {
- console.error('获取替换内容失败:', error)
- this.$message.error('获取替换内容失败')
- }
- }
- })
- .catch((error) => {
- console.error('获取模版位置信息失败:', error)
- this.$message.error('获取模版位置信失败')
- })
- },
- /* 产品类别名称 */
- getCategoryTypeName(categoryId) {
- if (!categoryId) return '未分类'
- const product = this.productList.find((item) => item.id === categoryId)
- return product && product.category ? product.category.name : '未知类'
- },
- getCategoryName(categoryId) {
- if (!categoryId) return '未知产品'
- const product = this.productList.find((item) => item.id === categoryId)
- return product ? product.name : '未知产品'
- },
- searchData() {
- const _this = this
- _this.dialogVisible = false
- _this.search()
- },
- btnDelete(e) {
- const _this = this
- const par = {
- id: e
- }
- _this
- .$confirm('您是否确认删除该记录?', '提示', {
- confirmButtonText: '确认',
- cancelButtonText: '取消',
- type: 'warning'
- })
- .then((res) => {
- // console.log("AAA")
- deleteProject(par).then((res) => {
- _this.search()
- })
- })
- .catch(() => {})
- },
- // 编辑
- btnEdit(e) {
- const _this = this
- _this.currentDataId = e.id
- _this.dialogVisible = true
- },
- onClose(needRefresh) {
- console.log('onClose triggered with needRefresh:', needRefresh) // 添加调试日志
- this.currentDataId = 0
- this.dialogVisible = false
- if (needRefresh) {
- this.search()
- }
- },
- handleUpdate() {
- this.search() // 添加额外的更新处理方法
- },
- // 搜索
- async search() {
- try {
- // 1. 先加载并显示项目列表
- const res = await searchProject(this.queryForm)
- if (!res) return
- // 2. 先用0初始化完成度,让列表可以立即渲染
- const projectListFromTemplate = res.data.dataList.map((item) => ({
- ...item,
- calculate: 0
- }))
- // 3. 立即更新列表数据
- this.dataList = projectListFromTemplate
- this.recordCount = res.data.totalRecord
- this.pageTotal = res.data.totalPage
- // 4. 异步加载完成度
- this.loadProjectCompletions(projectListFromTemplate)
- } catch (err) {
- console.error('搜索失败:', err)
- this.$message.error('获取项目列表失败')
- }
- },
- // 修改分页
- ChangePage(e) {
- const _this = this
- _this.queryForm.page = e
- _this.search()
- },
- init() {
- searchCategory().then((res) => {
- if (res.status !== 200) return
- this.categoryList = res.data.dataList
- })
- /* 获取阶段 */
- getphases().then((res) => {
- this.currentPhaseList = res.data.options
- })
- /* 获取目前态 */
- getstatus().then((res) => {
- this.getstatusList = res.data.options
- })
- /* 获取客户列表 */
- searchCustomer({ page: 1, pageSize: 999 }).then((res) => {
- this.customList = res.data.dataList
- })
- /* 获取产品列表 */
- searchProduct({ page: 1, pageSize: 999 }).then((res) => {
- this.productList = res.data.dataList
- })
- },
- // 新增:处理导出命令
- async handleExport(command, row) {
- switch (command) {
- case 'word':
- await this.exportDocumentWithHtmlDocx(row) // this.exportDocument(row);
- break
- case 'pdf':
- await this.exportToPDF(row)
- break
- case 'pdf-to-word': // 添加新的命令类型
- const pdfResult = await this.exportToPDF_url(row)
- /* if (pdfResult && pdfResult.file_path) {
- await this.convertPDFToWord(pdfResult.file_path);
- } */
- break
- }
- },
- /* 过去pdf链接 */
- async exportToPDF_url(template) {
- try {
- // 解析模版内容
- const templateData = JSON.parse(template.data || '[]')
- // 创建一个临时的容器来存放内容
- const contentContainer = document.createElement('div')
- contentContainer.className = 'content'
- // 处理每个模版段落
- templateData.forEach((item) => {
- const templateSection = document.createElement('div')
- templateSection.className = 'template-textarea'
- templateSection.innerHTML = item.content
- contentContainer.appendChild(templateSection)
- })
- // 处理所有输入框
- contentContainer
- .querySelectorAll('input[type="text"]')
- .forEach((input) => {
- const span = document.createElement('span')
- span.textContent = input.value
- input.parentNode.replaceChild(span, input)
- })
- // 构建HTML内容
- const contentData = `<!DOCTYPE html>
- <html>
- <head>
- <meta charset="UTF-8">
- <style>
- @page {
- size: A4;
- margin: 2cm;
- }
- body {
- font-family: Arial, sans-serif;
- font-size: 12pt;
- line-height: 1.5;
- }
- /* 添加图片控制样式 */
- img {
- max-width: 100%;
- max-height: 100vh;
- object-fit: contain;
- page-break-inside: avoid;
- }
- /* 图片容器样式 */
- .image-container {
- width: 100%;
- text-align: center;
- margin: 10px 0;
- page-break-inside: avoid;
- }
- table {
- width: 100%;
- border-collapse: collapse;
- }
- td, th {
- border: 1px solid black;
- padding: 5px;
- }
- .template-textarea {
- page-break-after: always;
- }
- .TOC {
- display: none !important;
- }
- </style>
- </head>
- <body>
- ${this.processImages(contentContainer.innerHTML)}
- </body>
- </html>`
- /* // 处理表格样式
- contentData = contentData
- .replaceAll("<table ", `<table style="border-collapse: collapse;" `)
- .replaceAll("<td>", `<td style="border: 1px solid black;">`); */
- const formData = new FormData()
- formData.append('content', contentData)
- formData.append('title', template.title)
- // 调用导出接口,指定导出类型为 PDF
- const res = await axios.post(
- `${process.env.VUE_APP_BASE_API}/document/htmlPDFWord`,
- formData,
- {
- headers: {
- 'Content-Type': 'multipart/form-data' // 设置请求头为form-data
- }
- }
- ) /* await htmlPDFWord({
- content: contentData,
- title: template.title,
- }); */
- if (res.status !== 200) {
- this.$alert(res.errMsg)
- return
- }
- console.log(res)
- // 处理文件下载
- const link = document.createElement('a')
- link.href = res.data.data.file_path
- link.download = res.data.data.file_name
- document.body.appendChild(link)
- link.click()
- document.body.removeChild(link)
- this.$message.success('导出成功')
- // 处理文件下载
- return res.data // 返回结果,包含 file_path
- } catch (error) {
- console.error('导出PDF失败:', error)
- this.$alert('导出PDF时发生错误,请稍后重试。')
- }
- },
- // 添加新的 exportToPDF 方法
- async exportToPDF(template) {
- /* const res = await searchlistDocTemplate({
-
- project_id:this.currentData.id,
- id: template.id
- })
- if(res.status!==200) return
- let templateData */
- try {
- // 1. 先获取文档模板数据
- const resData = await searchlistDocTemplate({
- project_id: this.currentData.id,
- id: template.id
- })
- if (resData.status !== 200) {
- this.$message.error('获取文档模板失败')
- return
- }
- // 2. 获取模板数据
- let templateData
- if (resData.data.documents.length !== 0) {
- templateData = JSON.parse(resData.data.documents[0].data || '[]')
- } else {
- const templateDataRes = await getDocumentInfo({id: template.id})
- if (templateDataRes.status !== 200) {
- this.$message.error('获取文档信息失败')
- return
- }
- templateData = JSON.parse(templateDataRes.data.data || '[]')
- }
- if (!templateData || !Array.isArray(templateData)) {
- this.$message.error('无效的模板数据格式')
- return
- }
- // 创建一个临时的容器来存放内容
- const contentContainer = document.createElement('div')
- contentContainer.className = 'content'
- // 处理每个模版段落
- templateData.forEach((item) => {
- const templateSection = document.createElement('div')
- templateSection.className = 'template-textarea'
- templateSection.innerHTML = item.content
- contentContainer.appendChild(templateSection)
- })
- // 处理所有输入框
- contentContainer
- .querySelectorAll('input[type="text"]')
- .forEach((input) => {
- const span = document.createElement('span')
- span.textContent = input.value
- input.parentNode.replaceChild(span, input)
- })
- // 构建HTML内容
- const contentData = `<!DOCTYPE html>
- <html>
- <head>
- <meta charset="UTF-8">
- <style>
- @page {
- size: A4;
- margin: 2cm;
- }
- body {
- font-family: Arial, sans-serif;
- font-size: 12pt;
- line-height: 1.5;
- }
- /* 添加图片控制样式 */
- img {
- max-width: 100%;
- max-height: 100vh;
- object-fit: contain;
- page-break-inside: avoid;
- }
- /* 图片容器样式 */
- .image-container {
- width: 100%;
- text-align: center;
- margin: 10px 0;
- page-break-inside: avoid;
- }
- table {
- width: 100%;
- border-collapse: collapse;
- }
- td, th {
- border: 1px solid black;
- padding: 5px;
- }
- .template-textarea {
- page-break-after: always;
- }
- .TOC {
- display: none !important;
- }
- </style>
- </head>
- <body>
- ${this.processImages(contentContainer.innerHTML)}
- </body>
- </html>`
- /* // 处理表格样式
- contentData = contentData
- .replaceAll("<table ", `<table style="border-collapse: collapse;" `)
- .replaceAll("<td>", `<td style="border: 1px solid black;">`); */
- // 调用导出接口,指定导出类型为 PDF
- const res = await exportToPDF({
- content: contentData,
- title: template.title
- })
- if (res.status !== 200) {
- this.$alert(res.errMsg)
- return
- }
- // 处理文件下载
- const link = document.createElement('a')
- link.href = res.data.file_path
- link.download = res.data.file_name
- document.body.appendChild(link)
- link.click()
- document.body.removeChild(link)
- this.$message.success('导出成功')
- return res.data // 返回结果,包含 file_path
- } catch (error) {
- console.error('导出PDF失败:', error)
- this.$alert('导出PDF时发生错误,请稍后重试。')
- }
- },
- // 添加处理图片的辅助方法
- processImages(content) {
- const tempDiv = document.createElement('div')
- tempDiv.innerHTML = content
- // 处理所有图片
- tempDiv.querySelectorAll('img').forEach((img) => {
- // 创建图片容器
- const container = document.createElement('div')
- container.className = 'image-container'
- // 获取所有可能的尺寸来源
- let width =
- img.getAttribute('width') || img.style.width || img.width || null
- let height =
- img.getAttribute('height') || img.style.height || img.height || null
- // 移除单位并转换为数字
- if (typeof width === 'string') {
- width = parseInt(width.replace(/[^0-9]/g, ''))
- }
- if (typeof height === 'string') {
- height = parseInt(height.replace(/[^0-9]/g, ''))
- }
- // 如果没有尺寸,创建一个临时图片来获取实际尺寸
- if (!width || !height) {
- const tempImg = new Image()
- tempImg.src = img.src
- width = tempImg.width || 595 // 默认宽度
- height = tempImg.height || Math.round(width * 0.75) // 默认高度比例
- }
- // 确保数值有效
- width = Math.max(1, parseInt(width))
- height = Math.max(1, parseInt(height))
- // 处理尺寸
- const maxWidth = 595 // A4纸宽度(像素,减去边距)
- if (width > maxWidth) {
- // 等比例缩放
- const aspectRatio = width / height
- const newHeight = Math.round(maxWidth / aspectRatio)
- width = maxWidth
- height = newHeight
- }
- // 同时设置style和属性
- img.style.width = `${width}px`
- img.style.height = `${height}px`
- img.setAttribute('width', width)
- img.setAttribute('height', height)
- // 将图片包装在容器中
- img.parentNode.insertBefore(container, img)
- container.appendChild(img)
- })
- return tempDiv.innerHTML
- },
- // 添加一个方法来显示选中数量
- getSelectedCount() {
- return this.selectedTemplateIds.size
- },
- // 添加处理每页条数变化的方法
- handleSizeChange(val) {
- // 保存当前的选择状态
- const currentSelection = this.$refs.templateTable?.selection || []
- currentSelection.forEach((row) => {
- this.selectedTemplateIds.add(row.id)
- })
- // 更新每页显示条数
- this.templateQueryForm.pageSize = val
- // 重置为第一页
- this.templateQueryForm.page = 1
- // 重新加载数据
- this.loadTemplateList()
- },
- // 处理文档类型变化
- handleDocTypeChange(value) {
- this.templateQueryForm.document_type = value
- this.templateQueryForm.page = 1 // 重置页码
- this.loadTemplateList() // 重新加载列表
- },
- // 新增方法:异步加载完成度
- async loadProjectCompletions(projects) {
- // 使用 Promise.all 和 chunk 分批处理
- const chunkSize = 5 // 每批处理5个项目
- const chunks = this.chunkArray(projects, chunkSize)
- for (const chunk of chunks) {
- const promises = chunk.map(async(project) => {
- try {
- const calcRes = await calculate({ id: project.id })
- if (calcRes.status === 200) {
- // 使用 Vue.set 更新单个项目的完成度
- const index = this.dataList.findIndex(
- (item) => item.id === project.id
- )
- if (index !== -1) {
- this.$set(
- this.dataList[index],
- 'calculate',
- calcRes.data.completionRate
- )
- }
- }
- } catch (err) {
- console.error(`获取项目${project.id}完成度失败:`, err)
- }
- })
- await Promise.all(promises)
- }
- },
- // 辅助方法:将数组分成小块
- chunkArray(array, size) {
- const chunks = []
- for (let i = 0; i < array.length; i += size) {
- chunks.push(array.slice(i, i + size))
- }
- return chunks
- },
- // 生成目录的方法
- generateTableOfContents(templateData) {
- let toc = ''
- let currentPage = 1
- // 定义章节标题数组
- const sections = [
- '1 PURPOSE OF THE RISK MANAGEMENT REVIEW',
- '2 SCOPE OF APPLICATION',
- '3 RISK MANAGEMENT PROCESS',
- '4 REFERENCES',
- '5 RISK MANAGEMENT REVIEW PERSONNEL',
- '6 RISK ACCEPTABILITY CRITERIA',
- '7 RISK MANAGEMENT REVIEW',
- '8 PRODUCTION AND POST-PRODUCTION ACTIVITIES',
- '9 CONCLUSIONS OF RISK MANAGEMENT REVIEW',
- '10 THE FILE REVIEW COMPLETED',
- 'ANNEX A: QUESTION LIST USED TO IDENTIFY MEDICAL DEVICE SAFETY CHARACTERISTICS',
- 'ANNEX B: IDENTIFICATION OF KNOWN OR FORESEEABLE HAZARDS',
- 'ANNEX C: DETERMINATION OF SOFTWARE SAFETY CLASS',
- 'ANNEX D: RISK ANALYSIS AND CONTROL MEASURES',
- 'ANNEX E: TRACEABILITY'
- ]
- sections.forEach((section) => {
- toc += `
- <div class="toc-item">
- <span class="toc-text">${section}</span>
- <span class="toc-page">${currentPage}</span>
- </div>
- `
- currentPage++ // 简单估算每个章节占一页
- })
- return toc
- },
- // 处理正文内容的方法
- processContentWithSections(templateData) {
- let processedContent = ''
- let currentPage = 1
- templateData.forEach((item, index) => {
- // 添加章节标题
- processedContent += `
- <div class="section-title">
- ${this.getSectionTitle(index + 1)}
- </div>
- <div class="section-content">
- ${item.content}
- </div>
- <div class="page-number">${currentPage}</div>
- `
- currentPage++
- })
- return processedContent
- },
- // 获取章节标题的辅助方法
- getSectionTitle(index) {
- const titles = {
- 1: '1 PURPOSE OF THE RISK MANAGEMENT REVIEW',
- 2: '2 SCOPE OF APPLICATION',
- 3: '3 RISK MANAGEMENT PROCESS',
- 4: '4 REFERENCES',
- 5: '5 RISK MANAGEMENT REVIEW PERSONNEL',
- 6: '6 RISK ACCEPTABILITY CRITERIA',
- 7: '7 RISK MANAGEMENT REVIEW',
- 8: '8 PRODUCTION AND POST-PRODUCTION ACTIVITIES',
- 9: '9 CONCLUSIONS OF RISK MANAGEMENT REVIEW',
- 10: '10 THE FILE REVIEW COMPLETED',
- 11: 'ANNEX A: QUESTION LIST USED TO IDENTIFY MEDICAL DEVICE SAFETY CHARACTERISTICS',
- 12: 'ANNEX B: IDENTIFICATION OF KNOWN OR FORESEEABLE HAZARDS',
- 13: 'ANNEX C: DETERMINATION OF SOFTWARE SAFETY CLASS',
- 14: 'ANNEX D: RISK ANALYSIS AND CONTROL MEASURES',
- 15: 'ANNEX E: TRACEABILITY'
- }
- return titles[index] || `${index} SECTION`
- },
- // 估算页数的方法
- estimatePageCount(content) {
- const tempDiv = document.createElement('div')
- tempDiv.style.cssText = `
- width: 595px; /* A4 宽度 */
- font-size: 12pt;
- line-height: 1.5;
- visibility: hidden;
- position: absolute;
- `
- tempDiv.innerHTML = content
- document.body.appendChild(tempDiv)
- const contentHeight = tempDiv.offsetHeight
- const pageHeight = 842 // A4 高度
- document.body.removeChild(tempDiv)
- return Math.ceil(contentHeight / pageHeight)
- },
- // 加载文档版本列表
- async loadDocumentVersions(template) {
- this.documentVersionsLoading = true
- this.documentSelectVisible = true
- try {
- const res = await searchlistDoc({
- page: 1,
- pageSize: 999,
- template_id: template.id
- })
- if (res.status === 200) {
- this.documentVersions = res.data.dataList
- if (this.documentVersions.length === 0) {
- this.$message.warning('未找到相关文档版本')
- }
- }
- } catch (error) {
- console.error('加载文档版本失败:', error)
- this.$message.error('加载文档版本失败')
- } finally {
- this.documentVersionsLoading = false
- }
- },
- // 处理文档预览
- handleDocPreview(doc) {
- const url = `#/document/viewCreate?articleId=${doc.id}&type=document`
- window.open(url, '_blank')
- this.documentSelectVisible = false
- },
- // 处理文档导出
- async handleDocExport(command, doc) {
- try {
- switch (command) {
- case 'word':
- // 获取完整的文档信息
- const docInfo = await getDocumentInfo({ id: doc.id })
- if (docInfo.status === 200 && docInfo.data) {
- // 使用 exportDocumentWithHtmlDocx 方法导出
- await this.exportDocumentWithHtmlDocx(docInfo.data)
- } else {
- throw new Error('获取文档信息失败')
- }
- break
- case 'pdf':
- // 保持原有的 PDF 导出功能不变
- const res = await exportToPDF({
- content: doc.content,
- title: doc.title
- })
- if (res.status === 200) {
- const link = document.createElement('a')
- link.href = res.data.file_path
- link.download = `${doc.title}.pdf`
- document.body.appendChild(link)
- link.click()
- document.body.removeChild(link)
- } else {
- throw new Error(res.message || '导出失败')
- }
- break
- }
- this.$message.success('导出成功')
- } catch (error) {
- console.error('导出失败:', error)
- this.$message.error(error.message || '导出失败')
- }
- this.documentSelectVisible = false
- }
- }
- }
- </script>
- <style lang="scss" scoped>
- @import "./dataList.scss";
- .match-results {
- margin-top: 20px;
- .match-item {
- padding: 10px;
- border-bottom: 1px solid #eee;
- display: flex;
- justify-content: space-between;
- align-items: center;
- .match-content {
- flex: 1;
- display: flex;
- gap: 20px;
- }
- .actions {
- margin-left: 10px;
- }
- }
- }
- // 完成度文字样式
- .completion-high {
- color: #67c23a;
- font-weight: bold;
- }
- .completion-medium {
- color: #e6a23c;
- font-weight: bold;
- }
- .completion-low {
- color: #f56c6c;
- font-weight: bold;
- }
- // 添加PDF相关样式
- .content {
- background: white;
- .template-textarea {
- page-break-after: always;
- table {
- width: 100%;
- border-collapse: collapse;
- td,
- th {
- border: 1px solid black;
- padding: 5px;
- }
- }
- }
- }
- .header_t {
- margin: 10px 0;
- }
- /* 添加分页样式 */
- .pagination-container {
- margin-top: 15px;
- padding: 10px 0;
- text-align: right;
- }
- .filter-section {
- display: flex;
- align-items: center;
- gap: 10px;
- padding: 0 20px;
- }
- /* 添加到已有的 style 标签中 */
- .dialog-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 0 20px;
- }
- .header-left {
- display: flex;
- gap: 10px;
- }
- .header-right {
- display: flex;
- align-items: center;
- }
- </style>
|