Index.vue 84 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590
  1. <template>
  2. <div class="documents-container">
  3. <div class="header-section">
  4. <div class="title-info">
  5. <h2>{{ currentTitle }}</h2>
  6. <div class="statistics-bar">
  7. <span class="stat-item">
  8. <el-icon><Document /></el-icon>
  9. 全部数据: <span class="stat-value">{{ statistics.allTotal }}</span>
  10. </span>
  11. <span class="stat-item">
  12. <el-icon><CircleCheck /></el-icon>
  13. 已入库: <span class="stat-value success">{{ statistics.totalEntered }}</span>
  14. </span>
  15. <span class="stat-item" v-if="searchQuery.keyword || searchQuery.table_type || searchQuery.whether_to_enter !== null">
  16. <el-icon><Search /></el-icon>
  17. 检索结果: <span class="stat-value">{{ total }}</span>
  18. </span>
  19. </div>
  20. </div>
  21. <div class="action-buttons">
  22. <el-button type="danger" :disabled="selectedIds.length === 0" @click="handleBatchDelete">
  23. <el-icon><Delete /></el-icon> 批量删除
  24. </el-button>
  25. <el-button type="info" :disabled="selectedIds.length === 0" @click="handleBatchClear">
  26. <el-icon><Refresh /></el-icon> 清空数据
  27. </el-button>
  28. <el-button type="warning" :disabled="selectedIds.length === 0" @click="handleBatchEnter">
  29. <el-icon><CircleCheck /></el-icon> 批量入库
  30. </el-button>
  31. <el-button type="primary" :disabled="selectedIds.length === 0" @click="handleBatchAddToTask">
  32. <el-icon><Tickets /></el-icon> 批量加入标注任务
  33. </el-button>
  34. <el-button type="success" class="upload-btn" @click="handleUpload">
  35. <el-icon><Upload /></el-icon> 上传文档
  36. </el-button>
  37. </div>
  38. </div>
  39. <!-- 板块切换 Tabs 移除,改为合并视图 -->
  40. <el-card class="search-card">
  41. <div class="search-bar">
  42. <el-input
  43. v-model="searchQuery.keyword"
  44. placeholder="搜索文档名称..."
  45. class="search-input"
  46. clearable
  47. @keyup.enter="handleSearch"
  48. >
  49. <template #prefix>
  50. <el-icon><Search /></el-icon>
  51. </template>
  52. </el-input>
  53. <div class="filter-group">
  54. <el-select v-model="searchQuery.table_type" placeholder="基本信息类型" clearable class="filter-select">
  55. <el-option label="全部类型" :value="null" />
  56. <el-option label="施工标准规范" value="standard" />
  57. <el-option label="施工方案" value="construction_plan" />
  58. <el-option label="办公制度" value="regulation" />
  59. <el-option label="其他文档" value="other" />
  60. </el-select>
  61. <el-select v-model="searchQuery.whether_to_enter" placeholder="入库状态" clearable class="filter-select">
  62. <el-option label="全部入库状态" :value="null" />
  63. <el-option label="未入库" :value="0" />
  64. <el-option label="已入库" :value="1" />
  65. </el-select>
  66. <el-select v-model="searchQuery.conversion_status" placeholder="解析状态" clearable class="filter-select">
  67. <el-option label="全部解析状态" :value="null" />
  68. <el-option label="未转换" :value="0" />
  69. <el-option label="转换中" :value="1" />
  70. <el-option label="转换成功" :value="2" />
  71. <el-option label="转换失败" :value="3" />
  72. </el-select>
  73. <el-button type="primary" @click="handleSearch" class="search-btn">
  74. <el-icon><Filter /></el-icon> 检索
  75. </el-button>
  76. </div>
  77. </div>
  78. </el-card>
  79. <div class="content-section" v-loading="loading">
  80. <el-empty v-if="documents.length === 0" description="暂无文档数据" />
  81. <el-table
  82. v-else
  83. ref="multipleTableRef"
  84. :data="documents"
  85. row-key="id"
  86. style="width: 100%"
  87. border
  88. stripe
  89. size="small"
  90. @selection-change="handleSelectionChange"
  91. :row-class-name="tableRowClassName"
  92. >
  93. <el-table-column type="selection" width="40" :selectable="canSelect" :reserve-selection="true" />
  94. <el-table-column prop="title" label="文档名称" min-width="280" show-overflow-tooltip>
  95. <template #default="scope">
  96. <div class="file-info-cell">
  97. <div class="file-icon-mini" :class="getFileIconClass(scope.row)">
  98. <el-icon v-if="getFileIcon(scope.row) === 'pdf'"><Document /></el-icon>
  99. <el-icon v-else-if="getFileIcon(scope.row) === 'word'"><Document /></el-icon>
  100. <el-icon v-else-if="getFileIcon(scope.row) === 'excel'"><Grid /></el-icon>
  101. <el-icon v-else-if="getFileIcon(scope.row) === 'ppt'"><DataAnalysis /></el-icon>
  102. <el-icon v-else><Document /></el-icon>
  103. </div>
  104. <div class="file-info-content">
  105. <span class="file-name-link" @click="handleView(scope.row)">
  106. {{ scope.row.title }}{{ getFileExtension(scope.row) }}
  107. </span>
  108. <span v-if="scope.row.note" class="file-note">
  109. {{ scope.row.note }}
  110. </span>
  111. </div>
  112. </div>
  113. </template>
  114. </el-table-column>
  115. <el-table-column label="转换状态" min-width="180">
  116. <template #default="scope">
  117. <div class="conversion-cell">
  118. <el-tooltip
  119. :content="scope.row.conversion_error"
  120. placement="top"
  121. :disabled="scope.row.conversion_status !== 3 || !scope.row.conversion_error"
  122. >
  123. <el-tag
  124. :type="getConversionStatusTag(scope.row)"
  125. size="small"
  126. effect="light"
  127. class="status-tag"
  128. >
  129. <el-icon v-if="scope.row.conversion_status === 3" style="vertical-align: middle; margin-right: 4px;">
  130. <Warning />
  131. </el-icon>
  132. {{ getConversionStatusText(scope.row) }}
  133. </el-tag>
  134. </el-tooltip>
  135. <div class="converted-file-links" v-if="scope.row.conversion_status === 2">
  136. <div class="converted-file-name" v-if="scope.row.md_url">
  137. <el-link type="primary" :underline="false" @click="handleDownloadConverted(scope.row)">
  138. <el-icon><Link /></el-icon> {{ scope.row.md_display_name || '下载 Markdown' }}
  139. </el-link>
  140. </div>
  141. <div class="converted-file-name" v-if="scope.row.json_url">
  142. <el-link type="success" :underline="false" @click="handleDownloadJson(scope.row)">
  143. <el-icon><Link /></el-icon> {{ scope.row.json_display_name || '下载 JSON' }}
  144. </el-link>
  145. </div>
  146. </div>
  147. </div>
  148. </template>
  149. </el-table-column>
  150. <el-table-column label="基本信息类型" min-width="120">
  151. <template #default="scope">
  152. <el-tag size="small" effect="plain" :type="getKBTagType(scope.row.source_type)">
  153. {{ getSourceTypeShortName(scope.row.source_type) }}
  154. </el-tag>
  155. </template>
  156. </el-table-column>
  157. <el-table-column label="所属知识库" min-width="150" show-overflow-tooltip>
  158. <template #default="scope">
  159. <span v-if="scope.row.kb_name" class="kb-name-tag">
  160. <el-icon><Tickets /></el-icon> {{ scope.row.kb_name }}
  161. </span>
  162. <span v-else class="text-info">未入库</span>
  163. </template>
  164. </el-table-column>
  165. <el-table-column prop="whether_to_enter" label="入库" width="80" align="center">
  166. <template #default="scope">
  167. <el-tooltip :content="isEntered(scope.row.whether_to_enter) ? '已入库' : '未入库'" placement="top">
  168. <el-icon :class="isEntered(scope.row.whether_to_enter) ? 'status-icon-success' : 'status-icon-info'">
  169. <CircleCheck v-if="isEntered(scope.row.whether_to_enter)" />
  170. <Warning v-else />
  171. </el-icon>
  172. </el-tooltip>
  173. </template>
  174. </el-table-column>
  175. <el-table-column prop="creator_name" label="上传人" min-width="100" show-overflow-tooltip />
  176. <el-table-column label="上传时间" min-width="150" prop="created_time">
  177. <template #default="scope">
  178. {{ formatDate(scope.row.created_time) }}
  179. </template>
  180. </el-table-column>
  181. <el-table-column prop="updater_name" label="修改人" min-width="100" show-overflow-tooltip />
  182. <el-table-column label="修改时间" min-width="150" prop="updated_time">
  183. <template #default="scope">
  184. {{ formatDate(scope.row.updated_time) }}
  185. </template>
  186. </el-table-column>
  187. <el-table-column label="操作" width="260" fixed="right" align="center">
  188. <template #default="scope">
  189. <div class="action-buttons">
  190. <el-tooltip content="编辑" placement="top">
  191. <el-button
  192. link
  193. type="primary"
  194. @click="handleEdit(scope.row)"
  195. class="action-btn-icon"
  196. >
  197. <el-icon><Edit /></el-icon>
  198. </el-button>
  199. </el-tooltip>
  200. <el-tooltip content="下载" placement="top" v-if="scope.row.file_url">
  201. <el-button
  202. link
  203. type="success"
  204. @click="handleDownload(scope.row)"
  205. class="action-btn-icon"
  206. >
  207. <el-icon><Download /></el-icon>
  208. </el-button>
  209. </el-tooltip>
  210. <el-tooltip
  211. :content="scope.row.conversion_status === 1 ? '转换中' : (scope.row.conversion_status === 2 ? '重新转换' : '开始转换')"
  212. placement="top"
  213. >
  214. <el-button
  215. link
  216. type="warning"
  217. @click="handleConvert(scope.row)"
  218. :disabled="scope.row.conversion_status === 1"
  219. class="action-btn-icon"
  220. >
  221. <el-icon><Switch /></el-icon>
  222. </el-button>
  223. </el-tooltip>
  224. <el-tooltip content="删除" placement="top">
  225. <el-button
  226. link
  227. type="danger"
  228. @click="handleDelete(scope.row)"
  229. class="action-btn-icon"
  230. >
  231. <el-icon><Delete /></el-icon>
  232. </el-button>
  233. </el-tooltip>
  234. <el-tooltip content="入库" placement="top">
  235. <el-button
  236. link
  237. type="warning"
  238. @click="handleSingleEnter(scope.row)"
  239. :disabled="isEntered(scope.row.whether_to_enter)"
  240. class="action-btn-icon"
  241. >
  242. <el-icon><CircleCheck /></el-icon>
  243. </el-button>
  244. </el-tooltip>
  245. </div>
  246. </template>
  247. </el-table-column>
  248. </el-table>
  249. <!-- 文档预览对话框 -->
  250. <el-dialog v-model="previewVisible" :title="previewTitle" width="85%" top="5vh" custom-class="preview-dialog" @closed="previewUrl = ''">
  251. <template #header>
  252. <div class="dialog-header-custom">
  253. <span>{{ previewTitle }}</span>
  254. <div class="header-actions">
  255. <el-button type="primary" link @click="openInNewWindow">
  256. <el-icon><TopRight /></el-icon>在新窗口打开原链接
  257. </el-button>
  258. </div>
  259. </div>
  260. </template>
  261. <div v-loading="previewLoading" class="preview-content">
  262. <!-- Office 文档未转换提示 -->
  263. <div v-if="isOfficeDoc && previewDocType === 'original' && !previewLoading" class="unsupported-preview">
  264. <el-result
  265. icon="warning"
  266. title="该格式暂不支持直接预览"
  267. sub-title="Word/Excel/PPT 等 Office 文档需要转换后才能在浏览器中直接预览。"
  268. >
  269. <template #extra>
  270. <div class="unsupported-actions">
  271. <el-button type="primary" @click="handleConvert(currentDoc!)">
  272. <el-icon><Switch /></el-icon> 立即转换
  273. </el-button>
  274. <el-button type="success" @click="handleDownload(currentDoc!)">
  275. <el-icon><Download /></el-icon> 下载原文档
  276. </el-button>
  277. </div>
  278. </template>
  279. </el-result>
  280. </div>
  281. <div v-if="isHtmlPage && !previewLoading && !isOfficeDoc" class="preview-tip">
  282. <el-alert
  283. title="网页预览提示"
  284. type="info"
  285. description="由于外部网站的安全限制,某些网页可能无法在此处正常预览。如果显示异常,请点击右上角按钮在新窗口中查看。"
  286. show-icon
  287. :closable="false"
  288. />
  289. </div>
  290. <iframe
  291. v-if="previewUrl && !(isOfficeDoc && previewDocType === 'original')"
  292. :src="proxyPreviewUrl"
  293. width="100%"
  294. height="100%"
  295. frameborder="0"
  296. allow="fullscreen"
  297. @load="previewLoading = false"
  298. ></iframe>
  299. </div>
  300. </el-dialog>
  301. <!-- 入库设置弹窗 -->
  302. <el-dialog v-model="ingestDialogVisible" title="入库设置" width="400px">
  303. <el-form :model="ingestForm" :rules="ingestRules" ref="ingestFormRef" label-width="100px">
  304. <el-form-item label="切分方式" prop="kb_method">
  305. <el-select v-model="ingestForm.kb_method" placeholder="请选择切分方式" style="width: 100%">
  306. <el-option label="按长度切分" value="length" />
  307. <el-option label="按符号切分" value="symbol" />
  308. <el-option label="父子段切分" value="parent_child" />
  309. </el-select>
  310. </el-form-item>
  311. <el-form-item label="切分长度" v-if="ingestForm.kb_method === 'length'">
  312. <el-select v-model="ingestForm.chunk_size" placeholder="请选择切分长度" style="width: 100%">
  313. <el-option label="200" :value="200" />
  314. <el-option label="500" :value="500" />
  315. <el-option label="800" :value="800" />
  316. <el-option label="1000" :value="1000" />
  317. <el-option label="1200" :value="1200" />
  318. </el-select>
  319. </el-form-item>
  320. <el-form-item label="切分符号" v-if="ingestForm.kb_method === 'symbol'">
  321. <el-select v-model="ingestForm.separator" placeholder="请选择切分符号" style="width: 100%">
  322. <el-option label="句号 (。)" value="。" />
  323. <el-option label="换行符 (\n)" value="\n" />
  324. <el-option label="分号 (;)" value=";" />
  325. <el-option label="感叹号 (!)" value="!" />
  326. <el-option label="问号 (?)" value="?" />
  327. </el-select>
  328. </el-form-item>
  329. </el-form>
  330. <template #footer>
  331. <span class="dialog-footer">
  332. <el-button @click="ingestDialogVisible = false">取消</el-button>
  333. <el-button type="primary" @click="confirmIngest" :loading="ingesting">
  334. 确认入库
  335. </el-button>
  336. </span>
  337. </template>
  338. </el-dialog>
  339. <!-- 加入任务设置弹窗 -->
  340. <el-dialog v-model="taskDialogVisible" title="加入标注任务" width="450px">
  341. <el-form :model="taskForm" :rules="taskRules" ref="taskFormRef" label-width="100px">
  342. <el-form-item label="项目名称" prop="project_name">
  343. <el-input v-model="taskForm.project_name" placeholder="请输入项目名称" clearable />
  344. </el-form-item>
  345. <el-form-item label="任务标签" prop="tags">
  346. <div class="tag-group">
  347. <el-tag
  348. v-for="(tag, index) in taskForm.selectedTags"
  349. :key="tag.id"
  350. closable
  351. @close="handleRemoveTag(index)"
  352. type="info"
  353. class="tag-item"
  354. >
  355. {{ tag.name }}
  356. </el-tag>
  357. <el-cascader
  358. :key="cascaderKey"
  359. v-model="tempTagValue"
  360. :options="tagTree"
  361. :props="{ checkStrictly: true, emitPath: false }"
  362. placeholder="添加标签"
  363. @change="handleAddTag"
  364. filterable
  365. clearable
  366. class="tag-adder"
  367. >
  368. <template #default="{ data }">
  369. <span>{{ data.label }}</span>
  370. </template>
  371. </el-cascader>
  372. </div>
  373. </el-form-item>
  374. </el-form>
  375. <template #footer>
  376. <span class="dialog-footer">
  377. <el-button @click="taskDialogVisible = false">取消</el-button>
  378. <el-button type="primary" @click="confirmAddTask" :loading="taskAdding">
  379. 确认加入
  380. </el-button>
  381. </span>
  382. </template>
  383. </el-dialog>
  384. <div class="pagination-container" v-if="total > 0">
  385. <el-pagination
  386. v-model:current-page="searchQuery.page"
  387. v-model:page-size="searchQuery.size"
  388. :page-sizes="[10, 20, 50, 100]"
  389. layout="total, sizes, prev, pager, next, jumper"
  390. :total="total"
  391. @size-change="handleSizeChange"
  392. @current-change="handleCurrentChange"
  393. />
  394. </div>
  395. </div>
  396. <!-- 上传文档对话框 -->
  397. <el-dialog v-model="uploadDialogVisible" title="上传文档" width="500px">
  398. <el-form :model="uploadForm" :rules="commonRules" ref="uploadFormRef" label-width="120px" v-loading="uploadingFile" element-loading-text="正在上传文件到服务器...">
  399. <el-form-item label="基本信息类型" prop="table_type">
  400. <el-select v-model="uploadForm.table_type" placeholder="请选择基本信息类型">
  401. <el-option label="施工标准规范" value="standard" />
  402. <el-option label="施工方案" value="construction_plan" />
  403. <el-option label="办公制度" value="regulation" />
  404. <el-option label="其他文档" value="other" />
  405. </el-select>
  406. </el-form-item>
  407. <el-form-item label="目标知识库" prop="kb_id">
  408. <el-select v-model="uploadForm.kb_id" placeholder="请选择目标知识库">
  409. <el-option
  410. v-for="item in kbOptions"
  411. :key="item.value"
  412. :label="item.label"
  413. :value="item.value"
  414. />
  415. </el-select>
  416. </el-form-item>
  417. <el-form-item label="文档标题" prop="title">
  418. <el-input v-model="uploadForm.title" placeholder="请输入文档标题" />
  419. </el-form-item>
  420. <el-form-item label="文档链接" v-if="false">
  421. <el-input v-model="uploadForm.file_url" placeholder="请输入文档链接 (URL) 或通过下方上传文件" />
  422. </el-form-item>
  423. <el-form-item label="文件上传">
  424. <el-upload
  425. ref="uploadRef"
  426. class="upload-demo"
  427. action="#"
  428. :http-request="customUpload"
  429. :limit="1"
  430. :on-exceed="handleExceed"
  431. :before-upload="beforeUpload"
  432. >
  433. <el-button type="primary">点击上传</el-button>
  434. <template #tip>
  435. <div class="el-upload__tip">
  436. 支持 PDF, Word, Excel, PPT, TXT 等文件,最大 50MB
  437. </div>
  438. </template>
  439. </el-upload>
  440. </el-form-item>
  441. <el-form-item label="文档备注">
  442. <el-input v-model="uploadForm.note" type="textarea" :rows="4" placeholder="请输入文档备注" />
  443. </el-form-item>
  444. </el-form>
  445. <template #footer>
  446. <el-button @click="uploadDialogVisible = false" :disabled="uploadingFile">取消</el-button>
  447. <el-button type="primary" @click="submitUpload" :loading="submitting" :disabled="uploadingFile">确定</el-button>
  448. </template>
  449. </el-dialog>
  450. <!-- 编辑文档对话框 -->
  451. <el-dialog v-model="editDialogVisible" :title="formTitle" width="800px">
  452. <el-form :model="editForm" :rules="commonRules" ref="editFormRef" label-width="110px">
  453. <el-row :gutter="20">
  454. <el-col :span="12">
  455. <el-form-item label="目标知识库" prop="kb_id">
  456. <el-select v-model="editForm.kb_id" placeholder="请选择目标知识库" style="width: 100%" disabled>
  457. <el-option
  458. v-for="item in kbOptions"
  459. :key="item.value"
  460. :label="item.label"
  461. :value="item.value"
  462. />
  463. </el-select>
  464. </el-form-item>
  465. </el-col>
  466. <el-col :span="12">
  467. <el-form-item :label="titleLabel" prop="title">
  468. <el-input v-model="editForm.title" :placeholder="'请输入' + titleLabel" disabled />
  469. </el-form-item>
  470. </el-col>
  471. <el-col :span="12" v-if="editForm.table_type === 'standard'">
  472. <el-form-item label="英文名称">
  473. <el-input v-model="editForm.english_name" placeholder="请输入英文名称" />
  474. </el-form-item>
  475. </el-col>
  476. <el-col :span="12" v-if="editForm.table_type === 'standard'">
  477. <el-form-item label="* 标准编号">
  478. <el-input v-model="editForm.standard_no" placeholder="请输入标准编号" />
  479. </el-form-item>
  480. </el-col>
  481. <el-col :span="12">
  482. <el-form-item :label="'* ' + authorityLabel">
  483. <el-input v-model="editForm.issuing_authority" :placeholder="'请输入' + authorityLabel" />
  484. </el-form-item>
  485. </el-col>
  486. <el-col :span="12">
  487. <el-form-item :label="'* ' + dateLabel">
  488. <el-date-picker v-model="editForm.release_date" type="date" placeholder="选择日期" value-format="YYYY-MM-DD" style="width: 100%" />
  489. </el-form-item>
  490. </el-col>
  491. <el-col :span="12" v-if="editForm.table_type === 'standard'">
  492. <el-form-item label="* 实施日期">
  493. <el-date-picker v-model="editForm.implementation_date" type="date" placeholder="选择实施日期" value-format="YYYY-MM-DD" style="width: 100%" />
  494. </el-form-item>
  495. </el-col>
  496. <el-col :span="12" v-if="editForm.table_type === 'standard' || editForm.table_type === 'regulation'">
  497. <el-form-item :label="editForm.table_type === 'standard' ? '* 文件类型' : '文件类型'">
  498. <el-select v-model="editForm.document_type" placeholder="请选择文件类型" style="width: 100%">
  499. <el-option v-for="item in documentTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
  500. </el-select>
  501. </el-form-item>
  502. </el-col>
  503. <el-col :span="12" v-if="editForm.table_type === 'standard'">
  504. <el-form-item label="* 专业领域">
  505. <el-select v-model="editForm.professional_field" placeholder="请选择专业领域" style="width: 100%">
  506. <el-option v-for="item in professionalFieldOptions" :key="item.value" :label="item.label" :value="item.value" />
  507. </el-select>
  508. </el-form-item>
  509. </el-col>
  510. <el-col :span="12" v-if="editForm.table_type === 'standard'">
  511. <el-form-item label="* 时效性">
  512. <el-select v-model="editForm.validity" placeholder="请选择时效性" style="width: 100%">
  513. <el-option v-for="item in validityOptions" :key="item.value" :label="item.label" :value="item.value" />
  514. </el-select>
  515. </el-form-item>
  516. </el-col>
  517. <el-col :span="12" v-if="editForm.table_type === 'standard'">
  518. <el-form-item label="* 起草单位">
  519. <el-input v-model="editForm.drafting_unit" placeholder="请输入起草单位" />
  520. </el-form-item>
  521. </el-col>
  522. <el-col :span="12" v-if="editForm.table_type === 'standard'">
  523. <el-form-item label="* 批准部门">
  524. <el-input v-model="editForm.approving_department" placeholder="请输入批准部门" />
  525. </el-form-item>
  526. </el-col>
  527. <el-col :span="24" v-if="editForm.table_type === 'standard'">
  528. <el-form-item label="参编单位">
  529. <div v-for="(unit, index) in participatingUnitsList" :key="index" class="dynamic-input-row">
  530. <el-input v-model="participatingUnitsList[index]" placeholder="请输入参编单位" />
  531. <div class="row-actions">
  532. <el-button :icon="Plus" circle size="small" @click="addListItem(participatingUnitsList)" />
  533. <el-button :icon="Minus" circle size="small" type="danger" @click="removeListItem(participatingUnitsList, index)" v-if="participatingUnitsList.length > 1" />
  534. </div>
  535. </div>
  536. </el-form-item>
  537. </el-col>
  538. <el-col :span="12" v-if="editForm.table_type === 'standard'">
  539. <el-form-item label="工程阶段">
  540. <el-input v-model="editForm.engineering_phase" placeholder="请输入工程阶段" />
  541. </el-form-item>
  542. </el-col>
  543. <el-col :span="24" v-if="editForm.table_type === 'standard'">
  544. <el-form-item label="引用依据">
  545. <div v-for="(std, index) in referenceBasisList" :key="index" class="dynamic-input-row">
  546. <el-input v-model="referenceBasisList[index]" placeholder="请输入引用依据" />
  547. <div class="row-actions">
  548. <el-button :icon="Plus" circle size="small" @click="addListItem(referenceBasisList)" />
  549. <el-button :icon="Minus" circle size="small" type="danger" @click="removeListItem(referenceBasisList, index)" v-if="referenceBasisList.length > 1" />
  550. </div>
  551. </div>
  552. </el-form-item>
  553. </el-col>
  554. <el-col :span="12" v-if="editForm.table_type === 'standard'">
  555. <el-form-item label="来源链接">
  556. <el-input v-model="editForm.source_url" placeholder="请输入来源链接" />
  557. </el-form-item>
  558. </el-col>
  559. <template v-if="editForm.table_type === 'construction_plan'">
  560. <el-col :span="12">
  561. <el-form-item label="* 项目名称">
  562. <el-input v-model="editForm.project_name" placeholder="请输入项目名称" />
  563. </el-form-item>
  564. </el-col>
  565. <el-col :span="12">
  566. <el-form-item label="项目标段">
  567. <el-input v-model="editForm.project_section" placeholder="请输入项目标段" />
  568. </el-form-item>
  569. </el-col>
  570. <el-col :span="12">
  571. <el-form-item label="* 方案类别">
  572. <el-select v-model="editForm.plan_category" placeholder="请选择方案类别" style="width: 100%">
  573. <el-option v-for="item in planCategoryOptions" :key="item.value" :label="item.label" :value="item.value" />
  574. </el-select>
  575. </el-form-item>
  576. </el-col>
  577. <el-col :span="12">
  578. <el-form-item label="* 一级分类">
  579. <el-select v-model="editForm.level_1_classification" placeholder="请选择一级分类" style="width: 100%">
  580. <el-option v-for="item in level1Options" :key="item.value" :label="item.label" :value="item.value" />
  581. </el-select>
  582. </el-form-item>
  583. </el-col>
  584. <el-col :span="12">
  585. <el-form-item label="* 二级分类">
  586. <el-select v-model="editForm.level_2_classification" placeholder="请选择二级分类" style="width: 100%">
  587. <el-option v-for="item in level2Options" :key="item.value" :label="item.label" :value="item.value" />
  588. </el-select>
  589. </el-form-item>
  590. </el-col>
  591. <el-col :span="12">
  592. <el-form-item label="* 三级分类">
  593. <el-select v-model="editForm.level_3_classification" placeholder="请选择三级分类" style="width: 100%">
  594. <el-option v-for="item in level3Options" :key="item.value" :label="item.label" :value="item.value" />
  595. </el-select>
  596. </el-form-item>
  597. </el-col>
  598. <el-col :span="12">
  599. <el-form-item label="* 四级分类">
  600. <el-select v-model="editForm.level_4_classification" placeholder="请选择四级分类" style="width: 100%">
  601. <el-option v-for="item in level4Options" :key="item.value" :label="item.label" :value="item.value" />
  602. </el-select>
  603. </el-form-item>
  604. </el-col>
  605. <el-col :span="24">
  606. <el-form-item label="* 方案摘要">
  607. <el-input v-model="editForm.plan_summary" type="textarea" :rows="3" placeholder="请输入方案摘要" />
  608. </el-form-item>
  609. </el-col>
  610. <el-col :span="24">
  611. <el-form-item label="编制依据">
  612. <div v-for="(planStd, index) in compilationBasisList" :key="index" class="dynamic-input-row">
  613. <el-input v-model="compilationBasisList[index]" placeholder="请输入编制依据" />
  614. <div class="row-actions">
  615. <el-button :icon="Plus" circle size="small" @click="addListItem(compilationBasisList)" />
  616. <el-button :icon="Minus" circle size="small" type="danger" @click="removeListItem(compilationBasisList, index)" v-if="compilationBasisList.length > 1" />
  617. </div>
  618. </div>
  619. </el-form-item>
  620. </el-col>
  621. </template>
  622. <template v-if="editForm.table_type === 'regulation'">
  623. <el-col :span="12">
  624. <el-form-item label="生效日期">
  625. <el-date-picker v-model="editForm.effective_start_date" type="date" placeholder="选择生效日期" value-format="YYYY-MM-DD" style="width: 100%" />
  626. </el-form-item>
  627. </el-col>
  628. <el-col :span="12">
  629. <el-form-item label="失效日期">
  630. <el-date-picker v-model="editForm.effective_end_date" type="date" placeholder="选择失效日期" value-format="YYYY-MM-DD" style="width: 100%" />
  631. </el-form-item>
  632. </el-col>
  633. </template>
  634. <el-col :span="24">
  635. <el-form-item label="备注">
  636. <el-input v-model="editForm.note" type="textarea" :rows="2" placeholder="请输入备注" />
  637. </el-form-item>
  638. </el-col>
  639. <el-col :span="24">
  640. <el-form-item label="上传文件">
  641. <div class="file-info-edit">
  642. <el-tag type="info" size="large" class="file-tag">
  643. <el-icon><Document /></el-icon>
  644. <span>文件已锁定(编辑模式不可更改)</span>
  645. </el-tag>
  646. </div>
  647. </el-form-item>
  648. </el-col>
  649. </el-row>
  650. </el-form>
  651. <template #footer>
  652. <el-button @click="editDialogVisible = false">取消</el-button>
  653. <el-button type="primary" @click="submitEdit" :loading="submitting">确定</el-button>
  654. </template>
  655. </el-dialog>
  656. <!-- 文档详情对话框 -->
  657. <el-dialog v-model="detailDialogVisible" title="文档详情" width="600px">
  658. <el-descriptions :column="1" border>
  659. <el-descriptions-item label="名称">{{ currentDoc?.title }}</el-descriptions-item>
  660. <el-descriptions-item label="基本信息类型">{{ getSourceTypeName(currentDoc?.source_type) }}</el-descriptions-item>
  661. <el-descriptions-item label="上传人">{{ currentDoc?.creator_name || '-' }}</el-descriptions-item>
  662. <el-descriptions-item label="上传时间">{{ formatDate(currentDoc?.created_time) }}</el-descriptions-item>
  663. <el-descriptions-item label="最后修改人" v-if="currentDoc?.updater_name">{{ currentDoc?.updater_name }}</el-descriptions-item>
  664. <el-descriptions-item label="修改时间" v-if="currentDoc?.updated_time">{{ formatDate(currentDoc?.updated_time) }}</el-descriptions-item>
  665. <el-descriptions-item label="入库状态">
  666. <el-tag :type="isEntered(currentDoc?.whether_to_enter) ? 'success' : 'info'">
  667. {{ isEntered(currentDoc?.whether_to_enter) ? '已入库' : '未入库' }}
  668. </el-tag>
  669. </el-descriptions-item>
  670. <el-descriptions-item label="所属知识库" v-if="currentDoc?.kb_name">
  671. <span class="kb-name-tag">
  672. <el-icon><Tickets /></el-icon> {{ currentDoc.kb_name }}
  673. </span>
  674. </el-descriptions-item>
  675. <el-descriptions-item label="文档备注">
  676. <div class="content-preview">{{ currentDoc?.note || '暂无备注' }}</div>
  677. </el-descriptions-item>
  678. <!-- 施工标准规范特有详情 -->
  679. <template v-if="currentDoc?.source_type === 'standard'">
  680. <el-descriptions-item label="标准编号">{{ currentDoc?.standard_no || '-' }}</el-descriptions-item>
  681. <el-descriptions-item label="英文名称">{{ currentDoc?.english_name || '-' }}</el-descriptions-item>
  682. <el-descriptions-item label="发布单位">{{ currentDoc?.issuing_authority || '-' }}</el-descriptions-item>
  683. <el-descriptions-item label="发布日期">{{ formatDate(currentDoc?.release_date) }}</el-descriptions-item>
  684. <el-descriptions-item label="实施日期">{{ formatDate(currentDoc?.implementation_date) }}</el-descriptions-item>
  685. <el-descriptions-item label="起草单位">{{ currentDoc?.drafting_unit || '-' }}</el-descriptions-item>
  686. <el-descriptions-item label="批准部门">{{ currentDoc?.approving_department || '-' }}</el-descriptions-item>
  687. <el-descriptions-item label="文件类型">{{ currentDoc?.document_type || '-' }}</el-descriptions-item>
  688. <el-descriptions-item label="专业领域">{{ currentDoc?.professional_field || '-' }}</el-descriptions-item>
  689. <el-descriptions-item label="时效性">{{ getValidityName(currentDoc?.validity) }}</el-descriptions-item>
  690. <el-descriptions-item label="参编单位">
  691. <div v-if="currentDoc?.participating_units">
  692. <div v-for="(unit, idx) in currentDoc.participating_units.split(';')" :key="idx">{{ unit }}</div>
  693. </div>
  694. <span v-else>-</span>
  695. </el-descriptions-item>
  696. <el-descriptions-item label="引用依据">
  697. <div v-if="currentDoc?.reference_basis">
  698. <div v-for="(std, idx) in currentDoc.reference_basis.split(';')" :key="idx">{{ std }}</div>
  699. </div>
  700. <span v-else>-</span>
  701. </el-descriptions-item>
  702. <el-descriptions-item label="来源 URL">{{ currentDoc?.source_url || '-' }}</el-descriptions-item>
  703. <el-descriptions-item label="工程阶段">{{ currentDoc?.engineering_phase || '-' }}</el-descriptions-item>
  704. </template>
  705. <!-- 施工方案特有详情 -->
  706. <template v-else-if="currentDoc?.source_type === 'construction_plan'">
  707. <el-descriptions-item label="工程名称">{{ currentDoc?.project_name || '-' }}</el-descriptions-item>
  708. <el-descriptions-item label="英文名称">{{ currentDoc?.english_name || '-' }}</el-descriptions-item>
  709. <el-descriptions-item label="工程标段">{{ currentDoc?.project_section || '-' }}</el-descriptions-item>
  710. <el-descriptions-item label="方案类别">{{ currentDoc?.plan_category || '-' }}</el-descriptions-item>
  711. <el-descriptions-item label="一级分类">{{ currentDoc?.level_1_classification || '-' }}</el-descriptions-item>
  712. <el-descriptions-item label="二级分类">{{ currentDoc?.level_2_classification || '-' }}</el-descriptions-item>
  713. <el-descriptions-item label="三级分类">{{ currentDoc?.level_3_classification || '-' }}</el-descriptions-item>
  714. <el-descriptions-item label="四级分类">{{ currentDoc?.level_4_classification || '-' }}</el-descriptions-item>
  715. <el-descriptions-item label="编制单位">{{ currentDoc?.issuing_authority || '-' }}</el-descriptions-item>
  716. <el-descriptions-item label="编制日期">{{ formatDate(currentDoc?.release_date) }}</el-descriptions-item>
  717. <el-descriptions-item label="方案摘要">
  718. <div class="content-preview">{{ currentDoc?.plan_summary || '-' }}</div>
  719. </el-descriptions-item>
  720. <el-descriptions-item label="编制依据">
  721. <div v-if="currentDoc?.compilation_basis">
  722. <div v-for="(planStd, idx) in currentDoc.compilation_basis.split(';')" :key="idx">{{ planStd }}</div>
  723. </div>
  724. <span v-else>-</span>
  725. </el-descriptions-item>
  726. </template>
  727. <!-- 办公制度特有详情 -->
  728. <template v-else-if="currentDoc?.source_type === 'regulation'">
  729. <el-descriptions-item label="发布部门">{{ currentDoc?.issuing_authority || '-' }}</el-descriptions-item>
  730. <el-descriptions-item label="英文名称">{{ currentDoc?.english_name || '-' }}</el-descriptions-item>
  731. <el-descriptions-item label="制度类型">{{ currentDoc?.document_type || '-' }}</el-descriptions-item>
  732. <el-descriptions-item label="发布日期">{{ formatDate(currentDoc?.release_date) }}</el-descriptions-item>
  733. <el-descriptions-item label="生效日期">{{ formatDate(currentDoc?.effective_start_date) }}</el-descriptions-item>
  734. <el-descriptions-item label="失效日期">{{ formatDate(currentDoc?.effective_end_date) }}</el-descriptions-item>
  735. </template>
  736. </el-descriptions>
  737. <template #footer>
  738. <div class="detail-footer">
  739. <div class="download-group" v-if="currentDoc">
  740. <el-button type="success" plain size="small" @click="handleDownload(currentDoc)" v-if="currentDoc.file_url">
  741. <el-icon><Download /></el-icon> 下载原文件
  742. </el-button>
  743. <el-button type="primary" plain size="small" @click="handleDownloadConverted(currentDoc)" v-if="currentDoc.md_url">
  744. <el-icon><Download /></el-icon> 下载 MD
  745. </el-button>
  746. <el-button type="warning" plain size="small" @click="handleDownloadJson(currentDoc)" v-if="currentDoc.json_url">
  747. <el-icon><Download /></el-icon> 下载 JSON
  748. </el-button>
  749. </div>
  750. <div class="action-group">
  751. <el-button @click="detailDialogVisible = false">关闭</el-button>
  752. <el-button type="primary" @click="handleEditFromDetail" v-if="currentDoc">
  753. <el-icon><Edit /></el-icon> 编辑文档
  754. </el-button>
  755. <el-button type="success" @click="handleSingleEnter(currentDoc)" v-if="currentDoc && !isEntered(currentDoc.whether_to_enter)">文档入库</el-button>
  756. <el-button type="primary" @click="handlePreview(currentDoc)" v-if="currentDoc?.file_url">预览原文档</el-button>
  757. </div>
  758. </div>
  759. </template>
  760. </el-dialog>
  761. </div>
  762. </template>
  763. <script setup lang="ts">
  764. import { ref, reactive, onMounted, onUnmounted, computed } from 'vue'
  765. import { ElMessage, ElMessageBox } from 'element-plus'
  766. import { Search, Filter, Upload, CircleCheck, Delete, Document, Warning, TopRight, Grid, DataAnalysis, Link, View, Switch, Edit, User, Download, Plus, Minus, Tickets, Refresh } from '@element-plus/icons-vue'
  767. import request from '@/api/request'
  768. import axios from 'axios'
  769. import { downloadFile } from '@/utils/download'
  770. import { getFileExtension } from '@/utils/file'
  771. import { useAuthStore } from '@/stores/auth'
  772. import dayjs from 'dayjs'
  773. import type { ApiResponse } from '@/types/auth'
  774. import { documentApi, type DocumentItem } from '@/api/document'
  775. import { getKnowledgeBases } from '@/api/knowledge-base'
  776. import { dictApi, type DictItem } from '@/api/dict'
  777. // 接口定义已移至 @/api/document
  778. // 状态变量
  779. const loading = ref(false)
  780. const uploadingFile = ref(false)
  781. const submitting = ref(false)
  782. const uploadRef = ref<any>(null)
  783. const uploadDialogVisible = ref(false)
  784. const editDialogVisible = ref(false)
  785. const detailDialogVisible = ref(false)
  786. const previewVisible = ref(false)
  787. const previewLoading = ref(false)
  788. const previewTitle = ref('')
  789. const previewUrl = ref('')
  790. const previewDocType = ref('') // 'original' or 'md'
  791. // 入库相关状态
  792. const ingesting = ref(false)
  793. const ingestDialogVisible = ref(false)
  794. const ingestForm = reactive({
  795. kb_method: 'length',
  796. chunk_size: 500,
  797. separator: '。'
  798. })
  799. // 任务相关状态
  800. const taskAdding = ref(false)
  801. const taskDialogVisible = ref(false)
  802. const tempTagValue = ref<number | null>(null)
  803. const cascaderKey = ref(0)
  804. const taskForm = reactive({
  805. project_name: '',
  806. selectedTags: [] as { id: number, name: string }[]
  807. })
  808. // 标签相关
  809. const tagTree = ref<any[]>([])
  810. const handleAddTag = (val: any) => {
  811. if (!val) return
  812. const id = val
  813. const name = findTagName(tagTree.value, id)
  814. if (name && !taskForm.selectedTags.find(t => t.id === id)) {
  815. taskForm.selectedTags.push({ id, name })
  816. }
  817. // 清空选择器并增加 key 以强制重置 (清除搜索缓存等)
  818. tempTagValue.value = null
  819. cascaderKey.value++
  820. }
  821. const handleRemoveTag = (index: number) => {
  822. taskForm.selectedTags.splice(index, 1)
  823. }
  824. const findTagName = (nodes: any[], id: number): string | null => {
  825. for (const node of nodes) {
  826. if (node.value === id) return node.label
  827. if (node.children) {
  828. const name = findTagName(node.children, id)
  829. if (name) return name
  830. }
  831. }
  832. return null
  833. }
  834. const fetchTagTree = async () => {
  835. try {
  836. const res = await documentApi.getTagTree()
  837. if (res.code === 200) {
  838. tagTree.value = formatTagTree(res.data)
  839. }
  840. } catch (error) {
  841. console.error('获取标签树失败:', error)
  842. }
  843. }
  844. // 格式化标签树以适应 Cascader
  845. const formatTagTree = (data: any[]) => {
  846. return data.map(node => ({
  847. value: node.id,
  848. label: node.name,
  849. children: node.children && node.children.length > 0 ? formatTagTree(node.children) : undefined
  850. }))
  851. }
  852. const taskRules = {
  853. project_name: [
  854. { required: true, message: '请输入项目名称', trigger: 'blur' },
  855. { min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' }
  856. ]
  857. }
  858. const taskFormRef = ref()
  859. const ingestFormRef = ref()
  860. const uploadFormRef = ref()
  861. const editFormRef = ref()
  862. const multipleTableRef = ref()
  863. const commonRules = {
  864. kb_id: [
  865. { required: true, message: '请选择目标知识库', trigger: 'change' }
  866. ],
  867. title: [
  868. { required: true, message: '请输入文档标题', trigger: 'blur' }
  869. ],
  870. table_type: [
  871. { required: true, message: '请选择基本信息类型', trigger: 'change' }
  872. ]
  873. }
  874. const isOfficeDoc = ref(false)
  875. const total = ref(0)
  876. const statistics = ref({
  877. allTotal: 0,
  878. totalEntered: 0
  879. })
  880. const authStore = useAuthStore()
  881. const documents = ref<DocumentItem[]>([])
  882. const currentDoc = ref<DocumentItem | null>(null)
  883. const editForm = reactive({
  884. id: '',
  885. title: '',
  886. note: '',
  887. table_type: 'standard' as 'standard' | 'construction_plan' | 'regulation' | 'other',
  888. year: new Date().getFullYear(),
  889. // 扩展字段 (子表特有属性)
  890. standard_no: '',
  891. issuing_authority: '',
  892. release_date: '',
  893. document_type: '',
  894. professional_field: '',
  895. validity: '',
  896. project_name: '',
  897. project_section: '',
  898. plan_category: '',
  899. level_1_classification: '施工方案',
  900. level_2_classification: '',
  901. level_3_classification: '',
  902. level_4_classification: '',
  903. plan_summary: '',
  904. compilation_basis: '',
  905. file_url: '',
  906. // 补全缺失字段
  907. english_name: '',
  908. implementation_date: '',
  909. drafting_unit: '',
  910. approving_department: '',
  911. engineering_phase: '',
  912. participating_units: '',
  913. reference_basis: '',
  914. source_url: '',
  915. effective_start_date: '',
  916. effective_end_date: '',
  917. kb_id: ''
  918. })
  919. const participatingUnitsList = ref<string[]>([''])
  920. const referenceBasisList = ref<string[]>([''])
  921. const compilationBasisList = ref<string[]>([''])
  922. // 字典选项(从字典表动态加载)
  923. const documentTypeOptions = ref<Array<{ label: string, value: string }>>([])
  924. const professionalFieldOptions = ref<Array<{ label: string, value: string }>>([])
  925. const validityOptions = ref<Array<{ label: string, value: string }>>([])
  926. const planCategoryOptions = ref<Array<{ label: string, value: string }>>([])
  927. const level1Options = ref<Array<{ label: string, value: string }>>([])
  928. const level2Options = ref<Array<{ label: string, value: string }>>([])
  929. const level3Options = ref<Array<{ label: string, value: string }>>([])
  930. const level4Options = ref<Array<{ label: string, value: string }>>([])
  931. // 加载字典选项
  932. const loadDictOptions = async () => {
  933. try {
  934. // 加载文件类型字典
  935. const fileTypeRes = await dictApi.getItemList({
  936. category_id: 'file_type',
  937. enable_flag: '1',
  938. page: 1,
  939. page_size: 100
  940. })
  941. if (fileTypeRes.code === '000000' || fileTypeRes.code === 0) {
  942. documentTypeOptions.value = fileTypeRes.data.list.map((item: DictItem) => ({
  943. label: item.dict_name,
  944. value: item.dict_value
  945. }))
  946. }
  947. // 加载专业类型字典
  948. const professionalTypeRes = await dictApi.getItemList({
  949. category_id: 'professional_type',
  950. enable_flag: '1',
  951. page: 1,
  952. page_size: 100
  953. })
  954. if (professionalTypeRes.code === '000000' || professionalTypeRes.code === 0) {
  955. professionalFieldOptions.value = professionalTypeRes.data.list.map((item: DictItem) => ({
  956. label: item.dict_name,
  957. value: item.dict_value
  958. }))
  959. }
  960. // 加载时效性字典
  961. const validityRes = await dictApi.getItemList({
  962. category_id: 'time_effect',
  963. enable_flag: '1',
  964. page: 1,
  965. page_size: 100
  966. })
  967. if (validityRes.code === '000000' || validityRes.code === 0) {
  968. validityOptions.value = validityRes.data.list.map((item: DictItem) => ({
  969. label: item.dict_name,
  970. value: item.dict_value
  971. }))
  972. }
  973. // 加载施工方案类别字典
  974. const planTypeRes = await dictApi.getItemList({
  975. category_id: 'construction_plan_type',
  976. enable_flag: '1',
  977. page: 1,
  978. page_size: 100
  979. })
  980. if (planTypeRes.code === '000000' || planTypeRes.code === 0) {
  981. planCategoryOptions.value = planTypeRes.data.list.map((item: DictItem) => ({
  982. label: item.dict_name,
  983. value: item.dict_value
  984. }))
  985. }
  986. // 加载一级分类字典
  987. const level1Res = await dictApi.getItemList({
  988. category_id: 'construction_plan_first_type',
  989. enable_flag: '1',
  990. page: 1,
  991. page_size: 100
  992. })
  993. if (level1Res.code === '000000' || level1Res.code === 0) {
  994. level1Options.value = level1Res.data.list.map((item: DictItem) => ({
  995. label: item.dict_name,
  996. value: item.dict_value
  997. }))
  998. }
  999. // 加载二级分类字典
  1000. const level2Res = await dictApi.getItemList({
  1001. category_id: 'construction_plan_second_type',
  1002. enable_flag: '1',
  1003. page: 1,
  1004. page_size: 100
  1005. })
  1006. if (level2Res.code === '000000' || level2Res.code === 0) {
  1007. level2Options.value = level2Res.data.list.map((item: DictItem) => ({
  1008. label: item.dict_name,
  1009. value: item.dict_value
  1010. }))
  1011. }
  1012. // 加载三级分类字典
  1013. const level3Res = await dictApi.getItemList({
  1014. category_id: 'construction_plan_third_type',
  1015. enable_flag: '1',
  1016. page: 1,
  1017. page_size: 100
  1018. })
  1019. if (level3Res.code === '000000' || level3Res.code === 0) {
  1020. level3Options.value = level3Res.data.list.map((item: DictItem) => ({
  1021. label: item.dict_name,
  1022. value: item.dict_value
  1023. }))
  1024. }
  1025. // 加载四级分类字典
  1026. const level4Res = await dictApi.getItemList({
  1027. category_id: 'construction_plan_fourth_type',
  1028. enable_flag: '1',
  1029. page: 1,
  1030. page_size: 100
  1031. })
  1032. if (level4Res.code === '000000' || level4Res.code === 0) {
  1033. level4Options.value = level4Res.data.list.map((item: DictItem) => ({
  1034. label: item.dict_name,
  1035. value: item.dict_value
  1036. }))
  1037. }
  1038. } catch (error) {
  1039. console.error('加载字典选项失败:', error)
  1040. }
  1041. }
  1042. // 列表项管理
  1043. const addListItem = (list: string[]) => {
  1044. list.push('')
  1045. }
  1046. const removeListItem = (list: string[], index: number) => {
  1047. if (list.length > 1) {
  1048. list.splice(index, 1)
  1049. } else {
  1050. list[0] = ''
  1051. }
  1052. }
  1053. const currentTitle = computed(() => {
  1054. return '文档管理中心'
  1055. })
  1056. const formTitle = computed(() => {
  1057. const typeMap: Record<string, string> = {
  1058. standard: '施工标准规范',
  1059. construction_plan: '施工方案',
  1060. regulation: '办公制度',
  1061. other: '其他文档'
  1062. }
  1063. return `编辑${typeMap[editForm.table_type] || ''}`
  1064. })
  1065. const titleLabel = computed(() => {
  1066. if (editForm.table_type === 'standard') return '标准名称'
  1067. if (editForm.table_type === 'construction_plan') return '方案名称'
  1068. if (editForm.table_type === 'regulation') return '制度名称'
  1069. return '文档名称'
  1070. })
  1071. const authorityLabel = computed(() => {
  1072. if (editForm.table_type === 'standard') return '发布单位'
  1073. if (editForm.table_type === 'construction_plan') return '编制单位'
  1074. if (editForm.table_type === 'regulation') return '发布部门'
  1075. return '发布机构'
  1076. })
  1077. const dateLabel = computed(() => {
  1078. if (editForm.table_type === 'standard') return '发布日期'
  1079. if (editForm.table_type === 'construction_plan') return '编制日期'
  1080. if (editForm.table_type === 'regulation') return '发布日期'
  1081. return '日期'
  1082. })
  1083. const selectedIds = ref<string[]>([])
  1084. const searchQuery = reactive({
  1085. keyword: '',
  1086. table_type: null as string | null,
  1087. whether_to_enter: null as number | null,
  1088. conversion_status: null as number | null,
  1089. page: 1,
  1090. size: 10
  1091. })
  1092. const uploadForm = reactive({
  1093. title: '',
  1094. note: '',
  1095. file_url: '',
  1096. table_type: 'standard' as 'standard' | 'construction_plan' | 'regulation' | 'other',
  1097. year: new Date().getFullYear(),
  1098. kb_id: ''
  1099. })
  1100. // 知识库列表
  1101. const kbOptions = ref<{ label: string, value: string }[]>([])
  1102. const loadKbOptions = async () => {
  1103. try {
  1104. const res = await getKnowledgeBases({ page_size: 100 })
  1105. if (res.code === '000000' || res.code === 0) {
  1106. kbOptions.value = res.data.map((item: any) => ({
  1107. label: item.name,
  1108. value: item.id
  1109. }))
  1110. }
  1111. } catch (error) {
  1112. console.error('加载知识库选项失败', error)
  1113. }
  1114. }
  1115. // 计算属性
  1116. const proxyPreviewUrl = computed(() => {
  1117. if (!previewUrl.value) return ''
  1118. // 使用后端代理接口查看外部网页,附加 token 以通过认证
  1119. let baseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000'
  1120. if (baseUrl.includes('localhost') && window.location.hostname !== 'localhost' && window.location.hostname !== '127.0.0.1') {
  1121. baseUrl = baseUrl.replace('localhost', window.location.hostname)
  1122. }
  1123. return `${baseUrl}/api/v1/sample/documents/proxy-view?url=${encodeURIComponent(previewUrl.value)}&token=${authStore.token}`
  1124. })
  1125. const isHtmlPage = computed(() => {
  1126. if (!previewUrl.value) return false
  1127. const url = previewUrl.value.toLowerCase()
  1128. return url.includes('html') || url.includes('newgbinfo') || !url.split(/[?#]/)[0].includes('.')
  1129. })
  1130. // 方法
  1131. const formatSimpleDate = (date: string | null) => {
  1132. if (!date) return '-'
  1133. return dayjs(date).format('YYYY-MM-DD')
  1134. }
  1135. const formatDate = (date: string | undefined | null, formatStr: string = 'YYYY-MM-DD HH:mm:ss') => {
  1136. return date ? dayjs(date).format(formatStr) : '-'
  1137. }
  1138. const getFileIcon = (row: DocumentItem) => {
  1139. const ext = getFileExtension(row).replace('.', '').toLowerCase()
  1140. if (['pdf'].includes(ext)) return 'pdf'
  1141. if (['doc', 'docx'].includes(ext)) return 'word'
  1142. if (['xls', 'xlsx'].includes(ext)) return 'excel'
  1143. if (['ppt', 'pptx'].includes(ext)) return 'ppt'
  1144. if (['html', 'htm'].includes(ext)) return 'html'
  1145. return 'file'
  1146. }
  1147. const getFileIconClass = (row: DocumentItem) => {
  1148. return `icon-${getFileIcon(row)}`
  1149. }
  1150. const getValidityName = (val: string | null | undefined) => {
  1151. const names: Record<string, string> = {
  1152. XH: '现行',
  1153. FZ: '废止',
  1154. SX: '试行'
  1155. }
  1156. return names[val || ''] || val || '-'
  1157. }
  1158. const getSourceTypeName = (sourceType: string | null | undefined) => {
  1159. const names: Record<string, string> = {
  1160. standard: '施工标准规范',
  1161. construction_plan: '施工方案',
  1162. regulation: '办公制度',
  1163. other: '其他文档'
  1164. }
  1165. return names[sourceType || ''] || '未知类型'
  1166. }
  1167. const getSourceTypeShortName = (sourceType: string | null | undefined) => {
  1168. const names: Record<string, string> = {
  1169. standard: '标准规范',
  1170. construction_plan: '施工方案',
  1171. regulation: '办公制度',
  1172. other: '其他'
  1173. }
  1174. return names[sourceType || ''] || '未知'
  1175. }
  1176. const getKBTagType = (sourceType: string | null | undefined) => {
  1177. const types: Record<string, string> = {
  1178. standard: 'primary',
  1179. construction_plan: 'success',
  1180. regulation: 'warning',
  1181. other: 'info'
  1182. }
  1183. return types[sourceType || ''] || 'info'
  1184. }
  1185. // 为已入库或未转化的行添加特定类名
  1186. const tableRowClassName = ({ row }: { row: DocumentItem }) => {
  1187. return ''
  1188. }
  1189. // 判断是否可以勾选(所有文档均可勾选,用于批量删除等操作)
  1190. const canSelect = (row: DocumentItem) => {
  1191. return true
  1192. }
  1193. const handleSelectionChange = (selection: DocumentItem[]) => {
  1194. selectedIds.value = selection.map(item => item.id)
  1195. }
  1196. const confirmIngest = async () => {
  1197. if (selectedIds.value.length === 0) {
  1198. ElMessage.warning('请先选择要入库的文档')
  1199. return
  1200. }
  1201. if (!ingestFormRef.value) return
  1202. try {
  1203. await ingestFormRef.value.validate()
  1204. } catch (error) {
  1205. return
  1206. }
  1207. ingesting.value = true
  1208. try {
  1209. const res = await request.post<ApiResponse>('/api/v1/sample/documents/batch-enter', {
  1210. ids: selectedIds.value,
  1211. kb_method: ingestForm.kb_method,
  1212. chunk_size: ingestForm.chunk_size,
  1213. separator: ingestForm.separator,
  1214. table_type: searchQuery.table_type
  1215. })
  1216. if (res.code === '000000' || res.code === 0) {
  1217. ElMessage.success(res.message || '已加入入库队列')
  1218. selectedIds.value = []
  1219. multipleTableRef.value?.clearSelection()
  1220. ingestDialogVisible.value = false
  1221. fetchDocuments()
  1222. }
  1223. } catch (error: any) {
  1224. if (error !== 'cancel') {
  1225. console.error('入库失败:', error)
  1226. }
  1227. } finally {
  1228. ingesting.value = false
  1229. }
  1230. }
  1231. // 提取字段检查逻辑
  1232. const checkDocumentsCompleteness = async (ids: string[]) => {
  1233. loading.value = true
  1234. try {
  1235. const detailPromises = ids.map(id => documentApi.getDetail(id))
  1236. const detailResults = await Promise.all(detailPromises)
  1237. const docs = detailResults.map(res => res.data)
  1238. let isIncomplete = false
  1239. let missingFields: string[] = []
  1240. for (const doc of docs) {
  1241. const type = doc.source_type || searchQuery.table_type
  1242. if (type === 'standard') {
  1243. const standardFields = [
  1244. { key: 'standard_no', label: '标准编号' },
  1245. { key: 'issuing_authority', label: '发布单位' },
  1246. { key: 'release_date', label: '发布日期' },
  1247. { key: 'implementation_date', label: '实施日期' },
  1248. { key: 'document_type', label: '文件类型' },
  1249. { key: 'professional_field', label: '专业领域' },
  1250. { key: 'validity', label: '时效性' },
  1251. { key: 'drafting_unit', label: '起草单位' },
  1252. { key: 'approving_department', label: '批准部门' }
  1253. ]
  1254. for (const f of standardFields) {
  1255. const val = doc[f.key as keyof typeof doc]
  1256. if (val === null || val === undefined || (typeof val === 'string' && val.trim() === '')) {
  1257. isIncomplete = true
  1258. if (!missingFields.includes(f.label)) missingFields.push(f.label)
  1259. }
  1260. }
  1261. } else if (type === 'construction_plan') {
  1262. const planFields = [
  1263. { key: 'issuing_authority', label: '编制单位' },
  1264. { key: 'release_date', label: '编制日期' },
  1265. { key: 'project_name', label: '项目名称' },
  1266. { key: 'plan_category', label: '方案类型' },
  1267. { key: 'level_1_classification', label: '一级分类' },
  1268. { key: 'level_2_classification', label: '二级分类' },
  1269. { key: 'level_3_classification', label: '三级分类' },
  1270. { key: 'level_4_classification', label: '四级分类' },
  1271. { key: 'plan_summary', label: '方案摘要' }
  1272. ]
  1273. for (const f of planFields) {
  1274. const val = doc[f.key as keyof typeof doc]
  1275. if (val === null || val === undefined || (typeof val === 'string' && val.trim() === '')) {
  1276. isIncomplete = true
  1277. if (!missingFields.includes(f.label)) missingFields.push(f.label)
  1278. }
  1279. }
  1280. }
  1281. if (isIncomplete && ids.length === 1) break
  1282. }
  1283. if (isIncomplete) {
  1284. loading.value = false // 弹出确认框前先关闭 loading
  1285. try {
  1286. await ElMessageBox.confirm(
  1287. '该文档基本信息其中某几个字段未补充完善,可能导致无法检索的问题,你是否继续?',
  1288. '提示',
  1289. {
  1290. confirmButtonText: '继续',
  1291. cancelButtonText: '取消',
  1292. type: 'warning',
  1293. }
  1294. )
  1295. return true
  1296. } catch (error) {
  1297. return false
  1298. }
  1299. }
  1300. loading.value = false // 检查完成,关闭 loading
  1301. return true
  1302. } catch (error) {
  1303. loading.value = false // 出错时也关闭 loading
  1304. console.error('检查文档完善度失败:', error)
  1305. ElMessage.error('检查文档完善度失败,请重试')
  1306. return false
  1307. } finally {
  1308. // 确保 loading 在所有路径下都被正确关闭
  1309. loading.value = false
  1310. }
  1311. }
  1312. const handleBatchEnter = async () => {
  1313. if (selectedIds.value.length === 0) {
  1314. ElMessage.warning('请先选择要入库的文档')
  1315. return
  1316. }
  1317. // 先检查字段完善度
  1318. const canContinue = await checkDocumentsCompleteness(selectedIds.value)
  1319. if (!canContinue) return
  1320. // 校验通过后,显示入库设置弹窗
  1321. ingestForm.kb_method = 'length'
  1322. ingestForm.chunk_size = 500
  1323. ingestForm.separator = '。'
  1324. if (ingestFormRef.value) {
  1325. ingestFormRef.value.clearValidate()
  1326. }
  1327. ingestDialogVisible.value = true
  1328. }
  1329. const handleBatchAddToTask = async () => {
  1330. if (selectedIds.value.length === 0) {
  1331. ElMessage.warning('请先选择要加入任务的文档')
  1332. return
  1333. }
  1334. // 检查是否有未入库的数据
  1335. const unenteredDocs = documents.value.filter(doc => selectedIds.value.includes(doc.id) && !isEntered(doc.whether_to_enter))
  1336. if (unenteredDocs.length > 0) {
  1337. ElMessage.warning(`选中数据中包含 ${unenteredDocs.length} 份未入库文档,只有已入库的文档可以加入标注任务。`)
  1338. return
  1339. }
  1340. // 显示任务弹窗(填入项目名称和标签),不再进行字段检查
  1341. taskForm.project_name = ''
  1342. taskForm.selectedTags = []
  1343. tempTagValue.value = null
  1344. if (taskFormRef.value) {
  1345. taskFormRef.value.clearValidate()
  1346. }
  1347. if (tagTree.value.length === 0) {
  1348. await fetchTagTree()
  1349. }
  1350. taskDialogVisible.value = true
  1351. }
  1352. const confirmAddTask = async () => {
  1353. if (!taskFormRef.value) return
  1354. try {
  1355. await taskFormRef.value.validate()
  1356. } catch (error) {
  1357. return
  1358. }
  1359. taskAdding.value = true
  1360. try {
  1361. const tags = taskForm.selectedTags.map(t => t.name)
  1362. const res = await documentApi.batchAddToTask(selectedIds.value, taskForm.project_name, tags)
  1363. if (res.code === '000000' || res.code === 0) {
  1364. ElMessage.success(res.message || '操作成功')
  1365. selectedIds.value = []
  1366. multipleTableRef.value?.clearSelection()
  1367. taskDialogVisible.value = false
  1368. fetchDocuments()
  1369. } else {
  1370. ElMessage.error(res.message || '操作失败')
  1371. }
  1372. } catch (error: any) {
  1373. console.error('批量加入任务失败:', error)
  1374. ElMessage.error('操作失败')
  1375. } finally {
  1376. taskAdding.value = false
  1377. }
  1378. }
  1379. const handleSingleEnter = async (doc: DocumentItem | null) => {
  1380. if (!doc) return
  1381. // 先检查字段完善度
  1382. const canContinue = await checkDocumentsCompleteness([doc.id])
  1383. if (!canContinue) return
  1384. selectedIds.value = [doc.id]
  1385. ingestForm.kb_method = 'length'
  1386. ingestForm.chunk_size = 500
  1387. ingestForm.separator = '。'
  1388. if (ingestFormRef.value) {
  1389. ingestFormRef.value.clearValidate()
  1390. }
  1391. ingestDialogVisible.value = true
  1392. }
  1393. const handleDelete = async (row: DocumentItem) => {
  1394. if (isEntered(row.whether_to_enter)) {
  1395. ElMessage.warning(`文档 "${row.title}" 已入库,请先执行“清空数据”操作后再删除。`)
  1396. return
  1397. }
  1398. try {
  1399. await ElMessageBox.confirm(
  1400. `确定要删除文档 "${row.title}" 吗?此操作不可恢复。`,
  1401. '确认删除',
  1402. {
  1403. confirmButtonText: '确定',
  1404. cancelButtonText: '取消',
  1405. type: 'warning',
  1406. }
  1407. )
  1408. const res = await documentApi.batchDelete([row.id])
  1409. if (res.code === '000000' || res.code === 0) {
  1410. ElMessage.success('删除成功')
  1411. fetchDocuments()
  1412. } else {
  1413. ElMessage.error(res.message || '删除失败')
  1414. }
  1415. } catch (error: any) {
  1416. if (error !== 'cancel') {
  1417. console.error('删除文档失败:', error)
  1418. ElMessage.error('删除失败')
  1419. }
  1420. }
  1421. }
  1422. const handleBatchDelete = async () => {
  1423. if (selectedIds.value.length === 0) return
  1424. // 检查是否有已入库的数据
  1425. const enteredDocs = documents.value.filter(doc => selectedIds.value.includes(doc.id) && isEntered(doc.whether_to_enter))
  1426. if (enteredDocs.length > 0) {
  1427. ElMessage.warning(`选中数据中包含 ${enteredDocs.length} 份已入库文档,请先执行“清空数据”操作后再删除。`)
  1428. return
  1429. }
  1430. try {
  1431. await ElMessageBox.confirm(
  1432. `确定要批量删除选中的 ${selectedIds.value.length} 条文档吗?此操作不可恢复。`,
  1433. '确认批量删除',
  1434. {
  1435. confirmButtonText: '确定',
  1436. cancelButtonText: '取消',
  1437. type: 'warning',
  1438. }
  1439. )
  1440. const res = await documentApi.batchDelete(selectedIds.value)
  1441. if (res.code === '000000' || res.code === 0) {
  1442. ElMessage.success(res.message || '批量删除成功')
  1443. selectedIds.value = []
  1444. multipleTableRef.value?.clearSelection()
  1445. fetchDocuments()
  1446. } else {
  1447. ElMessage.error(res.message || '批量删除失败')
  1448. }
  1449. } catch (error: any) {
  1450. if (error !== 'cancel') {
  1451. console.error('批量删除失败:', error)
  1452. ElMessage.error('操作失败')
  1453. }
  1454. }
  1455. }
  1456. const handleBatchClear = async () => {
  1457. if (selectedIds.value.length === 0) {
  1458. ElMessage.warning('请先选择要清空数据的文档')
  1459. return
  1460. }
  1461. // 过滤出已入库的文档
  1462. const enteredIds = documents.value
  1463. .filter(doc => selectedIds.value.includes(doc.id) && isEntered(doc.whether_to_enter))
  1464. .map(doc => doc.id)
  1465. if (enteredIds.length === 0) {
  1466. ElMessage.info('所选文档均未入库,无需清空')
  1467. return
  1468. }
  1469. try {
  1470. await ElMessageBox.confirm(
  1471. `确定要清空选中的 ${enteredIds.length} 份文档的知识库片段吗?清空后文档将变为“未入库”状态,且可以被删除。`,
  1472. '确认清空数据',
  1473. {
  1474. confirmButtonText: '确定',
  1475. cancelButtonText: '取消',
  1476. type: 'warning',
  1477. }
  1478. )
  1479. loading.value = true
  1480. const res = await documentApi.batchClear(enteredIds)
  1481. if (res.code === '000000' || res.code === 0) {
  1482. ElMessage.success(res.message || '数据清空成功')
  1483. selectedIds.value = []
  1484. multipleTableRef.value?.clearSelection()
  1485. fetchDocuments()
  1486. } else {
  1487. ElMessage.error(res.message || '数据清空失败')
  1488. }
  1489. } catch (error: any) {
  1490. if (error !== 'cancel') {
  1491. console.error('清空数据失败:', error)
  1492. ElMessage.error('操作失败')
  1493. }
  1494. } finally {
  1495. loading.value = false
  1496. }
  1497. }
  1498. const isEntered = (val: any) => {
  1499. return val === 1 || val === true
  1500. }
  1501. const getConversionStatusTag = (row: DocumentItem) => {
  1502. switch (row.conversion_status) {
  1503. case 1: return 'warning' // 转换中
  1504. case 2: return 'success' // 成功
  1505. case 3: return 'danger' // 失败
  1506. default: return 'info' // 未转换 (0)
  1507. }
  1508. }
  1509. const getConversionStatusText = (row: DocumentItem) => {
  1510. switch (row.conversion_status) {
  1511. case 1: return '转换中'
  1512. case 2: return '转换成功'
  1513. case 3: return '转换失败'
  1514. default: return '未转换'
  1515. }
  1516. }
  1517. const fetchDocuments = async () => {
  1518. loading.value = true
  1519. try {
  1520. const res = await documentApi.getList(searchQuery)
  1521. if (res.code === '000000' || res.code === 0) {
  1522. documents.value = res.data.items
  1523. total.value = res.data.total
  1524. statistics.value.allTotal = res.data.all_total || 0
  1525. statistics.value.totalEntered = res.data.total_entered || 0
  1526. // 自动检查是否需要开启轮询
  1527. const hasConverting = documents.value.some(doc => doc.conversion_status === 1)
  1528. if (hasConverting) {
  1529. startPolling()
  1530. } else {
  1531. stopPolling()
  1532. }
  1533. }
  1534. } catch (error) {
  1535. console.error('获取文档列表失败:', error)
  1536. } finally {
  1537. loading.value = false
  1538. }
  1539. }
  1540. const isRefreshing = ref(false)
  1541. const refreshTimer = ref<any>(null)
  1542. const startPolling = () => {
  1543. if (refreshTimer.value === null) {
  1544. refreshTimer.value = window.setTimeout(refreshDocumentsSilently, 5000)
  1545. }
  1546. }
  1547. const stopPolling = () => {
  1548. if (refreshTimer.value) {
  1549. window.clearTimeout(refreshTimer.value)
  1550. refreshTimer.value = null
  1551. }
  1552. }
  1553. const refreshDocumentsSilently = async () => {
  1554. if (isRefreshing.value) return
  1555. isRefreshing.value = true
  1556. try {
  1557. const res = await documentApi.getList(searchQuery, true)
  1558. if (res.code === '000000' || res.code === 0) {
  1559. documents.value = res.data.items
  1560. total.value = res.data.total
  1561. statistics.value.allTotal = res.data.all_total || 0
  1562. statistics.value.totalEntered = res.data.total_entered || 0
  1563. // 检查是否还需要继续轮询
  1564. const hasConverting = documents.value.some(doc => doc.conversion_status === 1)
  1565. if (!hasConverting) {
  1566. // 如果当前没有转换中的文档,不要立即停止,延迟一个周期再检查
  1567. // 这样可以避免转换任务刚提交但状态还没更新到 1 的情况
  1568. setTimeout(() => {
  1569. const stillNoConverting = documents.value.some(doc => doc.conversion_status === 1)
  1570. if (!stillNoConverting) {
  1571. stopPolling()
  1572. }
  1573. }, 5000)
  1574. }
  1575. }
  1576. } catch (error) {
  1577. console.error('静默刷新失败:', error)
  1578. } finally {
  1579. isRefreshing.value = false
  1580. // 如果没有手动停止,且组件未卸载,则安排下一次刷新
  1581. if (refreshTimer.value !== null) {
  1582. refreshTimer.value = window.setTimeout(refreshDocumentsSilently, 5000)
  1583. }
  1584. }
  1585. }
  1586. const handleSearch = () => {
  1587. searchQuery.page = 1
  1588. fetchDocuments()
  1589. }
  1590. const handleSizeChange = (val: number) => {
  1591. searchQuery.size = val
  1592. fetchDocuments()
  1593. }
  1594. const handleCurrentChange = (val: number) => {
  1595. searchQuery.page = val
  1596. fetchDocuments()
  1597. }
  1598. const beforeUpload = (file: File) => {
  1599. const isLt50M = file.size / 1024 / 1024 < 50
  1600. if (!isLt50M) {
  1601. ElMessage.error('上传文件大小不能超过 50MB!')
  1602. return false
  1603. }
  1604. return true
  1605. }
  1606. const handleExceed = () => {
  1607. ElMessage.warning('只能上传一个文件,请先移除已上传的文件')
  1608. }
  1609. const customUpload = async (options: any) => {
  1610. const { file, onSuccess, onError } = options
  1611. try {
  1612. uploadingFile.value = true
  1613. // 1. 获取预签名 URL
  1614. const res = await documentApi.getUploadUrl(file.name, file.type || 'application/octet-stream', uploadForm.table_type)
  1615. if (res.code !== '000000' && res.code !== 0) {
  1616. throw new Error(res.message || '获取上传链接失败')
  1617. }
  1618. const { upload_url, file_url } = res.data
  1619. // 2. 直接上传到 MinIO (PUT 请求)
  1620. await axios.put(upload_url, file, {
  1621. headers: {
  1622. 'Content-Type': file.type || 'application/octet-stream'
  1623. }
  1624. })
  1625. // 3. 上传成功,更新表单
  1626. uploadForm.file_url = file_url
  1627. if (!uploadForm.title) {
  1628. // 如果标题为空,自动填充文件名(去掉后缀)
  1629. uploadForm.title = file.name.replace(/\.[^/.]+$/, "")
  1630. }
  1631. ElMessage.success('文件上传成功')
  1632. onSuccess(res.data)
  1633. } catch (error: any) {
  1634. console.error('文件上传失败:', error)
  1635. ElMessage.error(error.message || '文件上传失败')
  1636. onError(error)
  1637. } finally {
  1638. uploadingFile.value = false
  1639. }
  1640. }
  1641. const handleUpload = () => {
  1642. uploadForm.title = ''
  1643. uploadForm.note = ''
  1644. uploadForm.file_url = ''
  1645. uploadForm.kb_id = ''
  1646. if (uploadRef.value) {
  1647. uploadRef.value.clearFiles()
  1648. }
  1649. if (uploadFormRef.value) {
  1650. uploadFormRef.value.clearValidate()
  1651. }
  1652. loadKbOptions()
  1653. uploadDialogVisible.value = true
  1654. }
  1655. const submitUpload = async () => {
  1656. if (!uploadFormRef.value) return
  1657. try {
  1658. await uploadFormRef.value.validate()
  1659. } catch (error) {
  1660. return
  1661. }
  1662. submitting.value = true
  1663. try {
  1664. const res = await documentApi.add(uploadForm)
  1665. if (res.code === '000000' || res.code === 0) {
  1666. ElMessage.success('上传成功')
  1667. uploadDialogVisible.value = false
  1668. fetchDocuments()
  1669. // 上传后可能立即开始转换,启动轮询监控状态
  1670. startPolling()
  1671. }
  1672. } catch (error) {
  1673. console.error('上传失败:', error)
  1674. } finally {
  1675. submitting.value = false
  1676. }
  1677. }
  1678. const handleEdit = async (row: DocumentItem) => {
  1679. loadKbOptions()
  1680. try {
  1681. const res = await documentApi.getDetail(row.id)
  1682. if ((res.code === '000000' || res.code === 0) && res.data) {
  1683. const data = res.data
  1684. editForm.id = data.id
  1685. editForm.title = data.title
  1686. editForm.note = data.note || ''
  1687. editForm.table_type = data.source_type
  1688. // 填充扩展字段
  1689. editForm.standard_no = data.standard_no || ''
  1690. editForm.issuing_authority = data.issuing_authority || ''
  1691. editForm.release_date = data.release_date || ''
  1692. editForm.document_type = data.document_type || ''
  1693. editForm.professional_field = data.professional_field || ''
  1694. editForm.validity = data.validity || ''
  1695. editForm.project_name = data.project_name || ''
  1696. editForm.project_section = data.project_section || ''
  1697. editForm.plan_category = data.plan_category || ''
  1698. editForm.level_1_classification = data.level_1_classification || '施工方案'
  1699. editForm.level_2_classification = data.level_2_classification || ''
  1700. editForm.level_3_classification = data.level_3_classification || ''
  1701. editForm.level_4_classification = data.level_4_classification || ''
  1702. editForm.plan_summary = data.plan_summary || ''
  1703. editForm.compilation_basis = data.compilation_basis || ''
  1704. editForm.file_url = data.file_url || ''
  1705. // 填充新补全的字段
  1706. editForm.english_name = data.english_name || ''
  1707. editForm.implementation_date = data.implementation_date || ''
  1708. editForm.drafting_unit = data.drafting_unit || ''
  1709. editForm.approving_department = data.approving_department || ''
  1710. editForm.engineering_phase = data.engineering_phase || ''
  1711. editForm.source_url = data.source_url || ''
  1712. editForm.effective_start_date = data.effective_start_date || ''
  1713. editForm.effective_end_date = data.effective_end_date || ''
  1714. editForm.kb_id = data.kb_id || ''
  1715. if (editFormRef.value) {
  1716. editFormRef.value.clearValidate()
  1717. }
  1718. // 处理列表字段
  1719. if (data.participating_units) {
  1720. participatingUnitsList.value = data.participating_units.split(';').filter((i: string) => i.trim())
  1721. if (participatingUnitsList.value.length === 0) participatingUnitsList.value = ['']
  1722. } else {
  1723. participatingUnitsList.value = ['']
  1724. }
  1725. if (data.reference_basis) {
  1726. referenceBasisList.value = data.reference_basis.split(';').filter((i: string) => i.trim())
  1727. if (referenceBasisList.value.length === 0) referenceBasisList.value = ['']
  1728. } else {
  1729. referenceBasisList.value = ['']
  1730. }
  1731. if (data.compilation_basis) {
  1732. compilationBasisList.value = data.compilation_basis.split(';').filter((i: string) => i.trim())
  1733. if (compilationBasisList.value.length === 0) compilationBasisList.value = ['']
  1734. } else {
  1735. compilationBasisList.value = ['']
  1736. }
  1737. editDialogVisible.value = true
  1738. } else {
  1739. ElMessage.error(res.message || '获取文档详情失败')
  1740. }
  1741. } catch (error) {
  1742. console.error('获取文档详情失败:', error)
  1743. ElMessage.error('获取文档详情失败')
  1744. }
  1745. }
  1746. const submitEdit = async () => {
  1747. if (!editFormRef.value) return
  1748. try {
  1749. await editFormRef.value.validate()
  1750. } catch (error) {
  1751. return
  1752. }
  1753. submitting.value = true
  1754. try {
  1755. // 将列表字段合并为分号分隔的字符串
  1756. const payload: any = {
  1757. ...editForm,
  1758. source_type: editForm.table_type
  1759. }
  1760. if (editForm.table_type === 'standard') {
  1761. payload.participating_units = participatingUnitsList.value.filter(i => i.trim()).join(';')
  1762. payload.reference_basis = referenceBasisList.value.filter(i => i.trim()).join(';')
  1763. } else if (editForm.table_type === 'construction_plan') {
  1764. payload.compilation_basis = compilationBasisList.value.filter(i => i.trim()).join(';')
  1765. }
  1766. const res = await documentApi.edit(payload)
  1767. if (res.code === '000000' || res.code === 0) {
  1768. ElMessage.success('更新成功')
  1769. editDialogVisible.value = false
  1770. fetchDocuments()
  1771. }
  1772. } catch (error) {
  1773. console.error('编辑失败:', error)
  1774. } finally {
  1775. submitting.value = false
  1776. }
  1777. }
  1778. const handleView = (row: DocumentItem) => {
  1779. if (row.file_url) {
  1780. handlePreview(row)
  1781. } else {
  1782. currentDoc.value = row
  1783. detailDialogVisible.value = true
  1784. }
  1785. }
  1786. const handleEditFromDetail = () => {
  1787. if (currentDoc.value) {
  1788. const doc = currentDoc.value
  1789. detailDialogVisible.value = false
  1790. handleEdit(doc)
  1791. }
  1792. }
  1793. const handlePreview = (row: DocumentItem | null) => {
  1794. if (!row || !row.file_url) return
  1795. currentDoc.value = row
  1796. previewTitle.value = row.title
  1797. const ext = getFileExtension(row).toLowerCase()
  1798. const officeExtensions = ['.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx']
  1799. isOfficeDoc.value = officeExtensions.includes(ext)
  1800. // 如果是 Office 文档且有转换后的 MD,优先预览 MD
  1801. if (isOfficeDoc.value && row.md_url) {
  1802. previewUrl.value = row.md_url
  1803. previewDocType.value = 'md'
  1804. previewTitle.value = `${row.title} (转换预览)`
  1805. } else {
  1806. previewUrl.value = row.file_url
  1807. previewDocType.value = 'original'
  1808. }
  1809. previewVisible.value = true
  1810. // 如果是无法直接预览的 Office 文档,不需要等待 iframe 加载
  1811. if (isOfficeDoc.value && previewDocType.value === 'original') {
  1812. previewLoading.value = false
  1813. } else {
  1814. previewLoading.value = true
  1815. }
  1816. }
  1817. const handleDownload = (row: DocumentItem) => {
  1818. if (row.file_url) {
  1819. const ext = getFileExtension(row)
  1820. const filename = row.title.endsWith(ext) ? row.title : `${row.title}${ext}`
  1821. downloadFile(row.file_url, filename)
  1822. } else {
  1823. ElMessage.warning('该文档暂无下载链接')
  1824. }
  1825. }
  1826. const handleDownloadConverted = (row: DocumentItem) => {
  1827. if (row.md_url) {
  1828. const filename = row.md_display_name || `${row.title}.md`
  1829. downloadFile(row.md_url, filename)
  1830. } else {
  1831. ElMessage.warning('该文档暂无转换后的文件')
  1832. }
  1833. }
  1834. const handleDownloadJson = (row: DocumentItem) => {
  1835. if (row.json_url) {
  1836. const filename = row.json_display_name || `${row.title}.json`
  1837. downloadFile(row.json_url, filename)
  1838. } else {
  1839. ElMessage.warning('该文档暂无转换后的 JSON 文件')
  1840. }
  1841. }
  1842. const handleConvert = async (row: DocumentItem) => {
  1843. // 如果已经转换成功(status 为 2),弹出确认框
  1844. if (row.conversion_status === 2) {
  1845. try {
  1846. await ElMessageBox.confirm(
  1847. '该文档已经转换完成,再次转换将覆盖原转换后端文件,你继续吗?',
  1848. '再次转换确认',
  1849. {
  1850. confirmButtonText: '确定',
  1851. cancelButtonText: '取消',
  1852. type: 'warning',
  1853. }
  1854. )
  1855. } catch (error) {
  1856. // 用户取消
  1857. return
  1858. }
  1859. }
  1860. try {
  1861. // 乐观更新:设置状态为转换中
  1862. row.conversion_status = 1
  1863. row.conversion_error = undefined
  1864. // 启动转换任务(静默刷新,不触发全局 loading)
  1865. const res = await documentApi.convert(row.id)
  1866. if (res.code === '000000' || res.code === 0) {
  1867. ElMessage.success(res.message || '转换任务已启动')
  1868. // 启动轮询以监控转换进度
  1869. startPolling()
  1870. // 触发一次静默刷新以更新状态
  1871. refreshDocumentsSilently()
  1872. } else {
  1873. // 失败了恢复状态并刷新
  1874. refreshDocumentsSilently()
  1875. }
  1876. } catch (error) {
  1877. console.error('启动转换失败:', error)
  1878. // 失败了恢复状态并刷新
  1879. refreshDocumentsSilently()
  1880. }
  1881. }
  1882. const openInNewWindow = () => {
  1883. if (previewUrl.value) {
  1884. // 优先尝试在新窗口打开代理后的链接,这有助于控制 Content-Disposition
  1885. window.open(proxyPreviewUrl.value, '_blank')
  1886. }
  1887. }
  1888. onMounted(() => {
  1889. fetchDocuments()
  1890. loadKbOptions()
  1891. loadDictOptions()
  1892. })
  1893. onUnmounted(() => {
  1894. if (refreshTimer.value) {
  1895. window.clearTimeout(refreshTimer.value)
  1896. }
  1897. stopPolling()
  1898. })
  1899. </script>
  1900. <style scoped>
  1901. .tag-group {
  1902. display: flex;
  1903. flex-wrap: wrap;
  1904. gap: 8px;
  1905. align-items: center;
  1906. min-height: 32px;
  1907. }
  1908. .file-info-edit {
  1909. display: flex;
  1910. flex-direction: column;
  1911. gap: 8px;
  1912. width: 100%;
  1913. }
  1914. .file-tag {
  1915. display: flex;
  1916. align-items: center;
  1917. gap: 5px;
  1918. width: fit-content;
  1919. }
  1920. .tag-item {
  1921. margin: 2px 0;
  1922. }
  1923. .tag-adder {
  1924. width: 120px;
  1925. }
  1926. .documents-container {
  1927. padding: 20px;
  1928. }
  1929. .header-section {
  1930. display: flex;
  1931. justify-content: space-between;
  1932. align-items: center;
  1933. margin-bottom: 20px;
  1934. }
  1935. .title-info h2 {
  1936. margin: 0;
  1937. font-size: 24px;
  1938. color: #303133;
  1939. }
  1940. .statistics-bar {
  1941. display: flex;
  1942. gap: 20px;
  1943. margin-top: 8px;
  1944. }
  1945. .conversion-progress-wrapper {
  1946. width: 100%;
  1947. padding: 0 5px;
  1948. display: flex;
  1949. flex-direction: column;
  1950. gap: 4px;
  1951. }
  1952. .error-msg-text {
  1953. font-size: 12px;
  1954. color: #f56c6c;
  1955. display: flex;
  1956. align-items: center;
  1957. gap: 4px;
  1958. white-space: nowrap;
  1959. overflow: hidden;
  1960. text-overflow: ellipsis;
  1961. margin-top: 2px;
  1962. }
  1963. .stat-item {
  1964. display: flex;
  1965. align-items: center;
  1966. gap: 6px;
  1967. font-size: 14px;
  1968. color: #606266;
  1969. }
  1970. .stat-item .el-icon {
  1971. font-size: 16px;
  1972. color: #909399;
  1973. }
  1974. .stat-value {
  1975. font-weight: bold;
  1976. color: #303133;
  1977. }
  1978. .stat-value.success {
  1979. color: #67c23a;
  1980. }
  1981. .clickable-filename {
  1982. color: #409eff;
  1983. cursor: pointer;
  1984. font-weight: 500;
  1985. transition: color 0.2s;
  1986. }
  1987. .clickable-filename:hover {
  1988. color: #66b1ff;
  1989. text-decoration: underline;
  1990. }
  1991. .action-btn {
  1992. padding: 4px 8px;
  1993. height: auto;
  1994. font-size: 13px;
  1995. display: flex;
  1996. align-items: center;
  1997. gap: 4px;
  1998. }
  1999. .action-btn-icon {
  2000. padding: 4px;
  2001. height: 28px;
  2002. width: 28px;
  2003. display: flex;
  2004. align-items: center;
  2005. justify-content: center;
  2006. border-radius: 4px;
  2007. transition: all 0.2s;
  2008. }
  2009. .action-btn-icon:hover {
  2010. background-color: #f5f7fa;
  2011. }
  2012. .action-btn-icon .el-icon {
  2013. font-size: 16px;
  2014. }
  2015. .action-btn .el-icon {
  2016. font-size: 14px;
  2017. }
  2018. .file-info-cell {
  2019. display: flex;
  2020. align-items: center;
  2021. gap: 8px;
  2022. }
  2023. .file-icon-mini {
  2024. display: flex;
  2025. align-items: center;
  2026. justify-content: center;
  2027. width: 24px;
  2028. height: 24px;
  2029. border-radius: 4px;
  2030. font-size: 14px;
  2031. }
  2032. .file-icon-mini.pdf { background-color: #fef0f0; color: #f56c6c; }
  2033. .file-icon-mini.word { background-color: #ecf5ff; color: #409eff; }
  2034. .file-icon-mini.excel { background-color: #f0f9eb; color: #67c23a; }
  2035. .file-icon-mini.ppt { background-color: #fff7e6; color: #e6a23c; }
  2036. .file-info-content {
  2037. display: flex;
  2038. flex-direction: column;
  2039. gap: 4px;
  2040. overflow: hidden;
  2041. }
  2042. .file-note {
  2043. font-size: 12px;
  2044. color: #909399;
  2045. white-space: nowrap;
  2046. overflow: hidden;
  2047. text-overflow: ellipsis;
  2048. line-height: 1.2;
  2049. }
  2050. .file-name-link {
  2051. color: #409eff;
  2052. cursor: pointer;
  2053. font-weight: 500;
  2054. white-space: nowrap;
  2055. overflow: hidden;
  2056. text-overflow: ellipsis;
  2057. }
  2058. .file-name-link:hover {
  2059. text-decoration: underline;
  2060. }
  2061. .compact-info {
  2062. display: flex;
  2063. flex-direction: column;
  2064. gap: 2px;
  2065. line-height: 1.2;
  2066. }
  2067. .info-row {
  2068. display: flex;
  2069. align-items: center;
  2070. gap: 4px;
  2071. font-size: 12px;
  2072. color: #606266;
  2073. }
  2074. .info-row.secondary {
  2075. color: #909399;
  2076. font-size: 11px;
  2077. }
  2078. .date-cell {
  2079. display: flex;
  2080. flex-direction: column;
  2081. line-height: 1.2;
  2082. }
  2083. .time-mini {
  2084. font-size: 11px;
  2085. color: #909399;
  2086. }
  2087. .conversion-cell {
  2088. display: flex;
  2089. flex-direction: column;
  2090. align-items: flex-start;
  2091. gap: 4px;
  2092. padding: 2px 0;
  2093. }
  2094. .status-tag {
  2095. font-weight: 500;
  2096. margin-bottom: 2px;
  2097. }
  2098. .converted-file-links {
  2099. display: flex;
  2100. flex-direction: column;
  2101. gap: 0px;
  2102. width: 100%;
  2103. }
  2104. .converted-file-name {
  2105. font-size: 12px;
  2106. line-height: 1.2;
  2107. white-space: nowrap;
  2108. overflow: hidden;
  2109. text-overflow: ellipsis;
  2110. }
  2111. .converted-file-name :deep(.el-link) {
  2112. font-size: 12px;
  2113. justify-content: flex-start;
  2114. }
  2115. .converted-file-name :deep(.el-link .el-icon) {
  2116. margin-right: 4px;
  2117. }
  2118. .file-name-mini {
  2119. font-size: 11px;
  2120. color: #909399;
  2121. white-space: nowrap;
  2122. overflow: hidden;
  2123. text-overflow: ellipsis;
  2124. }
  2125. .status-icon-success { color: #67c23a; font-size: 18px; }
  2126. .status-icon-info { color: #909399; font-size: 18px; }
  2127. .upload-btn {
  2128. background-color: #67c23a;
  2129. border-color: #67c23a;
  2130. }
  2131. .search-card {
  2132. margin-bottom: 20px;
  2133. background-color: #f8f9fa;
  2134. }
  2135. .search-bar {
  2136. display: flex;
  2137. flex-direction: column;
  2138. gap: 16px;
  2139. }
  2140. .search-input :deep(.el-input__wrapper) {
  2141. padding: 8px 12px;
  2142. font-size: 16px;
  2143. }
  2144. .filter-group {
  2145. display: flex;
  2146. gap: 12px;
  2147. align-items: center;
  2148. flex-wrap: wrap;
  2149. }
  2150. .filter-select {
  2151. width: 200px;
  2152. }
  2153. .filter-select-year {
  2154. width: 140px;
  2155. }
  2156. .search-btn {
  2157. padding: 0 30px;
  2158. height: 40px;
  2159. font-size: 15px;
  2160. font-weight: bold;
  2161. margin-left: auto;
  2162. }
  2163. .content-section {
  2164. background: #fff;
  2165. border-radius: 4px;
  2166. min-height: 400px;
  2167. }
  2168. .pagination-container {
  2169. margin-top: 20px;
  2170. display: flex;
  2171. justify-content: flex-end;
  2172. }
  2173. .dynamic-input-row {
  2174. display: flex;
  2175. align-items: center;
  2176. margin-bottom: 8px;
  2177. width: 100%;
  2178. }
  2179. .dynamic-input-row :deep(.el-input) {
  2180. flex: 1;
  2181. }
  2182. .dynamic-input-row:last-child {
  2183. margin-bottom: 0;
  2184. }
  2185. .row-actions {
  2186. display: flex;
  2187. gap: 5px;
  2188. margin-left: 10px;
  2189. flex-shrink: 0;
  2190. }
  2191. /* 文件信息单元格布局 */
  2192. .file-info-cell {
  2193. display: flex;
  2194. align-items: center;
  2195. padding: 8px 0;
  2196. }
  2197. .file-icon-wrapper {
  2198. position: relative;
  2199. width: 40px;
  2200. height: 48px;
  2201. margin-right: 16px;
  2202. display: flex;
  2203. flex-direction: column;
  2204. align-items: center;
  2205. justify-content: center;
  2206. border-radius: 4px;
  2207. flex-shrink: 0;
  2208. transition: transform 0.2s;
  2209. }
  2210. .file-icon-wrapper:hover {
  2211. transform: scale(1.05);
  2212. }
  2213. .file-icon-wrapper .el-icon {
  2214. font-size: 24px;
  2215. margin-bottom: 2px;
  2216. color: #fff;
  2217. }
  2218. .file-type-label {
  2219. font-size: 10px;
  2220. font-weight: bold;
  2221. color: #fff;
  2222. text-transform: uppercase;
  2223. }
  2224. /* 不同文件类型的背景色 */
  2225. .icon-pdf { background-color: #ff4d4f; }
  2226. .icon-word { background-color: #1890ff; }
  2227. .icon-excel { background-color: #52c41a; }
  2228. .icon-ppt { background-color: #fa8c16; }
  2229. .icon-html { background-color: #13c2c2; }
  2230. .icon-file { background-color: #8c8c8c; }
  2231. .file-text-content {
  2232. display: flex;
  2233. flex-direction: column;
  2234. overflow: hidden;
  2235. }
  2236. .file-name-title {
  2237. font-size: 15px;
  2238. font-weight: 500;
  2239. color: #303133;
  2240. margin-bottom: 4px;
  2241. cursor: pointer;
  2242. white-space: nowrap;
  2243. overflow: hidden;
  2244. text-overflow: ellipsis;
  2245. transition: color 0.2s;
  2246. }
  2247. .file-name-title:hover {
  2248. color: #409eff;
  2249. }
  2250. .file-description-subtitle {
  2251. font-size: 12px;
  2252. color: #909399;
  2253. white-space: nowrap;
  2254. overflow: hidden;
  2255. text-overflow: ellipsis;
  2256. }
  2257. /* 操作按钮样式 */
  2258. .action-buttons {
  2259. display: flex;
  2260. justify-content: center;
  2261. align-items: center;
  2262. gap: 4px;
  2263. }
  2264. .dialog-header-custom {
  2265. display: flex;
  2266. justify-content: space-between;
  2267. align-items: center;
  2268. padding-right: 30px;
  2269. }
  2270. .header-actions {
  2271. display: flex;
  2272. gap: 10px;
  2273. }
  2274. .preview-tip {
  2275. margin-bottom: 15px;
  2276. }
  2277. .preview-content {
  2278. height: 75vh;
  2279. display: flex;
  2280. flex-direction: column;
  2281. border: 1px solid #dcdfe6;
  2282. border-radius: 4px;
  2283. background-color: #f5f7fa;
  2284. position: relative;
  2285. }
  2286. .unsupported-preview {
  2287. flex: 1;
  2288. display: flex;
  2289. align-items: center;
  2290. justify-content: center;
  2291. background-color: #fff;
  2292. }
  2293. .unsupported-actions {
  2294. display: flex;
  2295. gap: 12px;
  2296. justify-content: center;
  2297. }
  2298. .preview-content iframe {
  2299. flex: 1;
  2300. display: block;
  2301. }
  2302. :deep(.preview-dialog) {
  2303. .el-dialog__body {
  2304. padding: 10px 20px 20px;
  2305. }
  2306. }
  2307. .kb-name-tag {
  2308. display: inline-flex;
  2309. align-items: center;
  2310. gap: 4px;
  2311. color: var(--el-color-primary);
  2312. font-weight: 500;
  2313. }
  2314. .text-info {
  2315. color: var(--el-text-color-placeholder);
  2316. font-size: 13px;
  2317. }
  2318. </style>