index.vue 44 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229
  1. <!-- 质量检测 — 质检任务 -->
  2. <!-- eslint-disable vue/no-v-model-argument -->
  3. <template>
  4. <div class="page-container">
  5. <header class="page-head">
  6. <h1>质检任务</h1>
  7. <p class="page-desc">按检测项目跟进任务;实验室自检需开始后再填写结果,第三方在填写结果时一并上传检测材料。</p>
  8. </header>
  9. <section class="panel">
  10. <div class="panel-top">
  11. <h2>任务列表</h2>
  12. </div>
  13. <div class="filter-block">
  14. <el-form :model="searchForm" class="filter-form" @submit.prevent>
  15. <div class="filter-grid">
  16. <el-form-item label="关键词">
  17. <el-input
  18. v-model="searchForm.keyword"
  19. placeholder="任务号/工单号/样本编码/项目"
  20. clearable
  21. />
  22. </el-form-item>
  23. <el-form-item label="业务类型">
  24. <el-select v-model="searchForm.qcBizType" placeholder="全部" clearable style="width: 100%">
  25. <el-option label="入库质检" value="inbound" />
  26. <el-option label="出库质检" value="outbound" />
  27. <el-option label="临时质检" value="temporary" />
  28. </el-select>
  29. </el-form-item>
  30. <el-form-item label="任务类型">
  31. <el-select v-model="searchForm.taskType" placeholder="全部" clearable style="width: 100%">
  32. <el-option label="实验室自检" value="sysz" />
  33. <el-option label="第三方检测" value="dsfjc" />
  34. </el-select>
  35. </el-form-item>
  36. <el-form-item label="任务状态">
  37. <el-select v-model="searchForm.taskStatus" placeholder="全部" clearable style="width: 100%">
  38. <el-option label="待执行" value="dzx" />
  39. <el-option label="进行中" value="jxz" />
  40. <el-option label="已完成" value="ywc" />
  41. </el-select>
  42. </el-form-item>
  43. </div>
  44. <div class="filter-actions-row">
  45. <div class="filter-actions-end">
  46. <el-button class="btn-ghost" @click="handleReset"><el-icon><RefreshLeft /></el-icon>重置</el-button>
  47. <el-button class="btn-main" type="primary" @click="handleSearch"><el-icon><Search /></el-icon>搜索</el-button>
  48. </div>
  49. </div>
  50. </el-form>
  51. </div>
  52. <div class="list-body list-wrap">
  53. <el-table class="list-table" :data="tableData" v-loading="loading" stripe @row-dblclick="handleView">
  54. <el-table-column type="index" label="序号" min-width="68" align="center" :index="(i) => (pagination.page - 1) * pagination.pageSize + i + 1" />
  55. <el-table-column prop="taskNo" label="任务号" min-width="150" align="center" />
  56. <el-table-column prop="qcBizTypeLabel" label="业务类型" min-width="100" align="center">
  57. <template #default="{ row }">
  58. <el-tag :type="qcBizTypeTagType(row.qcBizType || '')" size="small">
  59. {{ row.qcBizTypeLabel || qcBizTypeLabel(row.qcBizType || '') }}
  60. </el-tag>
  61. </template>
  62. </el-table-column>
  63. <el-table-column prop="workOrderNo" label="工单号" min-width="140" align="center" />
  64. <el-table-column prop="orderSource" label="来源" min-width="100" align="center">
  65. <template #default="{ row }">
  66. <el-tag :type="row.orderSource === 'temporary' ? 'warning' : 'success'" size="small">
  67. {{ row.orderSource === 'temporary' ? '临时' : '入库' }}
  68. </el-tag>
  69. </template>
  70. </el-table-column>
  71. <el-table-column prop="receptionSourceLabel" label="样本来源" min-width="100" align="center">
  72. <template #default="{ row }">
  73. <span v-if="shouldShowTempEmptyReceptionSource(row)">—</span>
  74. <el-tag
  75. v-else
  76. :type="row.receptionSource === 'research_donation' ? 'warning' : 'success'"
  77. size="small"
  78. >
  79. {{ row.receptionSourceLabel || receptionSourceLabel(row.receptionSource || '') }}
  80. </el-tag>
  81. </template>
  82. </el-table-column>
  83. <el-table-column prop="sampleTypeName" label="样本类型" min-width="110" align="center">
  84. <template #default="{ row }">{{ row.sampleTypeName || '—' }}</template>
  85. </el-table-column>
  86. <el-table-column prop="sampleCode" label="样本编码" min-width="120" align="center">
  87. <template #default="{ row }">{{ row.sampleCode || '—' }}</template>
  88. </el-table-column>
  89. <el-table-column prop="receptionTime" label="接收时间" min-width="170" align="center">
  90. <template #default="{ row }">{{ row.receptionTime || '—' }}</template>
  91. </el-table-column>
  92. <el-table-column prop="handlerName" label="接收经手人" min-width="110" align="center">
  93. <template #default="{ row }">{{ row.handlerName || '—' }}</template>
  94. </el-table-column>
  95. <el-table-column prop="taskType" label="检测类型" min-width="110" align="center">
  96. <template #default="{ row }">
  97. <el-tag :type="row.taskType === 'sysz' ? 'success' : 'info'" size="small">{{ taskTypeLabel(row.taskType) }}</el-tag>
  98. </template>
  99. </el-table-column>
  100. <el-table-column prop="itemName" label="检测项目" min-width="160" align="center" />
  101. <el-table-column prop="companyName" label="第三方公司" min-width="120" align="center">
  102. <template #default="{ row }">{{ row.companyName || '—' }}</template>
  103. </el-table-column>
  104. <el-table-column prop="conclusion" label="质检结论" min-width="100" align="center">
  105. <template #default="{ row }">
  106. <el-tag
  107. v-if="row.conclusion === 'tg'"
  108. type="success"
  109. size="small"
  110. >
  111. {{ row.conclusionLabel || qcTaskConclusionLabel(row.conclusion) }}
  112. </el-tag>
  113. <el-tag
  114. v-else-if="row.conclusion === 'btg'"
  115. type="danger"
  116. size="small"
  117. >
  118. {{ row.conclusionLabel || qcTaskConclusionLabel(row.conclusion) }}
  119. </el-tag>
  120. <el-tag v-else type="info" size="small">—</el-tag>
  121. </template>
  122. </el-table-column>
  123. <el-table-column prop="taskStatus" label="状态" min-width="100" align="center">
  124. <template #default="{ row }">
  125. <el-tag :type="row.taskStatus === 'ywc' ? 'success' : row.taskStatus === 'jxz' ? 'warning' : 'info'" size="small">
  126. {{ taskStatusLabel(row.taskStatus) }}
  127. </el-tag>
  128. </template>
  129. </el-table-column>
  130. <el-table-column prop="remark" label="备注" min-width="140" align="center" show-overflow-tooltip>
  131. <template #default="{ row }">{{ row.remark || '—' }}</template>
  132. </el-table-column>
  133. <el-table-column label="操作" min-width="320" fixed="right" align="center">
  134. <template #default="{ row }">
  135. <template v-if="row.taskType === 'sysz' && row.taskStatus === 'dzx'">
  136. <el-button class="btn-op-item primary" @click="handleStart(row)"><el-icon><VideoPlay /></el-icon>开始</el-button>
  137. </template>
  138. <template v-if="canFillResult(row)">
  139. <el-button class="btn-op-item primary" @click="openResult(row)"><el-icon><EditPen /></el-icon>填写结果</el-button>
  140. </template>
  141. <el-button class="btn-op-item" @click="handleView(row)"><el-icon><View /></el-icon>查看</el-button>
  142. <el-button v-if="row.taskStatus === 'dzx'" class="btn-op-item" @click="handleDelete(row)">删除</el-button>
  143. </template>
  144. </el-table-column>
  145. </el-table>
  146. <div class="pagination list-pager-bar">
  147. <el-pagination
  148. :current-page="pagination.page"
  149. :page-size="pagination.pageSize"
  150. :total="pagination.total"
  151. layout="total, sizes, prev, pager, next"
  152. @size-change="(v) => ((pagination.page = 1), (pagination.pageSize = v), loadData())"
  153. @current-change="(v) => ((pagination.page = v), loadData())"
  154. />
  155. </div>
  156. </div>
  157. <el-dialog
  158. v-model="resultVisible"
  159. title="填写检测结果"
  160. :width="resultDialogWidth"
  161. class="dialog-form-layout qc-task-result-dialog"
  162. align-center
  163. destroy-on-close
  164. :close-on-click-modal="false"
  165. >
  166. <DialogFormBody
  167. :loading="resultLoading"
  168. >
  169. <el-form label-position="top" class="biz-dialog-form">
  170. <div class="form-section">
  171. <div class="section-title">检测参数</div>
  172. <div
  173. v-if="!resultLoading && resultParams.length === 0"
  174. style="text-align: center; padding: 8px 0; color: #94a3b8; font-size: 13px"
  175. >
  176. 该项目暂无配置参数,请先在实验室/第三方项目中维护检测参数
  177. </div>
  178. <div class="form-grid-3">
  179. <el-form-item
  180. v-for="(param, idx) in resultParams"
  181. :key="param.paramId || param.paramCode || idx"
  182. :label="param.paramName + (param.paramUnit ? `(${param.paramUnit})` : '')"
  183. >
  184. <el-input v-if="param.paramType === 'text'" v-model="param.paramValue" placeholder="请输入" />
  185. <el-input-number
  186. v-else
  187. :model-value="toResultNumber(param.paramValue)"
  188. :precision="2"
  189. style="width: 100%"
  190. placeholder="请输入"
  191. @update:model-value="(v) => setResultNumber(param, v)"
  192. />
  193. </el-form-item>
  194. </div>
  195. </div>
  196. <div v-if="resultTaskType !== 'dsfjc'" class="form-section material-usage-section">
  197. <div class="section-title material-usage-head">
  198. <div class="section-title-left"><h3>物料使用(选填)</h3></div>
  199. <el-button class="btn-main" type="primary" size="small" @click="addMaterialRow">
  200. <el-icon><Plus /></el-icon>添加物料
  201. </el-button>
  202. </div>
  203. <el-table
  204. v-if="materialUsages.length"
  205. :data="materialUsages"
  206. :row-key="(row) => row._rowKey"
  207. stripe
  208. class="list-table inner-table material-usage-table"
  209. max-height="280"
  210. >
  211. <el-table-column
  212. type="index"
  213. label="序号"
  214. min-width="56"
  215. align="center"
  216. header-align="center"
  217. :index="(index) => index + 1"
  218. />
  219. <el-table-column label="物料名称" min-width="160" align="center" header-align="center">
  220. <template #default="{ row, $index }">
  221. <el-select
  222. v-model="row.materialId"
  223. filterable
  224. placeholder="搜索并选择物料"
  225. class="inner-cell-input"
  226. @change="(v) => onMaterialSelect($index, v)"
  227. >
  228. <el-option
  229. v-for="opt in materialOptions"
  230. :key="opt.id"
  231. :label="opt.label"
  232. :value="opt.id"
  233. :disabled="isMaterialOptionDisabled(opt.id, $index)"
  234. />
  235. </el-select>
  236. </template>
  237. </el-table-column>
  238. <el-table-column label="规格型号" min-width="120" align="center" header-align="center">
  239. <template #default="{ row }">{{ row.specModel || '—' }}</template>
  240. </el-table-column>
  241. <el-table-column label="用量" min-width="120" align="center" header-align="center">
  242. <template #default="{ row }">
  243. <el-input-number
  244. v-model="row.qty"
  245. :min="0.0001"
  246. :precision="4"
  247. :step="1"
  248. controls-position="right"
  249. placeholder="用量"
  250. class="inner-cell-input"
  251. />
  252. </template>
  253. </el-table-column>
  254. <el-table-column label="单位" min-width="72" align="center" header-align="center">
  255. <template #default="{ row }">{{ row.unit || '—' }}</template>
  256. </el-table-column>
  257. <el-table-column label="备注" min-width="140" align="center" header-align="center">
  258. <template #default="{ row }">
  259. <el-input v-model="row.remark" placeholder="选填" clearable class="inner-cell-input" />
  260. </template>
  261. </el-table-column>
  262. <el-table-column label="操作" min-width="96" align="center" header-align="center" fixed="right">
  263. <template #default="{ $index }">
  264. <el-button link type="danger" size="small" @click="removeMaterialRow($index)">
  265. 删除
  266. </el-button>
  267. </template>
  268. </el-table-column>
  269. </el-table>
  270. <el-empty
  271. v-else
  272. description="暂无物料记录,可点击上方按钮添加"
  273. :image-size="56"
  274. />
  275. </div>
  276. <div class="form-section result-conclusion-section">
  277. <div class="section-title">检测结论</div>
  278. <el-form-item label="结论" required>
  279. <el-radio-group v-model="resultConclusion">
  280. <el-radio value="tg">通过</el-radio>
  281. <el-radio value="btg">不通过</el-radio>
  282. </el-radio-group>
  283. </el-form-item>
  284. <el-form-item label="备注">
  285. <el-input v-model="resultRemark" type="textarea" :rows="3" placeholder="选填" />
  286. </el-form-item>
  287. <div class="result-attachment-block">
  288. <ProtocolFinanceConfirmFields
  289. v-model:finance-remark="resultRemark"
  290. v-model:attachments="resultAttachments"
  291. :show-remark="false"
  292. :attachment-label="resultAttachmentLabel"
  293. :upload-folder="resultUploadFolder"
  294. />
  295. </div>
  296. </div>
  297. </el-form>
  298. </DialogFormBody>
  299. <template #footer>
  300. <el-button class="btn-ghost" @click="resultVisible = false"><el-icon><Close /></el-icon>取消</el-button>
  301. <el-button class="btn-main" type="primary" :loading="resultLoading" @click="submitResult"><el-icon><Check /></el-icon>{{ resultSubmitLabel }}</el-button>
  302. </template>
  303. </el-dialog>
  304. <SystemViewDialog
  305. v-model:visible="viewVisible"
  306. title="任务详情"
  307. width="960px"
  308. :grid-columns="3"
  309. :icon="Tickets"
  310. :meta="viewDetail?.taskNo || ''"
  311. :sections="viewSections"
  312. >
  313. <template #reception-extra>
  314. <div class="view-reception-attachments">
  315. <div class="view-kv view-kv-span-2">
  316. <dt>接收附件</dt>
  317. <dd>
  318. <ProtocolAttachmentReadonly
  319. :files="viewReceptionAttachmentFiles"
  320. @preview="handleReceptionAttachmentPreview"
  321. @download="handleReceptionAttachmentDownload"
  322. />
  323. </dd>
  324. </div>
  325. </div>
  326. </template>
  327. <template #material-usages>
  328. <div v-if="viewMaterialUsages.length" class="view-subtable-wrap">
  329. <el-table :data="viewMaterialUsages" stripe class="list-table view-subtable">
  330. <el-table-column
  331. type="index"
  332. label="序号"
  333. min-width="56"
  334. align="center"
  335. header-align="center"
  336. :index="(index) => index + 1"
  337. />
  338. <el-table-column
  339. prop="materialName"
  340. label="物料名称"
  341. min-width="160"
  342. align="center"
  343. header-align="center"
  344. />
  345. <el-table-column label="规格型号" min-width="120" align="center" header-align="center">
  346. <template #default="{ row }">{{ row.specModel || '—' }}</template>
  347. </el-table-column>
  348. <el-table-column label="用量" min-width="100" align="center" header-align="center">
  349. <template #default="{ row }">
  350. {{ row.qty != null && row.qty !== '' ? row.qty : '—' }}
  351. </template>
  352. </el-table-column>
  353. <el-table-column label="单位" min-width="72" align="center" header-align="center">
  354. <template #default="{ row }">{{ row.unit || '—' }}</template>
  355. </el-table-column>
  356. <el-table-column label="备注" min-width="140" align="center" header-align="center">
  357. <template #default="{ row }">{{ row.remark || '—' }}</template>
  358. </el-table-column>
  359. </el-table>
  360. </div>
  361. <p v-else class="view-empty-hint">暂无物料使用记录</p>
  362. </template>
  363. <template #task-attachments>
  364. <ProtocolAttachmentReadonly
  365. :files="viewDetail?.attachmentFiles || []"
  366. @preview="handleViewAttachmentPreview"
  367. @download="handleViewAttachmentDownload"
  368. />
  369. </template>
  370. </SystemViewDialog>
  371. </section>
  372. </div>
  373. </template>
  374. <script setup lang="ts">
  375. import { ref, reactive, computed, onMounted } from 'vue'
  376. import { ElMessage, ElMessageBox } from 'element-plus'
  377. import { Close, Check, Search, RefreshLeft, VideoPlay, View, Tickets, EditPen, Plus } from '@element-plus/icons-vue'
  378. import { formatMoneyDisplay } from '@/utils/money-format'
  379. import ProtocolFinanceConfirmFields from '@/components/business/protocol-finance-confirm-fields/index.vue'
  380. import ProtocolAttachmentReadonly from '@/views/cell-storage/protocol/ProtocolAttachmentReadonly.vue'
  381. import DialogFormBody from '@/components/business/dialog-form-body/index.vue'
  382. import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
  383. import type { ViewSection } from '@/components/business/system-view-dialog/index.vue'
  384. import {
  385. financeAttachmentsToUrlString,
  386. type FinanceAttachmentItem
  387. } from '@/components/business/protocol-finance-confirm-fields/finance-attachments'
  388. import {
  389. fetchQcTaskList,
  390. fetchQcTaskDetail,
  391. fetchStartQcTask,
  392. fetchCreateQcTask,
  393. fetchDeleteQcTask,
  394. type QcTask,
  395. type QcTaskDetail
  396. } from '@/api/qc-task'
  397. import {
  398. fetchQcTaskResults,
  399. fetchQcTaskMaterialUsages,
  400. fetchCompleteTaskWithResult,
  401. type QcTaskResultItem,
  402. type QcTaskMaterialUsageItem
  403. } from '@/api/qc-task-result'
  404. import { fetchQcMaterialOptions, type QcMaterialOption } from '@/api/qc-material'
  405. import {
  406. taskTypeLabel,
  407. taskStatusLabel,
  408. qcBizTypeLabel,
  409. qcBizTypeTagType,
  410. receptionSourceLabel,
  411. acceptConclusionLabel,
  412. transportTypeLabel,
  413. workOrderStatusLabel,
  414. qcTaskConclusionLabel
  415. } from '@/utils/qc-labels'
  416. import {
  417. protocolStatusLabel,
  418. protocolStatusTagType,
  419. donationStatusLabel,
  420. donationStatusTagType,
  421. } from '@/utils/status-tag-registry'
  422. import type { TagType } from '@/utils/status-tag-registry'
  423. /** 根据来源类型返回协议状态标签 */
  424. function protocolStatusLabelBySource(status: string, source: string): string {
  425. return source === 'research_donation' ? donationStatusLabel(status) : protocolStatusLabel(status)
  426. }
  427. function protocolStatusTagTypeBySource(status: string, source: string): TagType {
  428. return source === 'research_donation' ? donationStatusTagType(status) : protocolStatusTagType(status)
  429. }
  430. /** 临时来源任务无关联样本,样本来源展示为 — */
  431. function shouldShowTempEmptyReceptionSource(row: { orderSource?: string }) {
  432. return row.orderSource === 'temporary'
  433. }
  434. import { downloadAttachmentFile } from '@/utils/download-attachment'
  435. defineOptions({ name: 'QualityInspectionQcTask' })
  436. const addVisible = ref(false)
  437. const addLoading = ref(false)
  438. const addFormRef = ref()
  439. const addForm = reactive({
  440. taskType: 'sysz',
  441. itemCode: '',
  442. itemName: '',
  443. companyId: '',
  444. companyName: '',
  445. feeAmount: undefined as number | undefined,
  446. sampleCode: '',
  447. qcBizType: 'inbound',
  448. remark: ''
  449. })
  450. const addRules = {
  451. taskType: [{ required: true, message: '请选择检测类型', trigger: 'change' }],
  452. itemCode: [{ required: true, message: '请输入项目编码', trigger: 'blur' }],
  453. itemName: [{ required: true, message: '请输入项目名称', trigger: 'blur' }]
  454. }
  455. function handleAdd() {
  456. addForm.taskType = 'sysz'
  457. addForm.itemCode = ''
  458. addForm.itemName = ''
  459. addForm.companyId = ''
  460. addForm.companyName = ''
  461. addForm.feeAmount = undefined
  462. addForm.sampleCode = ''
  463. addForm.qcBizType = 'inbound'
  464. addForm.remark = ''
  465. addVisible.value = true
  466. }
  467. async function submitAdd() {
  468. await addFormRef.value?.validate()
  469. addLoading.value = true
  470. try {
  471. const payload: Record<string, any> = {
  472. taskType: addForm.taskType,
  473. itemCode: addForm.itemCode,
  474. itemName: addForm.itemName,
  475. sampleCode: addForm.sampleCode || undefined,
  476. qcBizType: addForm.qcBizType,
  477. remark: addForm.remark || undefined
  478. }
  479. if (addForm.taskType === 'dsfjc') {
  480. payload.companyId = addForm.companyId || undefined
  481. payload.companyName = addForm.companyName || undefined
  482. payload.feeAmount = addForm.feeAmount
  483. }
  484. await fetchCreateQcTask(payload)
  485. ElMessage.success('创建成功')
  486. addVisible.value = false
  487. loadData()
  488. } finally {
  489. addLoading.value = false
  490. }
  491. }
  492. const searchForm = reactive({ keyword: '', qcBizType: '', taskType: '', taskStatus: '' })
  493. const tableData = ref<QcTask[]>([])
  494. const loading = ref(false)
  495. const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
  496. const resultVisible = ref(false)
  497. const resultLoading = ref(false)
  498. const resultTaskId = ref('')
  499. const resultTaskType = ref('')
  500. const resultTaskStatus = ref('')
  501. const resultParams = ref<QcTaskResultItem[]>([])
  502. const resultRemark = ref('')
  503. const resultConclusion = ref('')
  504. const resultAttachments = ref<FinanceAttachmentItem[]>([])
  505. type MaterialUsageRow = {
  506. _rowKey: string
  507. materialId: string
  508. materialName: string
  509. specModel: string
  510. unit: string
  511. qty: number | undefined
  512. remark: string
  513. }
  514. let materialUsageRowSeq = 0
  515. function nextMaterialRowKey() {
  516. materialUsageRowSeq += 1
  517. return `mu_${Date.now()}_${materialUsageRowSeq}`
  518. }
  519. const materialOptions = ref<QcMaterialOption[]>([])
  520. const materialUsages = ref<MaterialUsageRow[]>([])
  521. const resultDialogWidth = computed(() =>
  522. resultTaskType.value === 'sysz' ? '970px' : '720px'
  523. )
  524. function canFillResult(row: QcTask) {
  525. if (row.taskType === 'sysz') {
  526. return row.taskStatus === 'jxz' || row.taskStatus === 'ywc'
  527. }
  528. if (row.taskType === 'dsfjc') {
  529. return row.taskStatus === 'dzx' || row.taskStatus === 'jxz' || row.taskStatus === 'ywc'
  530. }
  531. return false
  532. }
  533. const resultSubmitLabel = computed(() =>
  534. resultTaskStatus.value === 'ywc' ? '保存' : '提交并完成'
  535. )
  536. const resultAttachmentLabel = computed(() =>
  537. resultTaskType.value === 'dsfjc' ? '检测材料' : '检测附件'
  538. )
  539. const resultUploadFolder = computed(() =>
  540. resultTaskType.value === 'dsfjc' ? 'qc-third-party' : 'qc-lab'
  541. )
  542. function clearMaterialRow(row: MaterialUsageRow) {
  543. row.materialId = ''
  544. row.materialName = ''
  545. row.specModel = ''
  546. row.unit = ''
  547. }
  548. function isMaterialOptionDisabled(materialId: string, currentIndex: number) {
  549. if (!materialId) return false
  550. return materialUsages.value.some((r, i) => i !== currentIndex && r.materialId === materialId)
  551. }
  552. function isDuplicateMaterial(materialId: string, excludeIndex: number) {
  553. if (!materialId) return false
  554. return materialUsages.value.some((r, i) => i !== excludeIndex && r.materialId === materialId)
  555. }
  556. function parseMaterialQty(qty: number | string | undefined): number | null {
  557. if (qty === undefined || qty === null || qty === '') return null
  558. const n = typeof qty === 'number' ? qty : Number(String(qty).trim())
  559. if (!Number.isFinite(n) || n <= 0) return null
  560. return n
  561. }
  562. function onMaterialSelect(index: number, id: string) {
  563. const row = materialUsages.value[index]
  564. if (!row) return
  565. if (!id) {
  566. if (parseMaterialQty(row.qty) == null) {
  567. removeMaterialRow(index)
  568. } else {
  569. clearMaterialRow(row)
  570. }
  571. return
  572. }
  573. if (isDuplicateMaterial(id, index)) {
  574. ElMessage.warning('该物料已在列表中,请勿重复添加')
  575. clearMaterialRow(row)
  576. return
  577. }
  578. const opt = materialOptions.value.find((o) => o.id === id)
  579. row.materialId = id
  580. row.materialName = opt?.materialName || ''
  581. row.specModel = opt?.specModel || ''
  582. row.unit = opt?.unit || ''
  583. }
  584. function addMaterialRow() {
  585. materialUsages.value.push({
  586. _rowKey: nextMaterialRowKey(),
  587. materialId: '',
  588. materialName: '',
  589. specModel: '',
  590. unit: '',
  591. qty: undefined,
  592. remark: ''
  593. })
  594. }
  595. function removeMaterialRow(index: number) {
  596. materialUsages.value = materialUsages.value.filter((_, i) => i !== index)
  597. }
  598. /** 合并同物料多条变动记录(历史重复扣减时),并生成稳定行 key */
  599. function materialRowsFromApi(items: QcTaskMaterialUsageItem[]): MaterialUsageRow[] {
  600. const merged = new Map<string, MaterialUsageRow>()
  601. for (const u of items || []) {
  602. const mid = (u.materialId || '').trim()
  603. if (!mid) continue
  604. const qtyRaw = Math.abs(Number(u.changeQty) || 0)
  605. const prev = merged.get(mid)
  606. if (prev) {
  607. prev.qty = (Number(prev.qty) || 0) + qtyRaw
  608. } else {
  609. merged.set(mid, {
  610. _rowKey: nextMaterialRowKey(),
  611. materialId: mid,
  612. materialName: u.materialName || '',
  613. specModel: '',
  614. unit: '',
  615. qty: qtyRaw > 0 ? qtyRaw : undefined,
  616. remark: u.remark || ''
  617. })
  618. }
  619. }
  620. return Array.from(merged.values())
  621. }
  622. function enrichMaterialUsageRows(
  623. rows: MaterialUsageRow[],
  624. options: QcMaterialOption[] = materialOptions.value
  625. ) {
  626. for (const row of rows) {
  627. const opt = options.find((o) => o.id === row.materialId)
  628. if (opt) {
  629. row.materialName = opt.materialName || row.materialName
  630. row.specModel = opt.specModel || ''
  631. row.unit = opt.unit || ''
  632. }
  633. }
  634. }
  635. const viewVisible = ref(false)
  636. const viewDetail = ref<QcTaskDetail | null>(null)
  637. const viewMaterialUsages = ref<MaterialUsageRow[]>([])
  638. const minioBaseUrl = (import.meta.env.VITE_MINIO_BASE_URL || '').replace(/\/$/, '')
  639. function apiFilesToFinanceItems(
  640. files?: Array<{ id?: string; fileUrl: string; fileName?: string; fileType?: string }>
  641. ): FinanceAttachmentItem[] {
  642. if (!files?.length) return []
  643. return files.map((f, i) => ({
  644. id: f.id || `att_${i}`,
  645. fileName: f.fileName || '',
  646. fileUrl: f.fileUrl || '',
  647. fileType: f.fileType || ''
  648. }))
  649. }
  650. function attachmentFullUrl(url: string) {
  651. const u = String(url || '').trim()
  652. if (!u) return ''
  653. if (/^https?:\/\//i.test(u)) return u
  654. return minioBaseUrl ? `${minioBaseUrl}/${u.replace(/^\//, '')}` : u
  655. }
  656. function handleViewAttachmentPreview(file: Record<string, unknown>) {
  657. const url = attachmentFullUrl(String(file.fileUrl || ''))
  658. if (url) window.open(url, '_blank')
  659. }
  660. function handleViewAttachmentDownload(file: Record<string, unknown>) {
  661. downloadAttachmentFile(file, { fullFileUrl: attachmentFullUrl })
  662. }
  663. const viewReceptionAttachmentFiles = computed(() => {
  664. const r = viewDetail.value?.reception
  665. if (!r) return []
  666. if (Array.isArray(r.receiveAttachmentFiles) && r.receiveAttachmentFiles.length) {
  667. return r.receiveAttachmentFiles as Array<Record<string, unknown>>
  668. }
  669. const urls = String(r.receiveAttachments || '')
  670. if (!urls.trim()) return []
  671. return urls
  672. .split(',')
  673. .map((u, i) => {
  674. const url = u.trim()
  675. if (!url) return null
  676. const name = url.includes('/') ? url.split('/').pop() || `附件${i + 1}` : url
  677. return { id: `url_${i}`, fileUrl: url, fileName: name }
  678. })
  679. .filter(Boolean) as Array<Record<string, unknown>>
  680. })
  681. function handleReceptionAttachmentPreview(file: Record<string, unknown>) {
  682. const url = attachmentFullUrl(String(file.fileUrl || ''))
  683. if (url) window.open(url, '_blank')
  684. }
  685. function handleReceptionAttachmentDownload(file: Record<string, unknown>) {
  686. downloadAttachmentFile(file, { fullFileUrl: attachmentFullUrl })
  687. }
  688. function normalizeResultParams(items: QcTaskResultItem[]): QcTaskResultItem[] {
  689. return (items || []).map((p) => ({
  690. ...p,
  691. paramType: p.paramType || 'text',
  692. paramValue: p.paramValue ?? ''
  693. }))
  694. }
  695. function toResultNumber(v: string | undefined) {
  696. if (v === '' || v === undefined || v === null) return undefined
  697. const n = Number(v)
  698. return Number.isFinite(n) ? n : undefined
  699. }
  700. function setResultNumber(param: QcTaskResultItem, v: number | undefined) {
  701. param.paramValue = v === undefined || v === null ? '' : String(v)
  702. }
  703. function hasResultValue(v: unknown) {
  704. return v !== '' && v !== undefined && v !== null
  705. }
  706. function formatMetric(v: unknown) {
  707. if (v === null || v === undefined || v === '') return '—'
  708. return String(v)
  709. }
  710. function formatCheckResults(items?: Array<{ itemName?: string; checked?: boolean }>) {
  711. if (!items?.length) return '—'
  712. return items.map((x) => `${x.itemName || ''}${x.checked ? '(✓)' : '(×)'}`).join('、')
  713. }
  714. const viewSections = computed<ViewSection[]>(() => {
  715. const d = viewDetail.value
  716. if (!d) return []
  717. const r = d.reception || {}
  718. const isTempQc = d.qcBizType === 'temporary'
  719. const transportFields: ViewSection['fields'] = [
  720. { label: '运输方式', value: transportTypeLabel(String(r.transportType || '')) }
  721. ]
  722. if (r.transportType === 'kdys') {
  723. transportFields.push(
  724. { label: '快递公司', value: r.expressCompanyName || '—' },
  725. { label: '快递单号', value: r.waybillNo || '—' }
  726. )
  727. } else if (r.transportType === 'ryxd') {
  728. transportFields.push(
  729. { label: '携带人', value: r.carrierName || '—' },
  730. { label: '携带人电话', value: r.carrierPhone || '—' }
  731. )
  732. }
  733. if (r.etaTime) {
  734. transportFields.push({ label: '预计到达', value: r.etaTime })
  735. }
  736. const cellProductSection: ViewSection[] = isTempQc
  737. ? [
  738. {
  739. title: '质检对象',
  740. fields: [
  741. { label: '产品编码', value: d.cellProductCode || '—' },
  742. { label: '产品名称', value: d.cellProductName || '—' },
  743. { label: '产品类型', value: d.cellProductTypeName || '—' },
  744. { label: '代次', value: d.cellProductPassage || '—' },
  745. { label: '规格', value: d.cellProductSpecName || '—' }
  746. ]
  747. }
  748. ]
  749. : []
  750. const sampleSections: ViewSection[] = isTempQc
  751. ? []
  752. : [
  753. {
  754. title: '样本信息',
  755. fields: [
  756. { label: '协议号', value: r.protocolNo || d.protocolNo || '—' },
  757. {
  758. label: '协议状态',
  759. value: protocolStatusLabelBySource(
  760. String(d.protocolStatus || r.protocolStatus || ''),
  761. d.receptionSource || r.sourceType || 'cell_storage'
  762. ),
  763. tagType: protocolStatusTagTypeBySource(
  764. String(d.protocolStatus || r.protocolStatus || ''),
  765. d.receptionSource || r.sourceType || 'cell_storage'
  766. )
  767. },
  768. { label: '样本编码', value: r.sampleCode || d.sampleCode || '—' },
  769. {
  770. label: '样本来源',
  771. value: r.sourceTypeLabel || d.receptionSourceLabel || receptionSourceLabel(d.receptionSource || ''),
  772. tagType: 'primary' as const
  773. },
  774. { label: '样本类型', value: r.sampleTypeName || d.sampleTypeName || '—' }
  775. ]
  776. },
  777. ...(transportFields.length > 1 || r.transportType
  778. ? [{ title: '运输信息', fields: transportFields }]
  779. : []),
  780. {
  781. title: '样本接收',
  782. slot: 'reception-extra',
  783. fields: [
  784. { label: '接收时间', value: r.receptionTime || d.receptionTime || '—' },
  785. { label: '接收经手人', value: r.handlerName || d.handlerName || '—' },
  786. {
  787. label: '接收温度(℃)',
  788. value: r.receiveTemperature ? `${r.receiveTemperature}℃` : '—'
  789. },
  790. { label: '体积(ml)', value: formatMetric(r.volumeMl) },
  791. { label: '净重(g)', value: formatMetric(r.netWeightG) },
  792. {
  793. label: '结论',
  794. value:
  795. r.acceptConclusionLabel ||
  796. acceptConclusionLabel(String(r.acceptConclusion || ''))
  797. },
  798. { label: '检查项', value: formatCheckResults(r.checkResults), span: true },
  799. { label: '备注', value: r.remark || '—', span: true }
  800. ]
  801. }
  802. ]
  803. const resultSection =
  804. d.results && d.results.length > 0
  805. ? [
  806. {
  807. title: '检测结果',
  808. fields: d.results.map((item) => ({
  809. label: item.paramName + (item.paramUnit ? `(${item.paramUnit})` : ''),
  810. value: item.paramValue || '—'
  811. }))
  812. }
  813. ]
  814. : []
  815. const tailSections: ViewSection[] = []
  816. if (d.taskType !== 'dsfjc') {
  817. tailSections.push({ title: '物料使用', fields: [], slot: 'material-usages' })
  818. }
  819. if (d.attachmentFiles?.length) {
  820. tailSections.push({
  821. title: d.taskType === 'dsfjc' ? '检测材料' : '检测附件',
  822. fields: [],
  823. slot: 'task-attachments'
  824. })
  825. }
  826. return [
  827. {
  828. title: '任务信息',
  829. fields: [
  830. { label: '任务号', value: d.taskNo },
  831. { label: '业务类型', value: d.qcBizTypeLabel || qcBizTypeLabel(d.qcBizType || ''), tagType: qcBizTypeTagType(d.qcBizType || '') },
  832. { label: '工单号', value: d.workOrderNo },
  833. {
  834. label: '工单状态',
  835. value: d.workOrderStatus ? workOrderStatusLabel(d.workOrderStatus) : '—',
  836. tagType: d.workOrderStatus === 'ywc' ? 'success' : d.workOrderStatus === 'ypz' ? 'warning' : 'info'
  837. },
  838. { label: '检测类型', value: taskTypeLabel(d.taskType), tagType: d.taskType === 'sysz' ? 'success' : 'info' },
  839. { label: '检测项目', value: d.itemName },
  840. { label: '项目编码', value: d.itemCode || '—' },
  841. { label: '第三方公司', value: d.companyName || '—' },
  842. { label: '检测费用', value: d.feeAmount != null ? '¥' + formatMoneyDisplay(d.feeAmount) : '—' },
  843. { label: '任务状态', value: taskStatusLabel(d.taskStatus), tagType: d.taskStatus === 'ywc' ? 'success' : d.taskStatus === 'jxz' ? 'warning' : 'info' },
  844. {
  845. label: '检测结论',
  846. value: d.conclusionLabel || qcTaskConclusionLabel(d.conclusion || ''),
  847. tagType: d.conclusion === 'tg' ? 'success' : d.conclusion === 'btg' ? 'danger' : 'info'
  848. },
  849. { label: '完成时间', value: d.completedTime || '—' },
  850. { label: '创建时间', value: d.createTime || '—' },
  851. { label: '备注', value: d.remark || '—', span: true }
  852. ]
  853. },
  854. ...cellProductSection,
  855. ...sampleSections,
  856. ...resultSection,
  857. ...tailSections
  858. ]
  859. })
  860. async function handleDelete(row: QcTask) {
  861. await ElMessageBox.confirm('确定要删除该质检任务吗?', '提示', { type: 'warning' })
  862. await fetchDeleteQcTask(row.id)
  863. ElMessage.success('删除成功')
  864. await loadData()
  865. }
  866. async function loadData() {
  867. loading.value = true
  868. try {
  869. const res = await fetchQcTaskList({
  870. page: pagination.page,
  871. pageSize: pagination.pageSize,
  872. keyword: searchForm.keyword || undefined,
  873. qcBizType: searchForm.qcBizType || undefined,
  874. taskType: searchForm.taskType || undefined,
  875. taskStatus: searchForm.taskStatus || undefined
  876. })
  877. tableData.value = res.list || []
  878. pagination.total = res.total ?? 0
  879. } finally {
  880. loading.value = false
  881. }
  882. }
  883. function handleSearch() {
  884. pagination.page = 1
  885. loadData()
  886. }
  887. function handleReset() {
  888. searchForm.keyword = ''
  889. searchForm.qcBizType = ''
  890. searchForm.taskType = ''
  891. searchForm.taskStatus = ''
  892. pagination.page = 1
  893. loadData()
  894. }
  895. async function handleStart(row: QcTask) {
  896. await fetchStartQcTask(row.id)
  897. ElMessage.success('已开始')
  898. loadData()
  899. }
  900. async function handleView(row: QcTask) {
  901. const detail = await fetchQcTaskDetail(row.id)
  902. try {
  903. const results = await fetchQcTaskResults(row.id)
  904. ;(detail as any).results = results || []
  905. } catch {
  906. ;(detail as any).results = []
  907. }
  908. viewMaterialUsages.value = []
  909. if (row.taskType !== 'dsfjc') {
  910. try {
  911. const [savedUsages, opts] = await Promise.all([
  912. fetchQcTaskMaterialUsages(row.id).catch(() => [] as QcTaskMaterialUsageItem[]),
  913. fetchQcMaterialOptions().catch(() => [] as QcMaterialOption[])
  914. ])
  915. const rows = materialRowsFromApi(savedUsages || [])
  916. enrichMaterialUsageRows(rows, opts || [])
  917. viewMaterialUsages.value = rows
  918. } catch {
  919. viewMaterialUsages.value = []
  920. }
  921. }
  922. viewDetail.value = detail
  923. viewVisible.value = true
  924. }
  925. async function openResult(row: QcTask) {
  926. if (!canFillResult(row)) {
  927. if (row.taskType === 'sysz') {
  928. ElMessage.warning('请先开始任务后再填写结果')
  929. } else {
  930. ElMessage.warning('当前状态不可填写结果')
  931. }
  932. return
  933. }
  934. resultTaskStatus.value = row.taskStatus
  935. resultTaskId.value = row.id
  936. resultTaskType.value = row.taskType
  937. resultRemark.value = row.remark || ''
  938. resultConclusion.value = ''
  939. resultAttachments.value = apiFilesToFinanceItems(row.attachmentFiles)
  940. materialUsages.value = []
  941. materialOptions.value = []
  942. resultLoading.value = true
  943. try {
  944. const detail = await fetchQcTaskDetail(row.id)
  945. resultTaskStatus.value = detail.taskStatus || row.taskStatus
  946. resultRemark.value = detail.remark || row.remark || ''
  947. resultConclusion.value = detail.conclusion || ''
  948. resultAttachments.value = apiFilesToFinanceItems(detail.attachmentFiles)
  949. const results = await fetchQcTaskResults(row.id)
  950. resultParams.value = normalizeResultParams(results || [])
  951. if (row.taskType !== 'dsfjc') {
  952. const [opts, savedUsages] = await Promise.all([
  953. fetchQcMaterialOptions().catch(() => [] as QcMaterialOption[]),
  954. fetchQcTaskMaterialUsages(row.id).catch(() => [] as QcTaskMaterialUsageItem[])
  955. ])
  956. materialOptions.value = opts || []
  957. materialUsages.value = materialRowsFromApi(savedUsages || [])
  958. enrichMaterialUsageRows(materialUsages.value)
  959. }
  960. } catch {
  961. resultParams.value = []
  962. materialUsages.value = []
  963. } finally {
  964. resultLoading.value = false
  965. }
  966. resultVisible.value = true
  967. }
  968. async function submitResult() {
  969. if (resultAttachments.value.some((a) => a._uploading)) {
  970. ElMessage.warning('附件正在上传,请稍候')
  971. return
  972. }
  973. if (!resultConclusion.value) {
  974. ElMessage.warning('请选择检测结论')
  975. return
  976. }
  977. if (resultTaskType.value === 'dsfjc' && !financeAttachmentsToUrlString(resultAttachments.value)) {
  978. ElMessage.warning('请上传第三方检测材料')
  979. return
  980. }
  981. if (resultTaskType.value !== 'dsfjc') {
  982. materialUsages.value = materialUsages.value.filter((mu) => !!mu.materialId)
  983. const seenMaterialIds = new Set<string>()
  984. for (const mu of materialUsages.value) {
  985. if (!mu.materialId) continue
  986. if (parseMaterialQty(mu.qty) == null) {
  987. ElMessage.warning('请为已选物料填写有效用量')
  988. return
  989. }
  990. if (seenMaterialIds.has(mu.materialId)) {
  991. ElMessage.warning('物料列表中存在重复物料,请检查')
  992. return
  993. }
  994. seenMaterialIds.add(mu.materialId)
  995. }
  996. }
  997. resultLoading.value = true
  998. try {
  999. const validMaterialUsages = materialUsages.value
  1000. .map((mu) => ({ ...mu, qtyNum: parseMaterialQty(mu.qty) }))
  1001. .filter((mu) => mu.materialId && mu.qtyNum != null)
  1002. const data: {
  1003. conclusion: string
  1004. results: QcTaskResultItem[]
  1005. remark?: string
  1006. thirdPartyAttachments?: string
  1007. materialUsages?: Array<{ materialId: string; qty: number; remark?: string }>
  1008. } = {
  1009. conclusion: resultConclusion.value,
  1010. remark: resultRemark.value,
  1011. results: resultParams.value
  1012. .filter((p) => hasResultValue(p.paramValue))
  1013. .map((p) => ({
  1014. ...p,
  1015. paramValue: p.paramValue === undefined || p.paramValue === null ? '' : String(p.paramValue)
  1016. }))
  1017. }
  1018. data.thirdPartyAttachments = financeAttachmentsToUrlString(resultAttachments.value)
  1019. if (resultTaskType.value !== 'dsfjc') {
  1020. data.materialUsages = validMaterialUsages.map((mu) => ({
  1021. materialId: mu.materialId,
  1022. qty: mu.qtyNum!,
  1023. remark: mu.remark?.trim() || undefined
  1024. }))
  1025. }
  1026. await fetchCompleteTaskWithResult(resultTaskId.value, data)
  1027. ElMessage.success(resultTaskStatus.value === 'ywc' ? '已保存' : '已完成')
  1028. resultVisible.value = false
  1029. loadData()
  1030. } finally {
  1031. resultLoading.value = false
  1032. }
  1033. }
  1034. // 输入检索条件时自动搜索(防抖 300ms)
  1035. watchDebounced(
  1036. searchForm,
  1037. () => {
  1038. pagination.page = 1
  1039. loadData()
  1040. },
  1041. { debounce: 300, deep: true }
  1042. )
  1043. onMounted(() => loadData())
  1044. </script>
  1045. <style scoped lang="scss">
  1046. .form-section {
  1047. background: #fafafa;
  1048. border-radius: 8px;
  1049. padding: 20px;
  1050. }
  1051. .form-section .section-title {
  1052. font-size: 15px;
  1053. font-weight: 500;
  1054. color: #303133;
  1055. margin-bottom: 16px;
  1056. padding-bottom: 12px;
  1057. padding-left: 10px;
  1058. border-bottom: 1px solid #ebeef5;
  1059. border-left: 3px solid #10b981;
  1060. }
  1061. .material-usage-section {
  1062. margin-top: 12px;
  1063. .material-usage-head {
  1064. display: flex;
  1065. align-items: center;
  1066. justify-content: space-between;
  1067. margin-bottom: 14px;
  1068. padding-bottom: 10px;
  1069. border-bottom: 1px solid #ebeef5;
  1070. border-left: none;
  1071. padding-left: 0;
  1072. }
  1073. .section-title-left {
  1074. display: flex;
  1075. align-items: center;
  1076. min-width: 0;
  1077. &::before {
  1078. content: '';
  1079. width: 3px;
  1080. height: 18px;
  1081. background: #10b981;
  1082. border-radius: 2px;
  1083. margin-right: 10px;
  1084. flex-shrink: 0;
  1085. }
  1086. h3 {
  1087. margin: 0;
  1088. font-size: 15px;
  1089. font-weight: 600;
  1090. color: #1e293b;
  1091. }
  1092. }
  1093. .material-usage-table {
  1094. width: 100%;
  1095. }
  1096. .inner-cell-input {
  1097. width: 100%;
  1098. }
  1099. :deep(.inner-cell-input.el-input-number) {
  1100. width: 100%;
  1101. }
  1102. }
  1103. .result-conclusion-section {
  1104. margin-top: 12px;
  1105. display: flex;
  1106. flex-direction: column;
  1107. gap: 16px;
  1108. .section-title {
  1109. margin-bottom: 0;
  1110. }
  1111. :deep(.el-form-item) {
  1112. margin-bottom: 0;
  1113. }
  1114. :deep(.el-form-item__label) {
  1115. display: inline-flex;
  1116. width: 100%;
  1117. justify-content: flex-start;
  1118. text-align: left;
  1119. padding-left: 0 !important;
  1120. }
  1121. .result-attachment-block {
  1122. margin-top: 0;
  1123. padding: 0;
  1124. background: transparent;
  1125. :deep(.protocol-finance-confirm-fields .el-form-item) {
  1126. margin-bottom: 0;
  1127. }
  1128. }
  1129. }
  1130. .biz-dialog-form {
  1131. :deep(.el-form-item) {
  1132. margin-bottom: 0;
  1133. }
  1134. }
  1135. .view-reception-attachments {
  1136. margin-top: 4px;
  1137. .view-kv {
  1138. display: grid;
  1139. grid-template-columns: 96px 1fr;
  1140. gap: 8px 12px;
  1141. font-size: 13px;
  1142. align-items: start;
  1143. dt {
  1144. margin: 0;
  1145. color: #64748b;
  1146. font-weight: 500;
  1147. }
  1148. dd {
  1149. margin: 0;
  1150. color: #1e293b;
  1151. }
  1152. }
  1153. }
  1154. </style>