| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551 |
- <!-- 采集协议单 — 只读查看(字段与新增/编辑弹窗一致) -->
- <template>
- <el-dialog
- :model-value="modelValue"
- :title="dialogTitle"
- width="1600px"
- class="dialog-form-layout protocol-view-dialog"
- align-center
- append-to-body
- modal-class="protocol-dialog-mask"
- destroy-on-close
- :close-on-click-modal="false"
- @update:model-value="emit('update:modelValue', $event)"
- @closed="handleClosed"
- >
- <div
- class="protocol-view-body"
- :class="{ 'is-detail-loading': detailLoading && !bundle }"
- v-loading="detailLoading && !bundle"
- element-loading-text="加载中…"
- >
- <div class="detail-tabs">
- <div
- v-for="tab in formTabs"
- :key="tab.key"
- class="detail-tab"
- :class="{ active: activeTab === tab.key, 'is-disabled': detailLoading && !bundle }"
- @click="scrollToSection(tab.key)"
- >
- {{ tab.label }}
- </div>
- </div>
- <template v-if="bundle">
- <div>
- <!-- 基本信息 -->
- <div id="view-section-basic" class="section-card section-card--white">
- <div class="section-title section-title--banner"><h3>基本信息</h3></div>
- <div class="data-grid data-grid--4">
- <div class="data-item"
- ><div class="data-label">协议状态</div
- ><div class="data-value"
- ><el-tag
- :type="protocolStatusTagType(String(protocol?.protocolStatus || ''))"
- size="small"
- >{{
- protocolStatusDisplayLabel(
- String(protocol?.protocolStatus || ''),
- String(protocol?.voidType || '')
- )
- }}</el-tag
- ></div
- ></div
- >
- <div class="data-item"
- ><div class="data-label">登记日期</div
- ><div class="data-value">{{ formatYmd(protocol?.registerDate) }}</div></div
- >
- <div class="data-item"
- ><div class="data-label">采集专员</div
- ><div class="data-value">{{ collectorDisplay }}</div></div
- >
- <div class="data-item"
- ><div class="data-label">创建时间</div
- ><div class="data-value">{{ dash(protocol?.createTime) }}</div></div
- >
- </div>
- <div v-if="protocol?.remark && String(protocol.remark).trim()" class="remark-row">
- <div class="data-item data-item--full">
- <div class="data-label">备注</div>
- <div class="data-value data-value--muted">{{ dash(protocol?.remark) }}</div>
- </div>
- </div>
- </div>
- <!-- 采集信息 -->
- <div id="view-section-collection" class="section-card section-card--white">
- <div class="section-title section-title--banner"><h3>采集信息</h3></div>
- <div class="data-grid data-grid--7 data-grid--collection-row">
- <div class="data-item"
- ><div class="data-label">采集地区</div
- ><div class="data-value">{{ regionDisplay }}</div></div
- >
- <div class="data-item"
- ><div class="data-label">采集医院</div
- ><div class="data-value">{{ hospitalDisplay }}</div></div
- >
- <div class="data-item"
- ><div class="data-label">采集医护</div
- ><div class="data-value">{{ dash(collectionInfo?.medicalStaff) }}</div></div
- >
- <div class="data-item"
- ><div class="data-label">市场专员</div
- ><div class="data-value">{{ salesDisplay }}</div></div
- >
- <div class="data-item"
- ><div class="data-label">地区经理</div
- ><div class="data-value">{{ regionManagerDisplay }}</div></div
- >
- <div class="data-item"
- ><div class="data-label">样本采集时间</div
- ><div class="data-value">{{ dash(collectionInfo?.sampleCollectTime) }}</div></div
- >
- <div class="data-item"
- ><div class="data-label">母血采集时间</div
- ><div class="data-value">{{
- dash(collectionInfo?.maternalBloodCollectTime)
- }}</div></div
- >
- </div>
- </div>
- <!-- 样本及运输(字典异步补齐,不阻塞整弹窗) -->
- <div
- id="view-section-sample"
- v-loading="lookupLoading"
- class="section-card section-card--white"
- >
- <div class="section-title section-title--banner"><h3>样本及运输信息</h3></div>
- <div v-if="samples.length" class="sample-cards">
- <div
- v-for="(row, sIdx) in samples"
- :key="row.id || sIdx"
- class="sample-card"
- :class="{
- 'is-highlight':
- !!highlightSampleCode && String(row.sampleCode || '') === highlightSampleCode
- }"
- :id="row.sampleCode ? `view-sample-${row.sampleCode}` : undefined"
- >
- <div class="sample-card__header">
- <div class="sample-card__title">
- <span class="sample-card__seq">{{ sIdx + 1 }}</span>
- {{ sampleTypeName(row.sampleTypeId) }}
- </div>
- <el-tag :type="sampleStatusTagType(String(row.sampleStatus || ''))" size="small">
- {{ sampleStatusLabel(String(row.sampleStatus || '')) }}
- </el-tag>
- </div>
- <div class="sample-card__body">
- <div class="sample-card__section">
- <div class="sample-card__section-label">样本信息</div>
- <div class="data-grid data-grid--4">
- <div class="data-item">
- <div class="data-label">样本编码</div>
- <div class="data-value data-value--code">
- <SampleCodeWithSpecialDisplay
- :sample-code="row.sampleCode"
- :is-special="!!row.isSpecial"
- :special-remark="row.specialRemark"
- monospace
- />
- </div>
- </div>
- <div class="data-item">
- <div class="data-label">原价格(元)</div>
- <div class="data-value data-value--price">{{
- row.originalPrice ? `¥${row.originalPrice}` : '—'
- }}</div>
- </div>
- <div class="data-item">
- <div class="data-label">优惠价格(元)</div>
- <div class="data-value data-value--price">{{
- row.price ? `¥${row.price}` : '—'
- }}</div>
- </div>
- <div class="data-item">
- <div class="data-label">存储年限(年)</div>
- <div class="data-value">{{ dash(row.storageYears) }}</div>
- </div>
- <div v-if="row.remark" class="data-item data-item--full">
- <div class="data-label">备注</div>
- <div class="data-value data-value--muted">{{ dash(row.remark) }}</div>
- </div>
- </div>
- </div>
- <div
- v-if="
- row.freight?.id ||
- row.freight?.transportMode ||
- row.transport?.etaTime
- "
- class="sample-card__section sample-card__section--transport"
- >
- <div class="sample-card__section-label">运输与运费</div>
- <div class="data-grid data-grid--4">
- <div class="data-item">
- <div class="data-label">运输方式</div>
- <div class="data-value">{{
- dash(row.freight?.transportModeLabel || row.freight?.transportMode)
- }}</div>
- </div>
- <div class="data-item">
- <div class="data-label">运费(元)</div>
- <div class="data-value"
- >{{
- row.freight?.freightAmount != null
- ? '¥' + formatMoneyDisplay(row.freight.freightAmount)
- : '—'
- }}{{
- Number(row.freight?.linkedSampleCount || 0) > 1 ? '(共用)' : ''
- }}</div
- >
- </div>
- <div class="data-item">
- <div class="data-label">预计到达</div>
- <div class="data-value">{{ dash(row.transport?.etaTime) }}</div>
- </div>
- </div>
- </div>
- <div class="sample-card__section sample-card__section--reception">
- <SampleReceptionDualReadonlyPanel
- :qc-reception="row.sampleReception"
- :production-reception="
- row.productionReception || row.sampleReception?.productionReception
- "
- source-type="cell_storage"
- :sample-type-name="row.sampleTypeName"
- />
- <div
- v-if="
- row.sampleReception &&
- sampleReceptionAttachmentFiles(row.sampleReception).length
- "
- class="attachment-row sample-card__attachments"
- >
- <div class="data-label">接收附件</div>
- <ProtocolAttachmentReadonly
- :files="sampleReceptionAttachmentFiles(row.sampleReception)"
- @preview="openPreview"
- @download="downloadFile"
- />
- </div>
- </div>
- <SampleQcInfoGroupedPanel
- :gate-qc-info="row.qcInfo"
- :process-qc-info-list="row.processQcInfoList"
- :sample-reception="row.sampleReception"
- :show-section-label="false"
- />
- <SampleProductionInfoCards :production-info="row.productionInfo" />
- <SampleStorageReleaseInfo :storage-release-info="row.storageReleaseInfo" />
- </div>
- </div>
- </div>
- <el-empty v-else description="暂无样本" :image-size="48" />
- </div>
- <div
- v-if="showHandlePanels"
- id="view-section-storage-cert-handle"
- class="section-card section-card--white"
- >
- <div class="section-title section-title--banner"><h3>储存证办理</h3></div>
- <ProtocolHandlePanel
- v-if="protocolId"
- mode="storage-cert"
- :protocol-id="protocolId"
- :sample-code="highlightSampleCode || undefined"
- :initial="storageCertInfo"
- @saved="loadDetail"
- />
- </div>
- <div
- v-if="showHandlePanels"
- id="view-section-insurance-handle"
- class="section-card section-card--white"
- >
- <div class="section-title section-title--banner"><h3>保险办理</h3></div>
- <ProtocolHandlePanel
- v-if="protocolId"
- mode="insurance"
- :protocol-id="protocolId"
- :sample-code="highlightSampleCode || undefined"
- :initial="fullmoonInsuranceInfo"
- @saved="loadDetail"
- />
- </div>
- <!-- 客户信息 -->
- <div id="view-section-party" class="section-card section-card--green">
- <div class="section-title section-title--banner"><h3>客户信息</h3></div>
- <div class="client-sub-cards">
- <!-- 母亲信息 -->
- <div class="client-sub-card">
- <div class="sub-card-header"
- ><span class="sub-card-icon">🤱</span><h4>母亲信息</h4></div
- >
- <div class="data-grid data-grid--4">
- <div class="data-item"
- ><div class="data-label">关联客户</div
- ><div class="data-value">{{
- partyCustomer?.customerId ? dash(partyCustomer?.motherName) : '—'
- }}</div></div
- >
- <div class="data-item"
- ><div class="data-label">母亲姓名</div
- ><div class="data-value">{{ dash(partyCustomer?.motherName) }}</div></div
- >
- <div class="data-item"
- ><div class="data-label">母亲联系电话</div
- ><div class="data-value">{{ dash(partyCustomer?.motherPhone) }}</div></div
- >
- <div class="data-item"
- ><div class="data-label">母亲身份证号</div
- ><div class="data-value">{{ dash(partyCustomer?.motherIdCard) }}</div></div
- >
- <div class="data-item"
- ><div class="data-label">母亲出生年月</div
- ><div class="data-value">{{ formatYm(partyCustomer?.motherBirthYm) }}</div></div
- >
- <div class="data-item"
- ><div class="data-label">母亲民族</div
- ><div class="data-value">{{ dash(partyCustomer?.motherEthnic) }}</div></div
- >
- <div class="data-item"
- ><div class="data-label">母亲邮箱</div
- ><div class="data-value">{{ dash(partyCustomer?.motherEmail) }}</div></div
- >
- </div>
- <div class="sub-card-full-row"
- ><div class="data-label">母亲家庭住址</div
- ><div class="data-value data-value--muted">{{
- dash(partyCustomer?.motherAddress)
- }}</div></div
- >
- </div>
- <!-- 父亲信息 -->
- <div class="client-sub-card">
- <div class="sub-card-header"
- ><span class="sub-card-icon">👨</span><h4>父亲信息</h4></div
- >
- <div class="data-grid data-grid--4">
- <div class="data-item"
- ><div class="data-label">父亲姓名</div
- ><div class="data-value">{{ dash(partyCustomer?.fatherName) }}</div></div
- >
- <div class="data-item"
- ><div class="data-label">父亲电话</div
- ><div class="data-value">{{ dash(partyCustomer?.fatherPhone) }}</div></div
- >
- <div class="data-item data-item--full"
- ><div class="data-label">父亲身份证号</div
- ><div class="data-value">{{ dash(partyCustomer?.fatherIdCard) }}</div></div
- >
- </div>
- </div>
- <!-- 新生儿信息 -->
- <div class="client-sub-card">
- <div class="sub-card-header"
- ><span class="sub-card-icon">👶</span><h4>新生儿信息</h4></div
- >
- <div class="data-grid data-grid--4">
- <div class="data-item"
- ><div class="data-label">新生儿姓名</div
- ><div class="data-value">{{ dash(partyCustomer?.newbornName) }}</div></div
- >
- <div class="data-item"
- ><div class="data-label">新生儿身份证</div
- ><div class="data-value">{{ dash(partyCustomer?.newbornIdCard) }}</div></div
- >
- <div class="data-item"
- ><div class="data-label">预产期</div
- ><div class="data-value">{{
- dash(partyCustomer?.expectedDeliveryDate)
- }}</div></div
- >
- <div class="data-item"
- ><div class="data-label">新生儿分娩时间</div
- ><div class="data-value">{{
- dash(partyCustomer?.newbornDeliveryTime)
- }}</div></div
- >
- <div class="data-item"
- ><div class="data-label">新生儿性别</div
- ><div class="data-value">{{
- newbornSexLabel(String(partyCustomer?.newbornSex || ''))
- }}</div></div
- >
- <div class="data-item"
- ><div class="data-label">分娩方式</div
- ><div class="data-value">{{
- deliveryModeLabel(String(partyCustomer?.deliveryMode || ''))
- }}</div></div
- >
- <div class="data-item"
- ><div class="data-label">胎次</div
- ><div class="data-value">{{ dash(partyCustomer?.birthOrder) }}</div></div
- >
- <div class="data-item"
- ><div class="data-label">新生儿体重(克)</div
- ><div class="data-value">{{ dash(partyCustomer?.newbornWeightGram) }}</div></div
- >
- <div class="data-item"
- ><div class="data-label">新生儿评分</div
- ><div class="data-value">{{ dash(partyCustomer?.apgar1min) }}</div></div
- >
- </div>
- </div>
- </div>
- </div>
- <!-- 储存证与新生儿保险(只读,字段对齐样本信息管理录入项) -->
- <div
- v-if="showHandleSummary"
- id="view-section-storage-insurance"
- class="section-card section-card--white"
- >
- <div class="section-title section-title--banner"><h3>储存证与新生儿保险</h3></div>
- <div class="handle-summary-cards">
- <div v-if="showStorageCert" class="handle-summary-card">
- <div class="sub-card-header"><h4>储存证</h4></div>
- <div class="data-grid data-grid--2">
- <div class="data-item">
- <div class="data-label">办理时间</div>
- <div class="data-value">{{ dash(storageCertInfo?.handleTime) }}</div>
- </div>
- <div class="data-item">
- <div class="data-label">办理人员</div>
- <div class="data-value">{{ dash(storageCertInfo?.handlerUserName) }}</div>
- </div>
- </div>
- <div class="attachment-row">
- <div class="data-label">储存证附件</div>
- <ProtocolAttachmentReadonly
- :files="storageCertAttachments"
- @preview="openPreview"
- @download="downloadFile"
- />
- </div>
- <div class="remark-row">
- <div class="data-item data-item--full">
- <div class="data-label">备注</div>
- <div class="data-value data-value--muted">{{
- dash(storageCertInfo?.remark)
- }}</div>
- </div>
- </div>
- </div>
- <div v-if="showFullmoonInsurance" class="handle-summary-card">
- <div class="sub-card-header"><h4>新生儿保险</h4></div>
- <div class="data-grid data-grid--2">
- <div class="data-item">
- <div class="data-label">办理时间</div>
- <div class="data-value">{{ dash(fullmoonInsuranceInfo?.handleTime) }}</div>
- </div>
- <div class="data-item">
- <div class="data-label">办理人员</div>
- <div class="data-value">{{ dash(fullmoonInsuranceInfo?.handlerUserName) }}</div>
- </div>
- </div>
- <div class="attachment-row">
- <div class="data-label">保险附件</div>
- <ProtocolAttachmentReadonly
- :files="fullmoonInsuranceAttachments"
- @preview="openPreview"
- @download="downloadFile"
- />
- </div>
- <div class="remark-row">
- <div class="data-item data-item--full">
- <div class="data-label">备注</div>
- <div class="data-value data-value--muted">{{
- dash(fullmoonInsuranceInfo?.handleRemark)
- }}</div>
- </div>
- </div>
- </div>
- </div>
- </div>
- <!-- 缴费信息 -->
- <div id="view-section-payment" class="section-card section-card--white">
- <div class="section-title section-title--banner"><h3>缴费信息</h3></div>
- <!-- v2: 按样本缴费只读(对齐新增/编辑弹窗) -->
- <template v-if="paymentModelVersion >= 2">
- <el-alert
- v-if="presignDepositHint"
- type="info"
- :closable="false"
- show-icon
- style="margin-bottom: 12px"
- :title="presignDepositHintText"
- />
- <div
- v-if="depositTransfers.length"
- class="sample-pay-readonly__batch-title"
- style="margin-bottom: 8px"
- >
- 意向金转账(关联预签约订单)
- </div>
- <el-table
- v-if="depositTransfers.length"
- :data="depositTransfers"
- class="list-table inner-table"
- stripe
- style="margin-bottom: 16px"
- >
- <el-table-column type="index" label="#" min-width="50" align="center" />
- <el-table-column label="转账方式" min-width="120" align="center">
- <template #default="{ row }">
- {{ transferTypeLabel(String(row.transferType || '')) || '—' }}
- </template>
- </el-table-column>
- <el-table-column label="转账金额(元)" min-width="130" align="center">
- <template #default="{ row }">{{ fmtMoney(row.transferAmount) }}</template>
- </el-table-column>
- <el-table-column label="转账时间" min-width="170" align="center">
- <template #default="{ row }">
- {{ formatDisplayOrDash(row.transferTime) }}
- </template>
- </el-table-column>
- <el-table-column label="转账凭证" min-width="220" align="left">
- <template #default="{ row }">
- <ProtocolAttachmentReadonly
- v-if="(row.attachments || []).length"
- :files="row.attachments || []"
- size="compact"
- @preview="openPreview"
- @download="downloadFile"
- />
- <span v-else class="muted-dash">—</span>
- </template>
- </el-table-column>
- <el-table-column
- label="备注"
- min-width="140"
- align="center"
- show-overflow-tooltip
- >
- <template #default="{ row }">{{ row.remark || '—' }}</template>
- </el-table-column>
- <el-table-column
- label="财务备注"
- min-width="140"
- align="center"
- show-overflow-tooltip
- >
- <template #default="{ row }">{{ row.financeRemark || '—' }}</template>
- </el-table-column>
- <el-table-column label="状态" min-width="110" align="center">
- <template #default="{ row }">
- <el-tag :type="payStatusTagType(String(row.payStatus || ''))" size="small">
- {{ payStatusLabel(String(row.payStatus || '')) }}
- </el-tag>
- </template>
- </el-table-column>
- </el-table>
- <div class="payment-summary payment-summary--3">
- <div class="payment-metric">
- <div class="pm-label">协议总金额(元)</div>
- <div class="pm-value pm-value--amount">{{
- fmtMoney(protocol?.protocolTotalAmount)
- }}</div>
- </div>
- <div class="payment-metric">
- <div class="pm-label">样本合同合计(元)</div>
- <div class="pm-value pm-value--amount">{{ fmtMoney(sampleContractSum) }}</div>
- </div>
- <div class="payment-metric">
- <div class="pm-label">已确认收款(元)</div>
- <div class="pm-value pm-value--amount pm-value--received">{{
- fmtMoney(v2ReceivedAmount)
- }}</div>
- </div>
- </div>
- <div v-if="samplePayments.length" class="sample-pay-readonly">
- <div
- v-for="(sp, sIdx) in samplePayments"
- :key="sp.id || sIdx"
- class="sample-pay-readonly__card"
- >
- <div class="sample-pay-readonly__head">
- <div class="sample-pay-readonly__title">
- <span>{{ samplePayTitle(sp) }}</span>
- <el-tag v-if="sp.sampleCode" size="small" type="info">{{
- sp.sampleCode
- }}</el-tag>
- <el-tag
- :type="String(sp.payMode) === 'fq' ? 'warning' : 'success'"
- size="small"
- >
- {{ payModeLabel(String(sp.payMode || '')) }}
- </el-tag>
- </div>
- <span class="sample-pay-readonly__amt">{{ fmtMoney(sp.contractAmount) }}</span>
- </div>
- <div
- class="data-grid"
- :class="
- String(sp.payMode) === 'fq' ? 'data-grid--6' : 'data-grid--3'
- "
- >
- <div class="data-item">
- <div class="data-label">缴费模式</div>
- <div class="data-value">{{ payModeLabel(String(sp.payMode || '')) }}</div>
- </div>
- <div class="data-item">
- <div class="data-label">合同金额(元)</div>
- <div class="data-value data-value--price">{{
- fmtMoney(sp.contractAmount)
- }}</div>
- </div>
- <template v-if="String(sp.payMode) === 'dj'">
- <div class="data-item">
- <div class="data-label">趸交截止时间</div>
- <div class="data-value">{{ formatYmd(sp.lumpSumDeadline) }}</div>
- </div>
- </template>
- <template v-else>
- <div class="data-item">
- <div class="data-label">分期期数</div>
- <div class="data-value">{{ dash(sp.installmentCount) }}</div>
- </div>
- <div class="data-item">
- <div class="data-label">每期间隔(月)</div>
- <div class="data-value">{{ dash(sp.installmentIntervalMonths) }}</div>
- </div>
- <div class="data-item">
- <div class="data-label">分期开始时间</div>
- <div class="data-value">{{ formatYmd(sp.installmentStartDate) }}</div>
- </div>
- <div class="data-item">
- <div class="data-label">提前催缴预警(天)</div>
- <div class="data-value">{{ dash(sp.installmentAdvanceDays ?? 30) }}</div>
- </div>
- </template>
- </div>
- <div class="sample-pay-readonly__batch-title">缴费明细</div>
- <div v-if="!(sp.schedules || []).length" class="sample-pay-readonly__empty">
- 暂无缴费明细
- </div>
- <div
- v-for="(sch, schIdx) in sp.schedules || []"
- :key="sch.id || schIdx"
- class="sample-pay-readonly__sch"
- >
- <div class="sample-pay-readonly__sch-title">
- 第 {{ sch.scheduleNo || schIdx + 1 }} 期
- </div>
- <div class="data-grid data-grid--3">
- <div class="data-item">
- <div class="data-label">应缴费金额(元)</div>
- <div class="data-value data-value--price">{{
- fmtMoney(sch.dueAmount)
- }}</div>
- </div>
- <div class="data-item">
- <div class="data-label">应缴费时间</div>
- <div class="data-value">{{ formatYmd(sch.dueDate) }}</div>
- </div>
- <div class="data-item">
- <div class="data-label">已缴费金额(元)</div>
- <div class="data-value data-value--price">{{
- fmtMoney(sch.paidAmount)
- }}</div>
- </div>
- </div>
- <div class="sample-pay-readonly__tr-title">
- 转账记录
- <span v-if="(sch.transfers || []).length" class="sample-pay-readonly__tr-count"
- >({{ (sch.transfers || []).length }} 笔)</span
- >
- </div>
- <div class="list-body list-wrap view-inner-table-wrap">
- <el-table
- :data="sch.transfers || []"
- class="list-table inner-table"
- stripe
- empty-text="暂无转账记录"
- >
- <el-table-column
- type="index"
- label="序号"
- min-width="68"
- align="center"
- header-align="center"
- />
- <el-table-column
- label="转账方式"
- min-width="140"
- align="center"
- header-align="center"
- >
- <template #default="{ row }">
- {{ transferTypeLabel(String(row.transferType || '')) || '—' }}
- </template>
- </el-table-column>
- <el-table-column
- label="转账金额(元)"
- min-width="140"
- align="center"
- header-align="center"
- >
- <template #default="{ row }">{{ fmtMoney(row.transferAmount) }}</template>
- </el-table-column>
- <el-table-column
- label="转账时间"
- min-width="190"
- align="center"
- header-align="center"
- >
- <template #default="{ row }">
- {{ formatDisplayOrDash(row.transferTime) }}
- </template>
- </el-table-column>
- <el-table-column
- label="转账凭证"
- min-width="280"
- align="left"
- header-align="center"
- >
- <template #default="{ row }">
- <ProtocolAttachmentReadonly
- v-if="transferAttachments(row).length"
- :files="transferAttachments(row)"
- size="compact"
- @preview="openPreview"
- @download="downloadFile"
- />
- <span v-else class="muted-dash">—</span>
- </template>
- </el-table-column>
- <el-table-column
- label="备注"
- min-width="140"
- align="center"
- header-align="center"
- show-overflow-tooltip
- >
- <template #default="{ row }">{{ row.remark || '—' }}</template>
- </el-table-column>
- <el-table-column
- label="财务备注"
- min-width="140"
- align="center"
- header-align="center"
- show-overflow-tooltip
- >
- <template #default="{ row }">{{ row.financeRemark || '—' }}</template>
- </el-table-column>
- <el-table-column
- label="状态"
- min-width="110"
- align="center"
- header-align="center"
- >
- <template #default="{ row }">
- <el-tag
- :type="payStatusTagType(String(row.payStatus || ''))"
- size="small"
- >
- {{ payStatusLabel(String(row.payStatus || '')) }}
- </el-tag>
- </template>
- </el-table-column>
- </el-table>
- </div>
- </div>
- </div>
- </div>
- <el-empty v-else description="暂无按样本缴费方案" :image-size="40" />
- <div class="sample-pay-readonly__batch-title sample-pay-readonly__batch-title--mt"
- >退费登记</div
- >
- <el-table
- v-if="refundRecords.length"
- :data="refundRecords"
- class="list-table inner-table"
- stripe
- >
- <el-table-column
- prop="refundAmount"
- label="退费金额"
- min-width="120"
- align="center"
- >
- <template #default="{ row }">{{ fmtMoney(row.refundAmount) }}</template>
- </el-table-column>
- <el-table-column prop="refundTime" label="退费时间" min-width="160" align="center">
- <template #default="{ row }">{{ formatDisplayOrDash(row.refundTime) }}</template>
- </el-table-column>
- <el-table-column
- prop="refundRemark"
- label="备注"
- min-width="180"
- align="center"
- show-overflow-tooltip
- />
- </el-table>
- <el-empty v-else description="暂无退费登记" :image-size="40" />
- </template>
- <!-- v1: 旧整单缴费只读(样式对齐 v2:期汇总 + 转账记录) -->
- <template v-else>
- <div class="payment-summary payment-summary--3">
- <div class="payment-metric">
- <div class="pm-label">合同金额(元)</div>
- <div class="pm-value pm-value--amount">{{
- fmtMoney(payment?.contractAmount)
- }}</div>
- </div>
- <div class="payment-metric">
- <div class="pm-label">已确认收款(元)</div>
- <div class="pm-value pm-value--amount pm-value--received">{{
- fmtMoney(receivedAmount)
- }}</div>
- </div>
- <div class="payment-metric">
- <div class="pm-label">缴费模式</div>
- <div class="pm-value">{{ payModeLabel(String(payment?.payMode || '')) }}</div>
- </div>
- </div>
- <div class="sample-pay-readonly">
- <div class="sample-pay-readonly__card">
- <div class="sample-pay-readonly__head">
- <div class="sample-pay-readonly__title">
- <span>协议缴费</span>
- <el-tag
- :type="
- isInstallmentPayMode(String(payment?.payMode || ''))
- ? 'warning'
- : 'success'
- "
- size="small"
- >
- {{ payModeLabel(String(payment?.payMode || '')) }}
- </el-tag>
- </div>
- <span class="sample-pay-readonly__amt">{{
- fmtMoney(payment?.contractAmount)
- }}</span>
- </div>
- <div
- class="data-grid"
- :class="
- isInstallmentPayMode(String(payment?.payMode || ''))
- ? 'data-grid--6'
- : 'data-grid--3'
- "
- >
- <div class="data-item">
- <div class="data-label">缴费模式</div>
- <div class="data-value">{{
- payModeLabel(String(payment?.payMode || ''))
- }}</div>
- </div>
- <div class="data-item">
- <div class="data-label">合同金额(元)</div>
- <div class="data-value data-value--price">{{
- fmtMoney(payment?.contractAmount)
- }}</div>
- </div>
- <template v-if="String(payment?.payMode) === 'dj'">
- <div class="data-item">
- <div class="data-label">趸交截止时间</div>
- <div class="data-value">{{ formatYmd(payment?.lumpSumDeadline) }}</div>
- </div>
- </template>
- <template v-else>
- <div class="data-item">
- <div class="data-label">分期期数</div>
- <div class="data-value">{{ dash(payment?.installmentCount) }}</div>
- </div>
- <div class="data-item">
- <div class="data-label">每期间隔(月)</div>
- <div class="data-value">{{
- dash(payment?.installmentIntervalMonths)
- }}</div>
- </div>
- <div class="data-item">
- <div class="data-label">分期开始时间</div>
- <div class="data-value">{{ formatYmd(payment?.installmentStartDate) }}</div>
- </div>
- <div class="data-item">
- <div class="data-label">提前催缴预警(天)</div>
- <div class="data-value">{{
- dash(payment?.installmentAdvanceDays ?? 30)
- }}</div>
- </div>
- </template>
- </div>
- <div class="sample-pay-readonly__batch-title">缴费明细</div>
- <div v-if="!v1PayPeriods.length" class="sample-pay-readonly__empty">
- 暂无缴费明细
- </div>
- <div
- v-for="(sch, schIdx) in v1PayPeriods"
- :key="sch.scheduleNo || schIdx"
- class="sample-pay-readonly__sch"
- >
- <div class="sample-pay-readonly__sch-title">
- 第 {{ sch.scheduleNo || schIdx + 1 }} 期
- </div>
- <div class="data-grid data-grid--3">
- <div class="data-item">
- <div class="data-label">应缴费金额(元)</div>
- <div class="data-value data-value--price">{{
- fmtMoney(sch.dueAmount)
- }}</div>
- </div>
- <div class="data-item">
- <div class="data-label">应缴费时间</div>
- <div class="data-value">{{ formatYmd(sch.dueDate) }}</div>
- </div>
- <div class="data-item">
- <div class="data-label">已缴费金额(元)</div>
- <div class="data-value data-value--price">{{
- fmtMoney(sch.paidAmount)
- }}</div>
- </div>
- </div>
- <div class="sample-pay-readonly__tr-title">
- 转账记录
- <span v-if="sch.transfers.length" class="sample-pay-readonly__tr-count"
- >({{ sch.transfers.length }} 笔)</span
- >
- </div>
- <div class="list-body list-wrap view-inner-table-wrap">
- <el-table
- :data="sch.transfers"
- class="list-table inner-table"
- stripe
- empty-text="暂无转账记录"
- >
- <el-table-column
- type="index"
- label="序号"
- min-width="68"
- align="center"
- header-align="center"
- />
- <el-table-column
- label="转账方式"
- min-width="140"
- align="center"
- header-align="center"
- >
- <template #default="{ row }">
- {{ transferTypeLabel(String(row.transferType || '')) || '—' }}
- </template>
- </el-table-column>
- <el-table-column
- label="转账金额(元)"
- min-width="140"
- align="center"
- header-align="center"
- >
- <template #default="{ row }">{{ fmtMoney(row.transferAmount) }}</template>
- </el-table-column>
- <el-table-column
- label="转账时间"
- min-width="190"
- align="center"
- header-align="center"
- >
- <template #default="{ row }">
- {{ formatDisplayOrDash(row.transferTime) }}
- </template>
- </el-table-column>
- <el-table-column
- label="转账凭证"
- min-width="280"
- align="left"
- header-align="center"
- >
- <template #default="{ row }">
- <ProtocolAttachmentReadonly
- v-if="detailAttachments(row).length"
- :files="detailAttachments(row)"
- size="compact"
- @preview="openPreview"
- @download="downloadFile"
- />
- <span v-else class="muted-dash">—</span>
- </template>
- </el-table-column>
- <el-table-column
- label="状态"
- min-width="110"
- align="center"
- header-align="center"
- >
- <template #default="{ row }">
- <el-tag
- :type="payStatusTagType(String(row.payStatus || ''))"
- size="small"
- >
- {{ payStatusLabel(String(row.payStatus || '')) }}
- </el-tag>
- </template>
- </el-table-column>
- </el-table>
- </div>
- </div>
- </div>
- </div>
- </template>
- </div>
- <!-- 审核记录 -->
- <div
- v-if="showAuditHistory"
- id="view-section-audit"
- class="section-card section-card--white"
- >
- <div class="section-title section-title--banner"><h3>审核记录</h3></div>
- <div class="list-body list-wrap view-inner-table-wrap">
- <el-table
- v-loading="auditLoading"
- class="list-table inner-table"
- :data="auditRecords"
- stripe
- empty-text="暂无审核记录"
- >
- <el-table-column
- type="index"
- label="序号"
- min-width="68"
- align="center"
- header-align="center"
- />
- <el-table-column
- prop="createTime"
- label="审核时间"
- min-width="170"
- align="center"
- header-align="center"
- >
- <template #default="{ row }">{{ formatDisplayOrDash(row.createTime) }}</template>
- </el-table-column>
- <el-table-column
- prop="auditStatus"
- label="审核状态"
- min-width="120"
- align="center"
- header-align="center"
- >
- <template #default="{ row }">
- <el-tag
- :type="String(row.auditStatus) === 'shtg' ? 'success' : 'warning'"
- size="small"
- >
- {{ auditStatusLabel(String(row.auditStatus || '')) }}
- </el-tag>
- </template>
- </el-table-column>
- <el-table-column
- prop="auditRemark"
- label="审核备注"
- min-width="200"
- align="center"
- header-align="center"
- show-overflow-tooltip
- />
- <el-table-column
- prop="auditorUserName"
- label="审核人"
- min-width="120"
- align="center"
- header-align="center"
- >
- <template #default="{ row }">{{ auditorDisplay(row) }}</template>
- </el-table-column>
- </el-table>
- </div>
- </div>
- <!-- 附件信息 -->
- <div id="view-section-attachment" class="section-card section-card--white">
- <div class="section-title section-title--banner"><h3>附件信息</h3></div>
- <div class="protocol-basic-attachments-block protocol-basic-attachments-block--flush">
- <div class="data-item protocol-attachments-item">
- <div class="data-label">协议附件</div>
- <div class="data-value protocol-attachments-value">
- <ProtocolAttachmentReadonly
- :files="protocolAttachments"
- @preview="openPreview"
- @download="downloadFile"
- />
- </div>
- </div>
- <div class="data-item protocol-attachments-item">
- <div class="data-label">知情同意书</div>
- <div class="data-value protocol-attachments-value">
- <ProtocolAttachmentReadonly
- :files="consentAttachments"
- @preview="openPreview"
- @download="downloadFile"
- />
- </div>
- </div>
- <div class="data-item protocol-attachments-item">
- <div class="data-label">孕妇健康调查表</div>
- <div class="data-value protocol-attachments-value">
- <ProtocolAttachmentReadonly
- :files="healthSurveyAttachments"
- @preview="openPreview"
- @download="downloadFile"
- />
- </div>
- </div>
- </div>
- </div>
- <!-- 状态变更记录(置底) -->
- <div id="view-section-status-change" class="section-card section-card--white">
- <div class="section-title section-title--banner"><h3>状态变更记录</h3></div>
- <el-empty v-if="!hasAnyStatusChange" description="暂无状态变更记录" :image-size="64" />
- <div v-else class="data-grid data-grid--4">
- <template v-if="protocol?.effectiveTime">
- <div class="data-item"
- ><div class="data-label">协议生效时间</div
- ><div class="data-value">{{ dash(protocol.effectiveTime) }}</div></div
- >
- <div class="data-item"
- ><div class="data-label">协议生效描述</div
- ><div class="data-value data-value--muted">{{
- dash(protocol.effectiveDesc)
- }}</div></div
- >
- </template>
- <template v-if="protocol?.expireTime">
- <div class="data-item"
- ><div class="data-label">协议过期时间</div
- ><div class="data-value">{{ dash(protocol.expireTime) }}</div></div
- >
- <div class="data-item"
- ><div class="data-label">协议过期描述</div
- ><div class="data-value data-value--muted">{{
- dash(protocol.expireDesc)
- }}</div></div
- >
- </template>
- <template v-if="protocol?.voidTime">
- <div class="data-item"
- ><div class="data-label">作废类型</div
- ><div class="data-value">{{
- voidTypeLabel(String(protocol?.voidType || '')) || '—'
- }}</div></div
- >
- <div class="data-item"
- ><div class="data-label">协议作废时间</div
- ><div class="data-value">{{ dash(protocol.voidTime) }}</div></div
- >
- <div class="data-item"
- ><div class="data-label">协议作废描述</div
- ><div class="data-value data-value--muted">{{
- dash(protocol.voidDesc)
- }}</div></div
- >
- </template>
- <template v-if="protocol?.toResearchTime">
- <div class="data-item"
- ><div class="data-label">协议转科研时间</div
- ><div class="data-value">{{ dash(protocol.toResearchTime) }}</div></div
- >
- <div class="data-item"
- ><div class="data-label">协议转科研描述</div
- ><div class="data-value data-value--muted">{{
- dash(protocol.toResearchDesc)
- }}</div></div
- >
- </template>
- </div>
- </div>
- </div>
- </template>
- </div>
- <template #footer>
- <el-button class="btn-ghost" @click="emit('update:modelValue', false)">
- <el-icon><Close /></el-icon>
- 关闭
- </el-button>
- </template>
- </el-dialog>
- <el-dialog
- v-model="previewVisible"
- title="文件预览"
- width="800px"
- align-center
- append-to-body
- destroy-on-close
- >
- <div v-if="previewType === 'image'" class="preview-container">
- <img
- :src="previewUrl"
- alt="预览"
- style="max-width: 100%; max-height: 70vh; display: block; margin: 0 auto"
- />
- </div>
- <div v-else class="preview-container">
- <video
- :src="previewUrl"
- controls
- style="max-width: 100%; max-height: 70vh; display: block; margin: 0 auto"
- />
- </div>
- </el-dialog>
- </template>
- <script setup lang="ts">
- import { formatDisplayOrDash } from '@/utils/datetime-display'
- import { computed, nextTick, ref, watch } from 'vue'
- import { Close } from '@element-plus/icons-vue'
- import ProtocolAttachmentReadonly from './ProtocolAttachmentReadonly.vue'
- import SampleQcInfoGroupedPanel from '@/components/business/sample-qc-info-grouped-panel/index.vue'
- import SampleReceptionDualReadonlyPanel from '@/components/business/sample-reception-dual-readonly-panel/index.vue'
- import SampleCodeWithSpecialDisplay from '@/components/business/sample-code-with-special-display/index.vue'
- import SampleProductionInfoCards from '@/components/business/sample-production-info-cards/index.vue'
- import SampleStorageReleaseInfo from '@/components/business/sample-storage-release-info/index.vue'
- import ProtocolHandlePanel from '@/components/business/protocol-handle-panel/index.vue'
- import {
- fetchCollectionProtocolAuditList,
- fetchCollectionProtocolDetail,
- type CollectionProtocolRow
- } from '@/api/collection-protocol'
- import {
- fetchCellSampleTypeList,
- fetchItemUnitList,
- fetchExpressCompanyList,
- fetchSiteList,
- fetchCollectionRegionList,
- fetchHospitalList,
- fetchUserList
- } from '@/api/admin'
- import { downloadAttachmentFile } from '@/utils/download-attachment'
- import { formatMoneyDisplay } from '@/utils/money-format'
- import {
- protocolStatusDisplayLabel,
- protocolStatusTagType,
- auditStatusLabel,
- payModeLabel,
- newbornSexLabel,
- deliveryModeLabel,
- sampleStatusLabel,
- sampleStatusTagType,
- transportTypeLabel,
- payStatusLabel,
- payStatusTagType
- } from '@/utils/status-tag-registry'
- import { loadTransferTypeDict, transferTypeLabel } from '@/utils/protocol-payment-labels'
- import { loadVoidTypeDict, voidTypeLabel } from '@/utils/void-type-dict'
- import { formatProtocolDocSubject } from '@/utils/protocol-doc-label'
- import { isInstallmentPayMode } from '@/utils/protocol-payment-presets'
- import { sumSampleContractAmounts } from '@/utils/protocol-sample-payment'
- const props = withDefaults(
- defineProps<{
- modelValue: boolean
- protocolId: string | null
- listRow?: CollectionProtocolRow | null
- /** 展示审核记录 Tab(采集协议单审核页) */
- showAuditHistory?: boolean
- /** 只读摘要:展示储存证办理字段 */
- showStorageCert?: boolean
- /** 只读摘要:与储存证并排展示新生儿保险办理字段 */
- showFullmoonInsurance?: boolean
- /** 样本信息管理:储存证/保险轻量办理(可编辑,与只读摘要互斥) */
- showHandlePanels?: boolean
- highlightSampleCode?: string
- }>(),
- {
- showAuditHistory: false,
- showStorageCert: false,
- showFullmoonInsurance: false,
- showHandlePanels: false,
- highlightSampleCode: ''
- }
- )
- const emit = defineEmits<{
- (e: 'update:modelValue', val: boolean): void
- }>()
- const baseFormTabs = [
- { key: 'basic', label: '基本信息' },
- { key: 'collection', label: '采集信息' },
- { key: 'sample', label: '样本及运输信息' },
- { key: 'party', label: '客户信息' },
- { key: 'payment', label: '缴费信息' }
- ] as const
- /** 协议审核等页:只读展示储存证/保险(与样本信息录入字段对齐);办理编辑用 showHandlePanels */
- const showHandleSummary = computed(
- () => !props.showHandlePanels && (props.showStorageCert || props.showFullmoonInsurance)
- )
- const formTabs = computed(() => {
- const tabs: Array<{ key: string; label: string }> = [...baseFormTabs]
- if (props.showHandlePanels) {
- const sampleIdx = tabs.findIndex((t) => t.key === 'sample')
- tabs.splice(sampleIdx + 1, 0, { key: 'storage-cert-handle', label: '储存证办理' })
- tabs.splice(sampleIdx + 2, 0, { key: 'insurance-handle', label: '保险办理' })
- }
- if (showHandleSummary.value) {
- const partyIdx = tabs.findIndex((t) => t.key === 'party')
- tabs.splice(partyIdx + 1, 0, { key: 'storage-insurance', label: '储存证与保险' })
- }
- if (props.showAuditHistory) {
- tabs.push({ key: 'audit', label: '审核记录' })
- }
- tabs.push({ key: 'attachment', label: '附件信息' })
- tabs.push({ key: 'status-change', label: '状态变更记录' })
- return tabs
- })
- const detailLoading = ref(false)
- const lookupLoading = ref(false)
- const auditLoading = ref(false)
- const bundle = ref<Record<string, any> | null>(null)
- const auditRecords = ref<Array<Record<string, unknown>>>([])
- const activeTab = ref('basic')
- const sampleTypeOptions = ref<Array<{ id: string; typeName: string }>>([])
- const unitOptions = ref<Array<{ id: string; unitName: string }>>([])
- const siteOptions = ref<Array<{ id: string; siteName: string }>>([])
- const expressCompanyOptions = ref<Array<{ id: string; expressName: string }>>([])
- const regionOptions = ref<Array<{ id: string; regionName: string }>>([])
- const hospitalOptions = ref<Array<{ id: string; hospitalName: string }>>([])
- const collectorUserOptions = ref<Array<{ id: string; nickName: string }>>([])
- const salesUserOptions = ref<Array<{ id: string; nickName: string }>>([])
- const minioBaseUrl = (import.meta.env.VITE_MINIO_BASE_URL || '').replace(/\/$/, '')
- const protocol = computed(() => bundle.value?.protocol as Record<string, any> | undefined)
- const partyCustomer = computed(() => bundle.value?.partyCustomer as Record<string, any> | null)
- const collectionInfo = computed(() => bundle.value?.collectionInfo as Record<string, any> | null)
- const payment = computed(() => bundle.value?.payment as Record<string, any> | null)
- const paymentModelVersion = computed(() =>
- Number((bundle.value?.protocol as any)?.paymentModelVersion || 1)
- )
- const samplePayments = computed(
- () => (bundle.value?.samplePayments as Array<Record<string, any>>) || []
- )
- const refundRecords = computed(
- () => (bundle.value?.refundRecords as Array<Record<string, any>>) || []
- )
- const depositTransfers = computed(
- () => (bundle.value?.depositTransfers as Array<Record<string, any>>) || []
- )
- const presignDepositHint = computed(
- () => (bundle.value?.presignDepositHint as Record<string, any>) || null
- )
- const sampleContractSum = computed(() => sumSampleContractAmounts(samplePayments.value))
- const presignDepositHintText = computed(() => {
- const h = presignDepositHint.value
- if (!h) return ''
- const amt = Number(h.confirmedDepositAmount || 0)
- const cnt = Number(h.confirmedTransferCount || 0)
- return `来源预签约已确认意向金合计 ¥${formatMoneyDisplay(amt)}(${cnt} 笔),下方列表与预签同一数据,不自动计入样本缴费`
- })
- const v2ReceivedAmount = computed(() => {
- let sum = 0
- for (const sp of samplePayments.value) {
- for (const sch of sp.schedules || []) {
- for (const tr of sch.transfers || []) {
- if (String(tr.payStatus || '') === 'yjf') {
- sum += Number(tr.transferAmount || 0) || 0
- }
- }
- }
- }
- return sum
- })
- const samples = computed(() => {
- const rows = (bundle.value?.samples as Array<Record<string, any>>) || []
- return [...rows].sort((a, b) => {
- const oa = Number(a.orderNum ?? 0)
- const ob = Number(b.orderNum ?? 0)
- if (oa !== ob) return oa - ob
- return String(a.createTime || '').localeCompare(String(b.createTime || ''))
- })
- })
- /** 严格按接口返回的库内明细展示,不做期次补齐 */
- const paymentDetails = computed(
- () => (bundle.value?.paymentDetails as Array<Record<string, any>>) || []
- )
- /**
- * v1 明细按期归组,结构对齐 v2 schedules(期汇总 + 多笔转账)
- * - 趸交:整单视为第 1 期
- * - 分期:按 installmentNo 归组
- */
- const v1PayPeriods = computed(() => {
- const details = paymentDetails.value
- if (!details.length) return [] as Array<{
- scheduleNo: number
- dueAmount: number | null
- dueDate: string
- paidAmount: number
- transfers: Array<Record<string, any>>
- }>
- const paidOf = (rows: Array<Record<string, any>>) =>
- rows
- .filter((d) => String(d.payStatus || '') === 'yjf')
- .reduce((sum, d) => sum + (Number(d.transferAmount) || 0), 0)
- const isFq = isInstallmentPayMode(String(payment.value?.payMode || ''))
- if (!isFq) {
- return [
- {
- scheduleNo: 1,
- dueAmount:
- details[0]?.dueAmount != null
- ? Number(details[0].dueAmount)
- : payment.value?.contractAmount != null
- ? Number(payment.value.contractAmount)
- : null,
- dueDate: String(
- payment.value?.lumpSumDeadline || details[0]?.installmentDueDate || ''
- ),
- paidAmount: paidOf(details),
- transfers: details
- }
- ]
- }
- const groups = new Map<number, Array<Record<string, any>>>()
- for (const d of details) {
- const no = Number(d.installmentNo) || 0
- const list = groups.get(no) || []
- list.push(d)
- groups.set(no, list)
- }
- return [...groups.entries()]
- .sort((a, b) => a[0] - b[0])
- .map(([no, rows]) => ({
- scheduleNo: no || 1,
- dueAmount: rows[0]?.dueAmount != null ? Number(rows[0].dueAmount) : null,
- dueDate: String(rows[0]?.installmentDueDate || ''),
- paidAmount: paidOf(rows),
- transfers: rows
- }))
- })
- /** 已收金额 = 已缴费明细的转账金额之和(排除待缴费、退费确认中、已退费) */
- const receivedAmount = computed(() => {
- return paymentDetails.value
- .filter((d) => {
- const s = String(d.payStatus || '')
- return s === 'yjf' || s === 'jfqrz'
- })
- .reduce((sum, d) => {
- const amt = Number(d.transferAmount || 0)
- return sum + (isNaN(amt) ? 0 : amt)
- }, 0)
- })
- const protocolAttachments = computed(() => {
- const list = bundle.value?.attachments
- return Array.isArray(list) ? list : []
- })
- const consentAttachments = computed(() => {
- const list = bundle.value?.consentAttachmentFiles
- return Array.isArray(list) ? list : []
- })
- const healthSurveyAttachments = computed(() => {
- const list = bundle.value?.healthSurveyAttachmentFiles
- return Array.isArray(list) ? list : []
- })
- const storageCertInfo = computed(() => {
- const sc = bundle.value?.storageCert as Record<string, any> | undefined | null
- if (sc) return sc
- if (!props.showStorageCert || !props.listRow) return null
- const row = props.listRow as Record<string, any>
- return {
- makeStatus: row.makeStatus ?? '0',
- certImageUrls: row.certImageUrls ?? '',
- makeDate: row.makeDate ?? '',
- handleTime: row.handleTime ?? '',
- handlerUserId: row.handlerUserId ?? '',
- handlerUserName: row.handlerUserName ?? '',
- remark: row.remark ?? ''
- }
- })
- const storageCertAttachments = computed(() => {
- const urls = String(storageCertInfo.value?.certImageUrls || '').trim()
- if (!urls) return []
- return urls
- .split(',')
- .map((u) => u.trim())
- .filter(Boolean)
- .map((url, index) => {
- const rawName = url.split('/').pop() || ''
- const fileName = decodeURIComponent(rawName) || `附件${index + 1}`
- return { id: `cert_${index}`, fileName, fileUrl: url, fileType: '' }
- })
- })
- const fullmoonInsuranceInfo = computed(() => {
- const fm = bundle.value?.fullmoonTodo as Record<string, any> | undefined
- if (fm) return fm
- if (!props.showFullmoonInsurance || !props.listRow) return null
- const row = props.listRow as Record<string, any>
- return {
- handleTime: row.handleTime ?? '',
- handlerUserId: row.handlerUserId ?? '',
- handlerUserName: row.handlerUserName ?? '',
- handleRemark: row.handleRemark ?? row.remark ?? '',
- insuranceAttachments: row.insuranceAttachments ?? ''
- }
- })
- const fullmoonInsuranceAttachments = computed(() => {
- const urls = String(fullmoonInsuranceInfo.value?.insuranceAttachments || '').trim()
- if (!urls) return []
- return urls
- .split(',')
- .map((u) => u.trim())
- .filter(Boolean)
- .map((url, index) => {
- const rawName = url.split('/').pop() || ''
- const fileName = decodeURIComponent(rawName) || `附件${index + 1}`
- return { id: `ins_${index}`, fileName, fileUrl: url, fileType: '' }
- })
- })
- /** 金额只读展示:千分位 + ¥ 前缀,null 显示 — */
- function fmtMoney(v: unknown): string {
- if (v == null || v === '') return '—'
- return '¥' + formatMoneyDisplay(v)
- }
- const dialogTitle = computed(() => {
- const name =
- partyCustomer.value?.motherName || props.listRow?.motherName || protocol.value?.motherName
- return `查看 — ${formatProtocolDocSubject('cell_storage', { motherName: name })}`
- })
- const hasAnyStatusChange = computed(() => {
- if (!protocol.value) return false
- return !!(
- protocol.value.effectiveTime ||
- protocol.value.expireTime ||
- protocol.value.voidTime ||
- protocol.value.toResearchTime
- )
- })
- const collectorDisplay = computed(() => {
- const fromList = props.listRow?.collectorUserName
- if (fromList) return String(fromList)
- const fromBundle = protocol.value?.collectorUserName
- if (fromBundle) return String(fromBundle)
- const id = protocol.value?.collectorUserId
- const u = collectorUserOptions.value.find((x) => x.id === id)
- return u?.nickName || dash(id)
- })
- const salesDisplay = computed(() => {
- const fromList = props.listRow?.salesUserName
- if (fromList) return String(fromList)
- const fromBundle = collectionInfo.value?.salesUserName
- if (fromBundle) return String(fromBundle)
- const id = collectionInfo.value?.salesUserId
- const u = salesUserOptions.value.find((x) => x.id === id)
- return u?.nickName || dash(id)
- })
- const regionManagerDisplay = computed(() => {
- const fromList = (props.listRow as any)?.regionManagerUserName
- if (fromList) return String(fromList)
- const fromBundle = (collectionInfo.value as any)?.regionManagerUserName
- if (fromBundle) return String(fromBundle)
- const id = (collectionInfo.value as any)?.regionManagerUserId
- return dash(id)
- })
- const regionDisplay = computed(() => {
- const fromList = props.listRow?.regionName
- if (fromList) return String(fromList)
- const fromBundle = collectionInfo.value?.regionName
- if (fromBundle) return String(fromBundle)
- const id = collectionInfo.value?.regionId
- const r = regionOptions.value.find((x) => x.id === id)
- return r?.regionName || dash(id)
- })
- const hospitalDisplay = computed(() => {
- const fromList = props.listRow?.hospitalName
- if (fromList) return String(fromList)
- const fromBundle = collectionInfo.value?.hospitalName
- if (fromBundle) return String(fromBundle)
- const id = collectionInfo.value?.hospitalId
- const h = hospitalOptions.value.find((x) => x.id === id)
- return h?.hospitalName || dash(id)
- })
- function dash(val: unknown) {
- return formatDisplayOrDash(val)
- }
- function formatYmd(val: unknown) {
- const s = String(val || '').replace(/-/g, '')
- if (s.length === 8) return `${s.slice(0, 4)}-${s.slice(4, 6)}-${s.slice(6, 8)}`
- return dash(val)
- }
- function formatInstallmentAmounts(values?: number[] | null) {
- if (!values?.length) return '—'
- return values.map((v) => fmtMoney(v)).join(' / ')
- }
- function formatYm(val: unknown) {
- const s = String(val || '')
- if (s.length === 6) return `${s.slice(0, 4)}-${s.slice(4, 6)}`
- return dash(val)
- }
- function payDetailColorClass(payStatus: string): string {
- const s = String(payStatus || '')
- if (s === 'ytf') return 'red'
- if (s === 'tfqrz') return 'amber'
- return 'green'
- }
- /** 缴费明细是否已完成财务收款确认(jfqrz 仅已提交待财务处理) */
- function isReceiptFinanceConfirmed(detail: Record<string, any>) {
- const s = String(detail.payStatus || '')
- return s !== 'jfqrz' && s !== 'dfj'
- }
- interface MiniFlowStep {
- key: string
- label: string
- dotClass: string
- dotIcon: string
- labelClass: string
- connectorToNext?: string
- hasDetailCard?: boolean
- }
- function buildMiniFlowSteps(detail: Record<string, any>): MiniFlowStep[] {
- const s = String(detail.payStatus || '')
- if (s === 'dfj') return []
- const steps: MiniFlowStep[] = []
- steps.push({
- key: 'pay',
- label: '缴费确认信息',
- dotClass: 'done',
- dotIcon: '✓',
- labelClass: 'done',
- connectorToNext: 'done',
- hasDetailCard: true
- })
- if (s === 'jfqrz') {
- steps.push({
- key: 'receipt-finance',
- label: '缴费财务确认信息',
- dotClass: 'current-amber',
- dotIcon: '●',
- labelClass: 'current-amber',
- hasDetailCard: true
- })
- } else {
- steps.push({
- key: 'receipt-finance',
- label: '缴费财务确认信息',
- dotClass: 'done',
- dotIcon: '✓',
- labelClass: 'done',
- connectorToNext: s === 'yjf' ? undefined : 'done',
- hasDetailCard: true
- })
- }
- if (s === 'tfqrz' || s === 'ytf') {
- steps.push({
- key: 'refund-apply',
- label: '退费确认信息',
- dotClass: s === 'tfqrz' ? 'current-amber' : 'done',
- dotIcon: s === 'tfqrz' ? '●' : '✓',
- labelClass: s === 'tfqrz' ? 'current-amber' : 'done',
- connectorToNext: s === 'ytf' ? 'done-red' : undefined,
- hasDetailCard: true
- })
- }
- if (s === 'ytf') {
- steps.push({
- key: 'refund-finance',
- label: '退费财务确认信息',
- dotClass: 'done-red',
- dotIcon: '✓',
- labelClass: 'done-red',
- hasDetailCard: true
- })
- }
- if (steps.length) {
- delete steps[steps.length - 1].connectorToNext
- }
- return steps
- }
- function miniFlowConnectorStyle(total: number, index: number): string {
- const leftPct = ((index + 0.5) / total) * 100
- const widthPct = 100 / total
- return `left: calc(${leftPct}% + 12px); width: calc(${widthPct}% - 12px);`
- }
- interface OverviewTimelineStep {
- key: string
- label: string
- subLabel: string
- dotClass: string
- dotIcon: string
- labelClass: string
- connectorToNext?: string
- }
- /** 缴费总览 — 横向时间线节点(每个缴费明细对应一个节点) */
- function buildOverviewTimelineSteps(): OverviewTimelineStep[] {
- const details = paymentDetails.value
- if (!details.length) return []
- const steps: OverviewTimelineStep[] = details.map((d, i) => {
- const s = String(d.payStatus || '')
- const name = isInstallmentPayMode(String(payment.value?.payMode || ''))
- ? `第${d.installmentNo || i + 1}期`
- : `缴费${i + 1}`
- if (s === 'ytf') {
- return {
- key: `ov-${i}`,
- label: name,
- subLabel: '已退费',
- dotClass: 'refund',
- dotIcon: '✓',
- labelClass: 'refund'
- }
- }
- if (s === 'tfqrz') {
- return {
- key: `ov-${i}`,
- label: name,
- subLabel: '退费中',
- dotClass: 'refund-current',
- dotIcon: '●',
- labelClass: 'refund-current',
- connectorToNext: 'refund-current'
- }
- }
- if (s === 'yjf') {
- return {
- key: `ov-${i}`,
- label: name,
- subLabel: '付款完成',
- dotClass: 'done',
- dotIcon: '✓',
- labelClass: 'done',
- connectorToNext: 'done'
- }
- }
- if (s === 'jfqrz') {
- return {
- key: `ov-${i}`,
- label: name,
- subLabel: '确认中',
- dotClass: 'current',
- dotIcon: '●',
- labelClass: 'done',
- connectorToNext: 'current'
- }
- }
- return {
- key: `ov-${i}`,
- label: name,
- subLabel: '已付款',
- dotClass: 'pending',
- dotIcon: '○',
- labelClass: ''
- }
- })
- // 填充 connectorToNext:下一个节点的连线颜色由本节点的状态决定
- for (let i = 0; i < steps.length - 1; i++) {
- if (!steps[i].connectorToNext) {
- steps[i].connectorToNext = steps[i].dotClass === 'pending' ? '' : 'done'
- }
- }
- return steps
- }
- function timelineConnectorStyle(total: number, index: number): string {
- const leftPct = ((index + 0.5) / total) * 100
- const widthPct = 100 / total
- return `left: calc(${leftPct}% + 14px); width: calc(${widthPct}% - 14px);`
- }
- function auditorDisplay(row: Record<string, unknown>) {
- const name = String(row.auditorUserName || '').trim()
- if (name) return name
- return dash(row.auditorUserId)
- }
- function sampleTypeName(id: string) {
- return sampleTypeOptions.value.find((x) => x.id === id)?.typeName || dash(id)
- }
- function samplePayTitle(sp: Record<string, any>) {
- const code = String(sp.sampleCode || '').trim()
- const sample = samples.value.find((s) => String(s.sampleCode || '') === code)
- if (sample?.sampleTypeName) return String(sample.sampleTypeName)
- if (sample?.sampleTypeId) return sampleTypeName(String(sample.sampleTypeId))
- return code || '样本'
- }
- function ensureUnitsInOptions(samples: Array<Record<string, any>>) {
- const merged = [...unitOptions.value]
- for (const s of samples) {
- const id = String(s.unitId || '')
- if (!id) continue
- const name = String(s.unitName || '').trim()
- const existing = merged.find((x) => x.id === id)
- if (existing) {
- if (name && (!existing.unitName || existing.unitName === existing.id)) {
- existing.unitName = name
- }
- continue
- }
- merged.push({ id, unitName: name || id })
- }
- unitOptions.value = merged
- }
- function siteName(id: string) {
- return siteOptions.value.find((x) => x.id === id)?.siteName || dash(id)
- }
- function expressName(id: string) {
- return expressCompanyOptions.value.find((x) => x.id === id)?.expressName || dash(id)
- }
- function sampleTransportExtra(transport: Record<string, any> | undefined) {
- if (!transport?.transportType) return '—'
- if (transport.transportType === 'kdys') {
- const parts: string[] = []
- const ec = expressName(String(transport.expressCompanyId || ''))
- if (ec !== '—') parts.push(ec)
- if (transport.waybillNo) parts.push(String(transport.waybillNo))
- if (transport.expressFee != null && transport.expressFee !== '') {
- parts.push(`¥${transport.expressFee}`)
- }
- return parts.length ? parts.join(' · ') : '—'
- }
- if (transport.transportType === 'ryxd') {
- const parts: string[] = []
- if (transport.carrierName) parts.push(String(transport.carrierName))
- if (transport.carrierPhone) parts.push(String(transport.carrierPhone))
- return parts.length ? parts.join(' · ') : '—'
- }
- return '—'
- }
- function sampleReceptionAttachmentFiles(reception: Record<string, any>) {
- const list = reception?.receiveAttachmentFiles
- if (Array.isArray(list) && list.length) return list
- const urls = String(reception?.receiveAttachments || '')
- if (!urls.trim()) return []
- return urls
- .split(',')
- .map((u, i) => {
- const url = u.trim()
- if (!url) return null
- const name = url.includes('/') ? url.split('/').pop() || `附件${i + 1}` : url
- return { id: `recv_${i}`, fileUrl: url, fileName: name }
- })
- .filter(Boolean) as Array<Record<string, unknown>>
- }
- function detailAttachments(detail: Record<string, any>) {
- return Array.isArray(detail.attachments) ? detail.attachments : []
- }
- function transferAttachments(tr: Record<string, any>) {
- if (Array.isArray(tr.attachments) && tr.attachments.length) return tr.attachments
- return urlsToAttachmentFiles(String(tr.transferAttachments || ''))
- }
- function financeConfirmAttachmentFiles(detail: Record<string, any>) {
- if (Array.isArray(detail.financeConfirmAttachmentFiles)) {
- return detail.financeConfirmAttachmentFiles
- }
- return urlsToAttachmentFiles(String(detail.financeConfirmAttachments || ''))
- }
- function refundAttachmentFiles(detail: Record<string, any>) {
- if (Array.isArray(detail.refundAttachmentFiles)) {
- return detail.refundAttachmentFiles
- }
- return urlsToAttachmentFiles(String(detail.refundAttachments || ''))
- }
- function refundFinanceConfirmAttachmentFiles(detail: Record<string, any>) {
- if (Array.isArray(detail.refundFinanceConfirmAttachmentFiles)) {
- return detail.refundFinanceConfirmAttachmentFiles
- }
- return urlsToAttachmentFiles(String(detail.refundFinanceConfirmAttachments || ''))
- }
- function urlsToAttachmentFiles(urls: string) {
- if (!urls.trim()) return []
- return urls
- .split(',')
- .map((u, i) => {
- const url = u.trim()
- if (!url) return null
- const name = url.includes('/') ? url.split('/').pop() || `附件${i + 1}` : url
- return { id: `url_${i}`, fileUrl: url, fileName: name, fileSize: 0 }
- })
- .filter(Boolean) as Array<Record<string, any>>
- }
- function fullFileUrl(suffix: string) {
- if (!suffix) return ''
- if (suffix.startsWith('http://') || suffix.startsWith('https://') || suffix.startsWith('/'))
- return suffix
- return `${minioBaseUrl}/${suffix}`
- }
- function isVideoFile(file: { fileType?: string; fileName?: string }) {
- const t = (file.fileType || '').toLowerCase()
- if (t.startsWith('video/')) return true
- return /\.(mp4|webm|ogg|mov)$/i.test(file.fileName || '')
- }
- const previewVisible = ref(false)
- const previewUrl = ref('')
- const previewType = ref<'image' | 'video'>('image')
- function openPreview(file: Record<string, any>) {
- if (!file.fileUrl) return
- previewUrl.value = fullFileUrl(file.fileUrl)
- previewType.value = isVideoFile(file) ? 'video' : 'image'
- previewVisible.value = true
- }
- function downloadFile(file: Record<string, any>) {
- downloadAttachmentFile(file, { fullFileUrl })
- }
- async function loadBaseLookups() {
- const [stRes, uRes, ecRes, siteRes] = await Promise.all([
- fetchCellSampleTypeList({ page: 1, pageSize: 100 }),
- fetchItemUnitList({ page: 1, pageSize: 100 }),
- fetchExpressCompanyList({ page: 1, pageSize: 100 }),
- fetchSiteList({ page: 1, pageSize: 100 })
- ])
- sampleTypeOptions.value = (stRes.list || []) as any
- unitOptions.value = (uRes.list || []) as any
- expressCompanyOptions.value = ecRes.list || []
- siteOptions.value = siteRes.list || []
- }
- async function loadConditionalLookups(data: Record<string, any>) {
- const ci = data.collectionInfo as Record<string, any> | null
- const p = data.protocol as Record<string, any> | undefined
- const tasks: Promise<void>[] = []
- if (ci?.regionId) {
- tasks.push(
- fetchCollectionRegionList({ page: 1, pageSize: 100 }).then((res) => {
- regionOptions.value = res.list || []
- })
- )
- tasks.push(
- fetchHospitalList(String(ci.regionId), { page: 1, pageSize: 100 }).then((res) => {
- hospitalOptions.value = res.list || []
- })
- )
- }
- if (p?.collectorUserId) {
- tasks.push(
- fetchUserList({ page: 1, pageSize: 50, status: '1' }).then((res) => {
- collectorUserOptions.value = res.list || []
- })
- )
- }
- if (ci?.salesUserId) {
- tasks.push(
- fetchUserList({ page: 1, pageSize: 50, status: '1' }).then((res) => {
- salesUserOptions.value = res.list || []
- })
- )
- }
- if (tasks.length) {
- await Promise.all(tasks)
- }
- }
- async function finishLookups(data: Record<string, any>, baseLookupsPromise: Promise<void>) {
- lookupLoading.value = true
- try {
- await baseLookupsPromise
- ensureUnitsInOptions((data.samples as Array<Record<string, any>>) || [])
- await loadConditionalLookups(data)
- } finally {
- lookupLoading.value = false
- }
- }
- async function loadAuditRecords() {
- if (!props.showAuditHistory || !props.protocolId) {
- auditRecords.value = []
- return
- }
- auditLoading.value = true
- try {
- const res = await fetchCollectionProtocolAuditList({
- page: 1,
- pageSize: 100,
- collectionProtocolId: props.protocolId
- })
- auditRecords.value = res.list || []
- } catch {
- auditRecords.value = []
- } finally {
- auditLoading.value = false
- }
- }
- async function loadDetail() {
- if (!props.protocolId) return
- detailLoading.value = true
- lookupLoading.value = true
- bundle.value = null
- auditRecords.value = []
- const baseLookupsPromise = loadBaseLookups().catch(() => {})
- const auditPromise = props.showAuditHistory ? loadAuditRecords() : null
- let data: Record<string, any> | null = null
- try {
- data = (await fetchCollectionProtocolDetail(props.protocolId)) as Record<string, any>
- bundle.value = data
- } finally {
- detailLoading.value = false
- }
- if (!data) {
- lookupLoading.value = false
- return
- }
- void finishLookups(data, baseLookupsPromise)
- void auditPromise
- if (props.highlightSampleCode) {
- nextTick(() => {
- document
- .getElementById(`view-sample-${props.highlightSampleCode}`)
- ?.scrollIntoView({ behavior: 'smooth', block: 'center' })
- })
- }
- }
- function scrollToSection(key: string) {
- if (detailLoading.value && !bundle.value) return
- activeTab.value = key
- nextTick(() => {
- document
- .getElementById(`view-section-${key}`)
- ?.scrollIntoView({ behavior: 'smooth', block: 'start' })
- })
- }
- function handleClosed() {
- bundle.value = null
- auditRecords.value = []
- activeTab.value = 'basic'
- detailLoading.value = false
- lookupLoading.value = false
- auditLoading.value = false
- }
- watch(
- () => [props.modelValue, props.protocolId] as const,
- ([visible, id]) => {
- if (visible) {
- void loadTransferTypeDict()
- void loadVoidTypeDict()
- }
- if (visible && id) loadDetail()
- }
- )
- watch(
- () => [props.modelValue, props.highlightSampleCode, bundle.value] as const,
- ([visible, code]) => {
- if (!visible || !code || !bundle.value) return
- nextTick(() => {
- document.getElementById(`view-sample-${code}`)?.scrollIntoView({
- behavior: 'smooth',
- block: 'center'
- })
- })
- }
- )
- </script>
- <style scoped lang="scss">
- .protocol-view-body {
- overflow: visible;
- &.is-detail-loading {
- min-height: min(680px, calc(100vh - 220px));
- }
- }
- /* ---- KPI 摘要条(统一薄荷绿通栏 + 图标徽章) ---- */
- .kpi-strip {
- display: grid;
- grid-template-columns: repeat(4, 1fr);
- gap: 0;
- margin-bottom: 24px;
- padding: 6px;
- background: linear-gradient(135deg, #e8f4ee 0%, #f8fafc 52%, #e8f4ee 100%);
- border: 1px solid #a8d4c4;
- border-radius: 12px;
- box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.85);
- }
- .kpi-card {
- display: flex;
- align-items: flex-start;
- gap: 12px;
- padding: 14px 18px;
- background: transparent;
- border-radius: 8px;
- transition: background 0.2s;
- &:not(:last-child) {
- border-right: 1px solid rgba(168, 212, 196, 0.75);
- }
- &:hover {
- background: rgba(255, 255, 255, 0.55);
- }
- }
- .kpi-card__icon {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 40px;
- height: 40px;
- flex-shrink: 0;
- border-radius: 10px;
- background: #fff;
- border: 1px solid #a8d4c4;
- color: #065f46;
- font-size: 18px;
- }
- .kpi-card__main {
- min-width: 0;
- flex: 1;
- }
- .kpi-label {
- margin-bottom: 4px;
- font-size: 11px;
- font-weight: 500;
- color: var(--mint-text-muted, #334155);
- }
- .kpi-value {
- font-size: 15px;
- font-weight: 600;
- color: #0f172a;
- display: flex;
- align-items: center;
- gap: 6px;
- &--emphasis {
- font-size: 16px;
- font-weight: 700;
- color: #047857;
- letter-spacing: 0.02em;
- }
- }
- /* ---- Section Card ---- */
- .section-card {
- border-radius: 12px;
- padding: 22px 24px;
- margin-bottom: 20px;
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
- transition:
- transform 0.2s,
- box-shadow 0.2s;
- position: relative;
- &:hover {
- transform: translateY(-1px);
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
- }
- &--white {
- background: #fff;
- }
- &--green {
- background: #f0fdf4;
- }
- }
- .section-title {
- display: flex;
- align-items: center;
- margin-bottom: 18px;
- padding-bottom: 12px;
- border-bottom: 1px solid #e2e8f0;
- &::before {
- content: '';
- width: 3px;
- height: 18px;
- background: #047857;
- border-radius: 2px;
- margin-right: 10px;
- flex-shrink: 0;
- }
- h3 {
- margin: 0;
- font-size: 15px;
- font-weight: 600;
- color: #1e293b;
- line-height: 1.4;
- }
- &--banner {
- margin: -22px -24px 18px;
- padding: 10px 24px;
- border-bottom: 1px solid rgba(16, 185, 129, 0.12);
- border-radius: 12px 12px 0 0;
- background: var(--primary-bg, #ecfdf5);
- h3 {
- color: var(--primary-dark, #059669);
- }
- }
- }
- /* ---- Data Grid ---- */
- .data-grid {
- display: grid;
- gap: 14px 20px;
- &--2 {
- grid-template-columns: repeat(2, minmax(0, 1fr));
- }
- &--3 {
- grid-template-columns: repeat(3, minmax(0, 1fr));
- }
- &--4 {
- grid-template-columns: repeat(4, minmax(0, 1fr));
- }
- &--6 {
- grid-template-columns: repeat(6, minmax(0, 1fr));
- }
- &--7 {
- grid-template-columns: repeat(7, minmax(0, 1fr));
- }
- }
- .data-grid--collection-row {
- gap: 12px 14px;
- }
- .data-item {
- min-width: 0;
- &--full {
- grid-column: 1 / -1;
- }
- }
- .data-label {
- font-size: 12px;
- color: var(--mint-text-muted, #334155);
- margin-bottom: 3px;
- }
- .data-value {
- font-size: 14px;
- color: #1e293b;
- font-weight: 500;
- word-break: break-word;
- &--muted {
- color: var(--mint-text-muted, #334155);
- font-weight: 400;
- }
- }
- /* 协议附件 / 知情同意书 / 孕妇健康调查表(三行堆叠) */
- .protocol-basic-attachments-block {
- margin-top: 14px;
- display: flex;
- flex-direction: column;
- gap: 14px;
- &--flush {
- margin-top: 0;
- }
- }
- .handle-summary-cards {
- display: grid;
- grid-template-columns: repeat(2, 1fr);
- gap: 16px;
- }
- .handle-summary-card {
- background: #fff;
- border-radius: 10px;
- padding: 18px 20px;
- border: 1px solid rgba(0, 0, 0, 0.03);
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
- .attachment-row,
- .remark-row {
- margin-top: 12px;
- padding-top: 12px;
- border-top: 1px dashed #e2e8f0;
- }
- }
- .protocol-attachments-item {
- .protocol-attachments-value {
- font-weight: 400;
- }
- :deep(.protocol-attachment-empty) {
- font-size: 14px;
- color: var(--mint-text-muted, #334155);
- }
- }
- /* ---- Attachment / Remark Row ---- */
- .attachment-row {
- margin-top: 12px;
- padding-top: 12px;
- border-top: 1px dashed #e2e8f0;
- display: flex;
- flex-direction: column;
- gap: 8px;
- .data-label {
- font-size: 12px;
- color: var(--mint-text-muted, #334155);
- }
- }
- .remark-row {
- margin-top: 12px;
- padding-top: 12px;
- border-top: 1px dashed #e2e8f0;
- }
- /* ---- Status Change Section ---- */
- .status-change-section {
- margin-top: 14px;
- padding-top: 14px;
- border-top: 1px dashed #e2e8f0;
- }
- .status-change-title {
- font-size: 12px;
- font-weight: 600;
- color: var(--mint-text-muted, #334155);
- margin-bottom: 12px;
- }
- /* ---- Client Sub Cards (🤱👨👶) ---- */
- .client-sub-cards {
- display: grid;
- grid-template-columns: repeat(3, 1fr);
- gap: 16px;
- }
- .client-sub-card {
- background: #fff;
- border-radius: 10px;
- padding: 18px 20px;
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
- border: 1px solid rgba(0, 0, 0, 0.03);
- transition:
- transform 0.2s,
- box-shadow 0.2s;
- &:hover {
- transform: translateY(-1px);
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.07);
- }
- }
- .sub-card-header {
- display: flex;
- align-items: center;
- gap: 8px;
- margin-bottom: 14px;
- padding-bottom: 10px;
- border-bottom: 1px solid #f1f5f9;
- h4 {
- margin: 0;
- font-size: 14px;
- font-weight: 600;
- color: #1e293b;
- }
- }
- .sub-card-icon {
- font-size: 22px;
- line-height: 1;
- }
- .sub-card-full-row {
- margin-top: 10px;
- padding-top: 10px;
- border-top: 1px dashed #e2e8f0;
- display: flex;
- flex-direction: column;
- gap: 2px;
- }
- /* ---- 审核记录内嵌列表(对齐 OpenSpec 嵌套列表 14px 标准) ---- */
- .view-inner-table-wrap.list-body.list-wrap {
- margin-top: 4px;
- border: 1px solid var(--mint-border, #e2e8f0);
- border-radius: var(--mint-radius, 10px);
- overflow: hidden;
- background: #fff;
- :deep(.el-table) {
- --el-table-header-bg-color: var(--mint-table-head, #f8fafc);
- --el-table-border-color: var(--mint-border, #e2e8f0);
- --el-table-row-hover-bg-color: var(--mint-row-hover, #f0fdf4);
- --el-table-text-color: var(--mint-text, #0f172a);
- --el-table-header-text-color: var(--mint-text-muted, #64748b);
- font-size: 14px;
- }
- :deep(.el-table th.el-table__cell) {
- font-size: 12px;
- font-weight: 600;
- color: var(--mint-text-muted, #64748b);
- }
- :deep(.el-table td.el-table__cell) {
- font-size: 14px;
- font-weight: 500;
- color: var(--mint-text, #0f172a);
- }
- :deep(.el-empty) {
- padding: 24px 0;
- }
- }
- /* ---- Sample Cards ---- */
- .sample-cards {
- display: flex;
- flex-direction: column;
- gap: 14px;
- }
- .sample-card {
- background: #fff;
- border-radius: 10px;
- border: 1px solid rgba(0, 0, 0, 0.05);
- overflow: hidden;
- transition:
- transform 0.2s,
- box-shadow 0.2s;
- &:hover {
- transform: translateY(-1px);
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06);
- }
- &.is-highlight {
- border-color: var(--primary, #10b981);
- box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.18);
- }
- }
- .sample-card__header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 12px 18px;
- background: #f8fafc;
- border-bottom: 1px solid #f1f5f9;
- }
- .sample-card__title {
- display: flex;
- align-items: center;
- gap: 8px;
- font-size: 14px;
- font-weight: 600;
- color: #1e293b;
- }
- .sample-card__seq {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- width: 22px;
- height: 22px;
- border-radius: 6px;
- background: #047857;
- color: #fff;
- font-size: 11px;
- font-weight: 700;
- flex-shrink: 0;
- }
- .sample-card__body {
- padding: 16px 18px;
- display: flex;
- flex-direction: column;
- gap: 14px;
- }
- .sample-card__section {
- &--transport,
- &--reception {
- margin-top: 2px;
- padding-top: 12px;
- border-top: 1px dashed #e2e8f0;
- }
- }
- :deep(.sample-card__section--qc) {
- margin-top: 2px;
- padding-top: 12px;
- border-top: 1px dashed #e2e8f0;
- }
- .sample-reception-empty {
- font-size: 13px;
- color: var(--mint-text-muted, #334155);
- }
- .check-list {
- display: flex;
- flex-wrap: wrap;
- gap: 12px 20px;
- }
- .check-list-view {
- pointer-events: none;
- :deep(.el-checkbox.is-disabled) {
- opacity: 1;
- cursor: default;
- .el-checkbox__label {
- color: var(--el-text-color-regular);
- }
- .el-checkbox__input.is-disabled {
- opacity: 1;
- cursor: default;
- .el-checkbox__inner {
- background-color: var(--el-checkbox-bg-color);
- border-color: var(--el-checkbox-input-border-color);
- cursor: default;
- }
- &.is-checked .el-checkbox__inner {
- background-color: var(--el-checkbox-checked-bg-color);
- border-color: var(--el-checkbox-checked-input-border-color);
- &::after {
- border-color: var(--el-checkbox-checked-icon-color);
- }
- }
- }
- }
- }
- .sample-card__attachments {
- margin-top: 12px;
- }
- .sample-card__section-label {
- margin: 0 0 10px;
- padding-left: 10px;
- font-size: 13px;
- font-weight: 600;
- line-height: 1.4;
- color: #334155;
- border-left: 3px solid #10b981;
- }
- /* 样本卡内子组件小节标题:与 sample-card__section-label 统一 */
- :deep(.sample-card__section-label),
- :deep(.sample-qc-grouped-panel__subtitle),
- :deep(.detail-section-title) {
- margin: 0 0 10px;
- padding-left: 10px;
- font-size: 13px;
- font-weight: 600;
- line-height: 1.4;
- color: #334155;
- border-left: 3px solid #10b981;
- }
- .data-value--code {
- font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Mono', monospace;
- font-size: 13px;
- color: var(--mint-text-muted, #334155);
- }
- .data-value--price {
- color: #065f46;
- font-weight: 700;
- }
- /* ---- Payment Summary (Metrics) ---- */
- .payment-summary {
- display: grid;
- grid-template-columns: repeat(4, 1fr);
- gap: 12px;
- margin-bottom: 18px;
- &--3 {
- grid-template-columns: repeat(3, 1fr);
- }
- }
- .payment-metric {
- background: #fff;
- border-radius: 8px;
- padding: 12px 16px;
- border: 1px solid #e2e8f0;
- }
- .pm-label {
- font-size: 11px;
- color: var(--mint-text-muted, #334155);
- margin-bottom: 4px;
- }
- .pm-value {
- font-size: 16px;
- font-weight: 700;
- color: #1e293b;
- &--amount {
- color: #065f46;
- }
- &--received {
- color: #047857;
- }
- }
- /* ---- Payment Detail Cards ---- */
- .payment-details {
- display: flex;
- flex-direction: column;
- gap: 16px;
- }
- .payment-detail-card {
- background: #fff;
- border-radius: 8px;
- padding: 14px 16px;
- border: 1px solid #e2e8f0;
- &--refunded {
- border-left: 3px solid #dc2626;
- border-color: #e2e8f0;
- border-left-color: #dc2626;
- }
- &--refund-pending {
- border-left: 3px solid #d97706;
- border-color: #e2e8f0;
- border-left-color: #d97706;
- }
- }
- /* ---- PDC Header ---- */
- .pdc-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- margin-bottom: 12px;
- padding-bottom: 10px;
- border-bottom: 1px solid #f1f5f9;
- }
- .pdc-header-left {
- display: flex;
- align-items: center;
- gap: 6px;
- font-size: 14px;
- font-weight: 600;
- color: #1e293b;
- &::before {
- content: '';
- width: 2px;
- height: 14px;
- border-radius: 1px;
- flex-shrink: 0;
- }
- &.green::before {
- background: #047857;
- }
- &.red::before {
- background: #dc2626;
- }
- &.amber::before {
- background: #d97706;
- }
- }
- .pdc-header-amount {
- font-size: 14px;
- font-weight: 700;
- margin-left: 4px;
- &.green {
- color: #065f46;
- }
- &.red {
- color: #dc2626;
- }
- &.amber {
- color: #d97706;
- }
- }
- /* ---- PDC Info Row ---- */
- .pdc-info-row {
- display: grid;
- grid-template-columns: repeat(4, minmax(0, 1fr));
- gap: 14px 20px;
- margin-bottom: 12px;
- }
- .pdc-info-item {
- display: flex;
- flex-direction: column;
- gap: 2px;
- .info-label {
- font-size: 11px;
- color: var(--mint-text-muted, #334155);
- }
- .info-value {
- font-size: 13px;
- color: #1e293b;
- font-weight: 500;
- }
- }
- .pdc-attachments-row {
- margin-bottom: 8px;
- padding-top: 8px;
- border-top: 1px dashed #e2e8f0;
- display: flex;
- flex-direction: column;
- gap: 4px;
- }
- /* ---- Payment Section Subtitle ---- */
- .section-subtitle {
- margin: 0 0 14px;
- font-size: 12px;
- font-weight: 600;
- color: var(--mint-text-muted, #334155);
- }
- .sample-pay-readonly {
- display: flex;
- flex-direction: column;
- gap: 12px;
- margin-bottom: 16px;
- }
- .sample-pay-readonly__card {
- padding: 12px 14px;
- border: 1px solid var(--mint-border, #e2e8f0);
- border-radius: 10px;
- background: #fff;
- }
- .sample-pay-readonly__head {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 12px;
- margin-bottom: 12px;
- font-size: 13px;
- font-weight: 600;
- }
- .sample-pay-readonly__title {
- display: flex;
- flex-wrap: wrap;
- gap: 8px;
- align-items: center;
- }
- .sample-pay-readonly__amt {
- flex-shrink: 0;
- color: #047857;
- }
- .sample-pay-readonly__batch-title {
- margin: 14px 0 8px;
- padding-left: 10px;
- font-size: 13px;
- font-weight: 600;
- line-height: 1.4;
- color: #334155;
- border-left: 3px solid #10b981;
- &--mt {
- margin-top: 20px;
- }
- }
- .sample-pay-readonly__empty {
- margin: 4px 0 8px;
- font-size: 12px;
- color: var(--mint-text-muted, #94a3b8);
- &--sm {
- margin: 0;
- padding: 2px 0 0;
- }
- }
- .sample-pay-readonly__sch {
- margin-top: 10px;
- padding: 12px 12px 10px;
- border-radius: 8px;
- background: #f8fafc;
- border: 1px solid #eef2f7;
- }
- .sample-pay-readonly__sch-title {
- margin-bottom: 10px;
- font-size: 13px;
- font-weight: 600;
- color: #0f172a;
- }
- .sample-pay-readonly__tr-title {
- margin: 12px 0 8px;
- font-size: 12px;
- font-weight: 600;
- color: #475569;
- }
- .sample-pay-readonly__tr-count {
- font-weight: 400;
- color: #94a3b8;
- }
- .sample-pay-readonly__sch .view-inner-table-wrap {
- margin-top: 0;
- }
- .muted-dash {
- color: var(--mint-text-muted, #94a3b8);
- }
- /* ---- v1 缴费明细(简洁平铺) ---- */
- .pay-detail-simple {
- display: flex;
- flex-direction: column;
- gap: 12px;
- margin-bottom: 16px;
- }
- .pay-detail-simple__card {
- padding: 12px 14px;
- border: 1px solid var(--mint-border, #e2e8f0);
- border-radius: 10px;
- background: #fff;
- }
- .pay-detail-simple__head {
- display: flex;
- gap: 12px;
- align-items: center;
- justify-content: space-between;
- margin-bottom: 12px;
- }
- .pay-detail-simple__title {
- display: flex;
- flex-wrap: wrap;
- gap: 8px;
- align-items: center;
- font-size: 13px;
- font-weight: 600;
- color: #0f172a;
- }
- .pay-detail-simple__amt {
- flex-shrink: 0;
- font-size: 14px;
- font-weight: 700;
- color: #047857;
- }
- .pay-detail-simple__attach {
- display: flex;
- flex-wrap: wrap;
- gap: 8px;
- align-items: flex-start;
- margin-top: 10px;
- padding-top: 10px;
- border-top: 1px dashed #e2e8f0;
- }
- .pay-detail-simple__attach-label {
- flex-shrink: 0;
- margin-top: 4px;
- font-size: 12px;
- color: #64748b;
- }
- .pay-detail-simple__extra {
- margin-top: 10px;
- padding-top: 10px;
- border-top: 1px dashed #e2e8f0;
- }
- /* ---- Mini Flow ---- */
- .mini-flow {
- display: flex;
- align-items: center;
- gap: 0;
- margin: 12px 0;
- padding: 10px 14px;
- background: #f8fafc;
- border-radius: 8px;
- position: relative;
- }
- .mini-flow-step {
- display: flex;
- flex-direction: column;
- align-items: center;
- position: relative;
- flex: 1;
- z-index: 1;
- }
- .mini-flow-dot {
- width: 24px;
- height: 24px;
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 11px;
- font-weight: 700;
- z-index: 1;
- &.done {
- background: #047857;
- color: #fff;
- box-shadow: 0 0 0 3px rgba(4, 120, 87, 0.15);
- }
- &.done-red {
- background: #dc2626;
- color: #fff;
- box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.15);
- }
- &.current-amber {
- background: #fff;
- border: 2px solid #d97706;
- color: #d97706;
- box-shadow: 0 0 0 3px rgba(217, 119, 6, 0.15);
- animation: pulseAmber 2s ease-in-out infinite;
- }
- &.pending-gray {
- background: #f1f5f9;
- color: #94a3b8;
- border: 2px solid #e2e8f0;
- }
- }
- .mini-flow-label {
- font-size: 10px;
- margin-top: 6px;
- text-align: center;
- max-width: 96px;
- line-height: 1.3;
- &.done {
- color: #065f46;
- font-weight: 600;
- }
- &.done-red {
- color: #dc2626;
- font-weight: 600;
- }
- &.current-amber {
- color: #d97706;
- font-weight: 600;
- }
- &.pending-gray {
- color: #94a3b8;
- }
- }
- /* ---- Flow Section: track + detail cards ---- */
- .flow-section {
- margin-top: 12px;
- }
- .flow-detail-cards {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
- gap: 12px;
- margin-top: 14px;
- }
- .flow-detail-card {
- border: 1px solid #e2e8f0;
- border-radius: 8px;
- overflow: hidden;
- &--current-amber {
- border-color: #fde68a;
- background: #fffbeb;
- }
- &--done-red {
- border-left: 3px solid #dc2626;
- background: #fff;
- }
- &--pending-gray {
- border-style: dashed;
- background: #f8fafc;
- }
- }
- .flow-detail-card__header {
- display: flex;
- align-items: center;
- gap: 8px;
- padding: 10px 14px;
- border-bottom: 1px solid #f1f5f9;
- background: #f8fafc;
- .flow-detail-card--done-red & {
- background: #fef2f2;
- border-bottom-color: #fecaca;
- }
- .flow-detail-card--current-amber & {
- background: #fffbeb;
- border-bottom-color: #fde68a;
- }
- .flow-detail-card--pending-gray & {
- background: #f1f5f9;
- border-bottom-color: #e2e8f0;
- }
- }
- .flow-detail-card__dot {
- width: 20px;
- height: 20px;
- border-radius: 50%;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- font-size: 10px;
- font-weight: 700;
- flex-shrink: 0;
- &.done {
- background: #047857;
- color: #fff;
- }
- &.done-red {
- background: #dc2626;
- color: #fff;
- }
- &.current-amber {
- background: #fff;
- border: 2px solid #d97706;
- color: #d97706;
- }
- &.pending-gray {
- background: #f1f5f9;
- border: 2px solid #e2e8f0;
- color: #94a3b8;
- }
- }
- .flow-detail-card__title {
- font-size: 13px;
- font-weight: 600;
- color: #1e293b;
- &.done {
- color: #065f46;
- }
- &.done-red {
- color: #dc2626;
- }
- &.current-amber {
- color: #d97706;
- }
- &.pending-gray {
- color: #94a3b8;
- }
- }
- .flow-detail-card__body {
- padding: 12px 14px;
- display: flex;
- flex-direction: column;
- gap: 8px;
- }
- .flow-detail-card__kv {
- display: flex;
- flex-direction: column;
- gap: 2px;
- }
- .flow-detail-card__label {
- font-size: 11px;
- color: var(--mint-text-muted, #334155);
- }
- .flow-detail-card__value {
- font-size: 13px;
- color: #1e293b;
- font-weight: 500;
- word-break: break-word;
- &.red {
- color: #dc2626;
- font-weight: 700;
- }
- &.amber {
- color: #d97706;
- font-weight: 700;
- }
- &.green {
- color: #065f46;
- font-weight: 700;
- }
- }
- .flow-detail-card__empty {
- font-size: 12px;
- color: var(--mint-text-muted, #334155);
- font-style: italic;
- }
- .mini-flow-connector {
- position: absolute;
- top: 22px;
- height: 2px;
- z-index: 0;
- &.done {
- background: #047857;
- }
- &.done-red {
- background: #dc2626;
- }
- &.current-amber {
- background: #d97706;
- }
- &.pending-gray {
- background: #e2e8f0;
- }
- }
- /* ---- Refund Area ---- */
- .refund-area {
- margin-top: 12px;
- padding: 14px 16px;
- border-radius: 8px;
- display: flex;
- flex-direction: column;
- gap: 10px;
- &.red-tint {
- background: #fef2f2;
- border: 1px solid #fecaca;
- }
- &.amber-tint {
- background: #fffbeb;
- border: 1px solid #fde68a;
- }
- }
- .refund-area-title {
- font-size: 12px;
- font-weight: 600;
- margin-bottom: 2px;
- display: flex;
- align-items: center;
- gap: 4px;
- &.red {
- color: #dc2626;
- }
- &.amber {
- color: #d97706;
- }
- }
- .refund-area-grid {
- display: grid;
- grid-template-columns: repeat(4, minmax(0, 1fr));
- gap: 14px 20px;
- .data-label {
- font-size: 11px;
- }
- .data-value {
- font-size: 13px;
- &.red {
- color: #dc2626;
- font-weight: 700;
- }
- &.amber {
- color: #d97706;
- font-weight: 700;
- }
- }
- .full-col {
- grid-column: 1 / -1;
- }
- }
- /* ---- Finance Area ---- */
- .finance-area {
- margin-top: 12px;
- padding: 12px 16px;
- border-radius: 8px;
- background: #f0fdf4;
- border: 1px solid #bbf7d0;
- display: flex;
- flex-direction: column;
- gap: 8px;
- }
- .finance-area-title {
- font-size: 12px;
- font-weight: 600;
- color: #065f46;
- margin-bottom: 2px;
- }
- .finance-area-grid {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 8px 20px;
- .data-label {
- font-size: 11px;
- }
- .data-value {
- font-size: 13px;
- }
- }
- @keyframes pulseAmber {
- 0%,
- 100% {
- box-shadow: 0 0 0 3px rgba(217, 119, 6, 0.15);
- }
- 50% {
- box-shadow: 0 0 0 6px rgba(217, 119, 6, 0.25);
- }
- }
- /* ---- Payment Overview Timeline ---- */
- .payment-timeline {
- margin-top: 16px;
- padding-top: 14px;
- border-top: 1px dashed #e2e8f0;
- }
- .timeline-title {
- font-size: 12px;
- color: var(--mint-text-muted, #334155);
- font-weight: 600;
- margin-bottom: 12px;
- }
- .timeline {
- display: flex;
- align-items: center;
- gap: 0;
- position: relative;
- }
- .timeline-step {
- display: flex;
- flex-direction: column;
- align-items: center;
- position: relative;
- flex: 1;
- }
- .timeline-dot {
- width: 28px;
- height: 28px;
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 12px;
- font-weight: 600;
- z-index: 1;
- transition: transform 0.2s;
- &.done {
- background: #047857;
- color: #fff;
- box-shadow: 0 0 0 4px rgba(4, 120, 87, 0.15);
- }
- &.current {
- background: #fff;
- border: 2px solid #047857;
- color: #047857;
- box-shadow: 0 0 0 4px rgba(4, 120, 87, 0.1);
- }
- &.pending {
- background: #f1f5f9;
- color: #94a3b8;
- border: 2px solid #e2e8f0;
- }
- &.refund {
- background: #dc2626;
- color: #fff;
- box-shadow: 0 0 0 4px rgba(220, 38, 38, 0.15);
- }
- &.refund-current {
- background: #fff;
- border: 2px solid #d97706;
- color: #d97706;
- box-shadow: 0 0 0 4px rgba(217, 119, 6, 0.15);
- animation: pulseAmber 2s ease-in-out infinite;
- }
- }
- .timeline-label {
- font-size: 11px;
- color: var(--mint-text-muted, #334155);
- margin-top: 8px;
- text-align: center;
- max-width: 80px;
- line-height: 1.4;
- &.done {
- color: #065f46;
- font-weight: 600;
- }
- &.refund {
- color: #dc2626;
- font-weight: 600;
- }
- &.refund-current {
- color: #d97706;
- font-weight: 600;
- }
- }
- .timeline-connector {
- position: absolute;
- top: 14px;
- height: 2px;
- background: #e2e8f0;
- z-index: 0;
- &.done {
- background: #047857;
- }
- &.current {
- background: #047857;
- }
- &.refund {
- background: #dc2626;
- }
- &.refund-current {
- background: #d97706;
- }
- }
- /* ---- Preview ---- */
- .preview-container {
- text-align: center;
- }
- /* ---- Responsive ---- */
- @media (max-width: 1200px) {
- .kpi-strip {
- grid-template-columns: repeat(2, 1fr);
- }
- .kpi-strip .kpi-card {
- border-right: none !important;
- border-bottom: 1px solid rgba(168, 212, 196, 0.75);
- &:nth-last-child(-n + 2) {
- border-bottom: none;
- }
- }
- .client-sub-cards,
- .handle-summary-cards {
- grid-template-columns: 1fr;
- }
- .protocol-attach-remark-row.data-grid--2 {
- grid-template-columns: 1fr;
- }
- .payment-summary {
- grid-template-columns: repeat(2, 1fr);
- }
- }
- @media (max-width: 900px) {
- .kpi-strip {
- grid-template-columns: repeat(2, 1fr);
- }
- .kpi-strip .kpi-card:nth-last-child(-n + 2) {
- border-bottom: 1px solid rgba(168, 212, 196, 0.75);
- }
- .kpi-strip .kpi-card:last-child {
- border-bottom: none;
- }
- .client-sub-cards,
- .handle-summary-cards {
- grid-template-columns: 1fr;
- }
- .data-grid--4,
- .data-grid--6,
- .data-grid--7 {
- grid-template-columns: repeat(2, minmax(0, 1fr));
- }
- .payment-summary,
- .payment-summary--3 {
- grid-template-columns: repeat(2, 1fr);
- }
- .pdc-info-row {
- grid-template-columns: repeat(2, minmax(0, 1fr));
- }
- .refund-area-grid {
- grid-template-columns: repeat(2, minmax(0, 1fr));
- }
- .finance-area-grid {
- grid-template-columns: 1fr;
- }
- }
- </style>
|