PolicyMatch.vue 86 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920
  1. <template>
  2. <div v-if="showSummary" class="policy-match-summary" :class="{ 'record-mode': recordMode }">
  3. <div class="policy-header" v-if="panelData.length > 0">匹配完成!推荐您了解以下政策事项</div>
  4. <div class="policy-content-wrapper">
  5. <div class="policy-line"></div>
  6. <div class="policy-box">
  7. <div class="policy-item" v-for="(item, index) in panelData" :key="`${item.title}-${index}`">
  8. <div class="policy-item-header">
  9. <div class="policy-badge">
  10. <svg width="11" height="14" viewBox="0 0 11 14" fill="none" xmlns="http://www.w3.org/2000/svg">
  11. <path
  12. d="M5.33333 13.33333c-1.54444-0.38889-2.81944-1.275-3.825-2.65833-1.00556-1.38333-1.50833-2.91944-1.50833-4.60833l0-4.06667 5.33333-2 5.33334 2 0 4.06667c0 0.94444-0.16111 1.85278-0.48334 2.725-0.32222 0.87222-0.78333 1.64167-1.38333 2.30833l-2.13333-2.13333c-0.2 0.12222-0.41389 0.21389-0.64167 0.275-0.22778 0.06111-0.45833 0.09167-0.69167 0.09166-0.73333 0-1.36111-0.26111-1.88333-0.78333-0.52222-0.52222-0.78333-1.15-0.78333-1.88333 0-0.73333 0.26111-1.36111 0.78333-1.88334 0.52222-0.52222 1.15-0.78333 1.88333-0.78333 0.73333 0 1.36111 0.26111 1.88334 0.78333 0.52222 0.52222 0.78333 1.15 0.78333 1.88334 0 0.24444-0.03056 0.48056-0.09167 0.70833-0.06111 0.22778-0.15278 0.44722-0.275 0.65833l1 1c0.22222-0.45556 0.39444-0.93333 0.51667-1.43333 0.12222-0.5 0.18333-1.01111 0.18333-1.53333l0-3.15-4-1.5-4 1.5 0 3.15c0 1.34444 0.37778 2.56667 1.13334 3.66666 0.75556 1.1 1.71111 1.83333 2.86666 2.2 0.28889-0.08889 0.56389-0.20278 0.825-0.34166 0.26111-0.13889 0.51944-0.30278 0.775-0.49167l0.93334 0.93333c-0.36667 0.3-0.76389 0.56111-1.19167 0.78334-0.42778 0.22222-0.875 0.39444-1.34167 0.51666l0 0m0-5.33333c0.36667 0 0.68056-0.13056 0.94167-0.39167 0.26111-0.26111 0.39167-0.575 0.39167-0.94166 0-0.36667-0.13056-0.68056-0.39167-0.94167-0.26111-0.26111-0.575-0.39167-0.94167-0.39167-0.36667 0-0.68056 0.13056-0.94166 0.39167-0.26111 0.26111-0.39167 0.575-0.39167 0.94167 0 0.36667 0.13056 0.68056 0.39167 0.94166 0.26111 0.26111 0.575 0.39167 0.94166 0.39167l0 0"
  13. fill="currentColor" />
  14. </svg>
  15. <span>{{ item.enterprise_benefit || '企业收益' }}</span>
  16. </div>
  17. <!-- 来源标识:来自惠企政策库的卡片在右上角打标 -->
  18. <div class="policy-source-badge" v-if="item.is_policy_library">惠企</div>
  19. <!-- <div class="policy-match-degree" v-if="item.match_degree">
  20. <span class="label">匹配度</span>
  21. <span class="value">{{ item.match_degree }}</span>
  22. </div> -->
  23. </div>
  24. <div class="policy-title">{{ item.title }}</div>
  25. <div class="policy-desc" v-if="item.desc">{{ item.desc }}</div>
  26. <div class="policy-item-action" @click.stop="openDetailByItem(item)">查看详情</div>
  27. </div>
  28. </div>
  29. <div class="view-all-btn" @click.stop="openCardList">更多 >></div>
  30. </div>
  31. </div>
  32. <Teleport to="body">
  33. <div v-if="panelVisible" class="policy-viewer-modal"
  34. :class="[{ 'is-wide': isWide }, recordMode ? 'record-theme' : '']">
  35. <transition name="policy-panel">
  36. <div v-if="panelVisible" class="policy-viewer-content" :class="[
  37. isWide ? 'right-fixed' : '',
  38. panelMode === 'detail' ? 'detail-mode-bg' : 'list-mode-bg'
  39. ]">
  40. <div class="policy-viewer-header" :class="{ 'policy-viewer-header-record': recordMode }">
  41. <div class="header-title" v-if="!recordMode && !(isMobileViewer && panelMode === 'detail')">
  42. {{ panelMode === 'cardList' ? '政策列表' : '智能政策匹配' }}
  43. </div>
  44. <div class="header-actions">
  45. <button v-if="panelMode === 'detail' && !recordMode" class="header-back" @click.stop="openPolicyList">
  46. <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
  47. <path d="M15 18L9 12L15 6" stroke="white" stroke-width="2" stroke-linecap="round"
  48. stroke-linejoin="round" />
  49. </svg>
  50. </button>
  51. <button class="header-close" @click.stop="closePanel" :class="{ 'record-close-btn': recordMode }">
  52. <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
  53. <path d="M18 6L6 18M6 6L18 18" :stroke="recordMode ? 'currentColor' : 'white'" stroke-width="2"
  54. stroke-linecap="round" stroke-linejoin="round" />
  55. </svg>
  56. </button>
  57. </div>
  58. </div>
  59. <!-- list / cardList 共用这个 body(靠下面的 v-if 分叉内容),样式类完全复用 -->
  60. <div v-if="panelMode !== 'detail'" class="panel-scroll list-body" ref="listBodyRef"
  61. :class="{ 'list-body-record': recordMode }">
  62. <div class="list-content">
  63. <div class="record-list-header" v-if="recordMode">
  64. <div class="record-list-title">{{ panelMode === 'cardList' ? '政策列表' : '政策事项列表' }}</div>
  65. <div class="record-list-subtitle">
  66. {{ panelMode === 'cardList' ? '汇总本会话内出现的全部政策事项' : '浏览、搜索及筛选最新的惠企政策' }}
  67. </div>
  68. </div>
  69. <div class="search-filter-bar" v-if="isZhaoshangLLM && (recordMode || panelMode === 'cardList')">
  70. <div class="search-filter-top">
  71. <div class="search-input-wrapper">
  72. <svg class="search-icon" width="18" height="18" viewBox="0 0 18 18" fill="none"
  73. xmlns="http://www.w3.org/2000/svg">
  74. <path
  75. d="M12.5 11H11.71L11.43 10.73C12.41 9.59 13 8.11 13 6.5C13 2.91 10.09 0 6.5 0C2.91 0 0 2.91 0 6.5C0 10.09 2.91 13 6.5 13C8.11 13 9.59 12.41 10.73 11.43L11 11.71V12.5L16 17.49L17.49 16L12.5 11ZM6.5 11C4.01 11 2 8.99 2 6.5C2 4.01 4.01 2 6.5 2C8.99 2 11 4.01 11 6.5C11 8.99 8.99 11 6.5 11Z"
  76. fill="#777587" />
  77. </svg>
  78. <input type="text" placeholder="输入政策关键词" class="search-input" v-model="searchKeyword" />
  79. </div>
  80. <!-- 部门 / 分类筛选:只在旧列表面板出现 -->
  81. <div class="filter-selects" v-if="panelMode === 'list'" @click.stop>
  82. <div class="filter-select-wrapper">
  83. <div class="filter-option" @click="toggleFilterMenu" :class="{ 'is-open': showFilterMenu }">
  84. {{ currentFilterOption }}
  85. <svg width="21" height="21" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg">
  86. <path d="M6.3 8.4L10.5 12.6L14.7 8.4" stroke="#6b7280" stroke-width="1.5"
  87. stroke-linecap="round" stroke-linejoin="round" />
  88. </svg>
  89. </div>
  90. <transition name="popover-fade">
  91. <div class="filter-popover" v-if="showFilterMenu">
  92. <div class="filter-popover-item" v-for="opt in filterOptions" :key="opt"
  93. :class="{ active: currentFilterOption === opt }" @click="selectFilter(opt)">
  94. {{ opt }}
  95. </div>
  96. </div>
  97. </transition>
  98. </div>
  99. <div class="filter-select-wrapper">
  100. <button class="filter-btn" @click="toggleDeptMenu" :class="{ 'is-open': showDeptMenu }">
  101. <svg width="15" height="10" viewBox="0 0 15 10" fill="none" xmlns="http://www.w3.org/2000/svg">
  102. <path
  103. d="M5.83333 10V8.33333H9.16667V10H5.83333ZM2.5 5.83333V4.16667H12.5V5.83333H2.5ZM0 1.66667V0H15V1.66667H0Z"
  104. fill="white" />
  105. </svg>
  106. {{ currentDeptFilter === '全部部门' ? '部门筛选' : currentDeptFilter }}
  107. </button>
  108. <transition name="popover-fade">
  109. <div class="filter-popover dept-popover" v-if="showDeptMenu">
  110. <div class="filter-popover-item" v-for="dept in deptOptions" :key="dept"
  111. :class="{ active: currentDeptFilter === dept }" @click="selectDeptFilter(dept)">
  112. {{ dept }}
  113. </div>
  114. </div>
  115. </transition>
  116. </div>
  117. </div>
  118. </div>
  119. <div class="quick-chips">
  120. <span class="chip-label">热门搜索:</span>
  121. <div class="chip" v-for="keyword in quickSearchKeywords" :key="keyword"
  122. @click="handleQuickSearch(keyword)">
  123. {{ keyword }}
  124. </div>
  125. </div>
  126. </div>
  127. <div class="list-items-container">
  128. <div class="list-item-wrapper" v-for="(item, index) in paginatedPolicies"
  129. :key="`${item.title}-${index}`" @click.stop="openDetailByItem(item)">
  130. <div class="list-item" :class="{ 'record-list-item': recordMode }">
  131. <div class="list-item-content">
  132. <div class="list-item-tag" v-if="item.tag && !recordMode">
  133. <svg class="tag-icon" width="14" height="14" viewBox="0 0 14 14" fill="none"
  134. xmlns="http://www.w3.org/2000/svg">
  135. <path d="M11 5L5.5 10.5L3 8" stroke="white" stroke-width="1.5" stroke-linecap="round"
  136. stroke-linejoin="round" />
  137. <circle cx="7" cy="7" r="6" stroke="white" stroke-width="1.5" />
  138. </svg>
  139. {{ item.tag }}
  140. </div>
  141. <template v-if="recordMode">
  142. <div class="record-item-top">
  143. <div class="record-item-tag" v-if="item.tag">{{ item.tag }}</div>
  144. <!-- <div class="record-item-date">申报结束时间: {{ item.end_date || item.date }}</div> -->
  145. </div>
  146. <div class="list-item-title">{{ item.declaration_item || item.title }}</div>
  147. <div class="record-item-source">{{ item.title }}</div>
  148. </template>
  149. <template v-else>
  150. <div class="list-item-title">{{ item.declaration_item }}</div>
  151. <div class="list-item-source">{{ item.title }}</div>
  152. <div class="list-item-date">
  153. <svg class="date-icon" width="20" height="20" viewBox="0 0 20 20" fill="none"
  154. xmlns="http://www.w3.org/2000/svg">
  155. <path
  156. d="M16.6667 4.16667H3.33333C2.41286 4.16667 1.66667 4.91286 1.66667 5.83333V17.5C1.66667 18.4205 2.41286 19.1667 3.33333 19.1667H16.6667C17.5871 19.1667 18.3333 18.4205 18.3333 17.5V5.83333C18.3333 4.91286 17.5871 4.16667 16.6667 4.16667Z"
  157. stroke="#505050" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
  158. <path d="M13.3333 1.66667V6.66667" stroke="#505050" stroke-width="1.5"
  159. stroke-linecap="round" stroke-linejoin="round" />
  160. <path d="M6.66667 1.66667V6.66667" stroke="#505050" stroke-width="1.5"
  161. stroke-linecap="round" stroke-linejoin="round" />
  162. <path d="M1.66667 9.16667H18.3333" stroke="#505050" stroke-width="1.5"
  163. stroke-linecap="round" stroke-linejoin="round" />
  164. </svg>
  165. {{ item.date }}
  166. </div>
  167. </template>
  168. </div>
  169. <div class="record-item-actions" v-if="recordMode">
  170. <!-- <div class="record-match-box">
  171. <svg class="record-match-icon" viewBox="0 0 57 57" fill="none"
  172. xmlns="http://www.w3.org/2000/svg" style="transform: rotate(-90deg)">
  173. <circle cx="28.5" cy="28.5" r="25.5" stroke="#DCE2F7" stroke-width="5.33"
  174. stroke-linecap="round" stroke-linejoin="round" />
  175. <circle cx="28.5" cy="28.5" r="25.5" stroke="#3525CD" stroke-width="5.33"
  176. stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="160.22"
  177. :stroke-dashoffset="160.22 - 160.22 * getMatchProgress(item.match_degree)" />
  178. </svg>
  179. <div class="record-match-text">{{ formatMatchDegree(item.match_degree) }}</div>
  180. </div> -->
  181. <!-- <div class="record-match-meta">匹配度</div> -->
  182. <button class="record-detail-btn" @click.stop="openDetailByItem(item)">查看详情</button>
  183. </div>
  184. <div v-if="item.isRecommended && !recordMode" class="list-item-recommend">推荐</div>
  185. </div>
  186. </div>
  187. </div>
  188. <div class="pagination-container" v-if="totalPages > 1">
  189. <button class="page-btn prev-btn" :disabled="currentPage === 1" @click="changePage(currentPage - 1)">
  190. <svg width="8" height="12" viewBox="0 0 8 12" fill="none" xmlns="http://www.w3.org/2000/svg">
  191. <path d="M7.4 1.4L6 0L0 6L6 12L7.4 10.6L2.8 6Z" fill="currentColor" />
  192. </svg>
  193. </button>
  194. <template v-for="page in visiblePages" :key="page">
  195. <div v-if="page === '...'" class="page-ellipsis">...</div>
  196. <button v-else class="page-btn" :class="{ active: page === currentPage }"
  197. @click="changePage(page as number)">
  198. {{ page }}
  199. </button>
  200. </template>
  201. <button class="page-btn next-btn" :disabled="currentPage === totalPages"
  202. @click="changePage(currentPage + 1)">
  203. <svg width="8" height="12" viewBox="0 0 8 12" fill="none" xmlns="http://www.w3.org/2000/svg">
  204. <path d="M0 1.4L1.4 0L7.4 6L1.4 12L0 10.6L4.6 6Z" fill="currentColor" />
  205. </svg>
  206. </button>
  207. </div>
  208. </div>
  209. </div>
  210. <div v-else class="panel-scroll detail-body" :class="{ 'detail-body-record': recordMode }">
  211. <div class="record-header-section" v-if="recordMode">
  212. <!-- 只有惠企政策才显示「返回政策事项列表」——它通向惠企政策库 -->
  213. <div class="record-back-btn" v-if="selectedIsPolicyLibrary" @click.stop="openPolicyList">
  214. <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
  215. <path d="M10.5 12.5L6 8L10.5 3.5L9.5 2.5L4 8L9.5 13.5L10.5 12.5Z" fill="#3525CD" />
  216. </svg>
  217. <span>返回政策事项列表</span>
  218. </div>
  219. <div class="record-main-title">{{ selectedPolicyItem?.declaration_item ||
  220. '建立重点产品碳足迹背景数据库,构建碳足迹因子核算和发布工作机制' }}</div>
  221. <div class="record-meta-info">
  222. <div class="record-meta-item meta-name">
  223. <div class="meta-label">政策名称</div>
  224. <div class="meta-value">
  225. <a v-if="selectedPolicyItem?.apply_link" :href="selectedPolicyItem.apply_link" target="_blank" class="policy-link" rel="noopener noreferrer">{{ selectedPolicyItem?.title || '上海市青浦区加强知识产权保护、促进质量提升和标准体系建设的政策措施' }}</a>
  226. <template v-else>{{ selectedPolicyItem?.title || '上海市青浦区加强知识产权保护、促进质量提升和标准体系建设的政策措施' }}</template>
  227. </div>
  228. </div>
  229. <div class="record-meta-divider"></div>
  230. <div class="record-meta-item meta-no">
  231. <div class="meta-label">{{ recordMode ? '政策所属部门' : '政策文号' }}</div>
  232. <div class="meta-value">{{ selectedPolicyItem?.source || '青府发〔2024〕1号' }}</div>
  233. </div>
  234. </div>
  235. <div class="mobile-record-meta-info">
  236. <div class="mobile-record-meta-item meta-name">
  237. <div class="meta-value">
  238. <a v-if="selectedPolicyItem?.apply_link" :href="selectedPolicyItem.apply_link" target="_blank" class="policy-link" rel="noopener noreferrer">{{ selectedPolicyItem?.title || '上海市青浦区加强知识产权保护、促进质量提升和标准体系建设的政策措施' }}</a>
  239. <template v-else>{{ selectedPolicyItem?.title || '上海市青浦区加强知识产权保护、促进质量提升和标准体系建设的政策措施' }}</template>
  240. </div>
  241. </div>
  242. </div>
  243. </div>
  244. <div class="detail-header" v-if="!recordMode">
  245. <div class="detail-title">
  246. <a v-if="selectedPolicyItem?.apply_link" :href="selectedPolicyItem.apply_link" target="_blank" class="policy-link" rel="noopener noreferrer">{{ selectedPolicyItem?.title }}</a>
  247. <template v-else>{{ selectedPolicyItem?.title }}</template>
  248. </div>
  249. <div class="detail-subtitle">{{ selectedPolicyItem?.declaration_item }}</div>
  250. </div>
  251. <div class="divider" v-if="!recordMode"></div>
  252. <div class="detail-section-title" v-if="!recordMode">政策解析</div>
  253. <div :class="recordMode ? 'detail-body-grid' : ''">
  254. <div class="detail-row card-style">
  255. <div class="record-condition-title">
  256. <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
  257. <path
  258. d="M0 18V16.425C0 15.7083 0.366667 15.125 1.1 14.675C1.83333 14.225 2.8 14 4 14C4.21667 14 4.425 14.0042 4.625 14.0125C4.825 14.0208 5.01667 14.0417 5.2 14.075C4.96667 14.425 4.79167 14.7917 4.675 15.175C4.55833 15.5583 4.5 15.9583 4.5 16.375V18H0ZM6 18V16.375C6 15.8417 6.14583 15.3542 6.4375 14.9125C6.72917 14.4708 7.14167 14.0833 7.675 13.75C8.20833 13.4167 8.84583 13.1667 9.5875 13C10.3292 12.8333 11.1333 12.75 12 12.75C12.8833 12.75 13.6958 12.8333 14.4375 13C15.1792 13.1667 15.8167 13.4167 16.35 13.75C16.8833 14.0833 17.2917 14.4708 17.575 14.9125C17.8583 15.3542 18 15.8417 18 16.375V18H6ZM19.5 18V16.375C19.5 15.9417 19.4458 15.5333 19.3375 15.15C19.2292 14.7667 19.0667 14.4083 18.85 14.075C19.0333 14.0417 19.2208 14.0208 19.4125 14.0125C19.6042 14.0042 19.8 14 20 14C21.2 14 22.1667 14.2208 22.9 14.6625C23.6333 15.1042 24 15.6917 24 16.425V18H19.5ZM8.125 16H15.9C15.7333 15.6667 15.2708 15.375 14.5125 15.125C13.7542 14.875 12.9167 14.75 12 14.75C11.0833 14.75 10.2458 14.875 9.4875 15.125C8.72917 15.375 8.275 15.6667 8.125 16ZM4 13C3.45 13 2.97917 12.8042 2.5875 12.4125C2.19583 12.0208 2 11.55 2 11C2 10.4333 2.19583 9.95833 2.5875 9.575C2.97917 9.19167 3.45 9 4 9C4.56667 9 5.04167 9.19167 5.425 9.575C5.80833 9.95833 6 10.4333 6 11C6 11.55 5.80833 12.0208 5.425 12.4125C5.04167 12.8042 4.56667 13 4 13ZM20 13C19.45 13 18.9792 12.8042 18.5875 12.4125C18.1958 12.0208 18 11.55 18 11C18 10.4333 18.1958 9.95833 18.5875 9.575C18.9792 9.19167 19.45 9 20 9C20.5667 9 21.0417 9.19167 21.425 9.575C21.8083 9.95833 22 10.4333 22 11C22 11.55 21.8083 12.0208 21.425 12.4125C21.0417 12.8042 20.5667 13 20 13ZM12 12C11.1667 12 10.4583 11.7083 9.875 11.125C9.29167 10.5417 9 9.83333 9 9C9 8.15 9.29167 7.4375 9.875 6.8625C10.4583 6.2875 11.1667 6 12 6C12.85 6 13.5625 6.2875 14.1375 6.8625C14.7125 7.4375 15 8.15 15 9C15 9.83333 14.7125 10.5417 14.1375 11.125C13.5625 11.7083 12.85 12 12 12ZM12 10C12.2833 10 12.5208 9.90417 12.7125 9.7125C12.9042 9.52083 13 9.28333 13 9C13 8.71667 12.9042 8.47917 12.7125 8.2875C12.5208 8.09583 12.2833 8 12 8C11.7167 8 11.4792 8.09583 11.2875 8.2875C11.0958 8.47917 11 8.71667 11 9C11 9.28333 11.0958 9.52083 11.2875 9.7125C11.4792 9.90417 11.7167 10 12 10Z"
  259. fill="#3525CD" />
  260. </svg>
  261. <span>支持对象</span>
  262. </div>
  263. <div class="card-content"
  264. style="font-size: 14px; line-height: 1.6; color: #464555; gap: 11px; display: flex; flex-direction: column;">
  265. <div class="detail-item">
  266. {{ selectedPolicyItem?.support_objects }}
  267. </div>
  268. </div>
  269. </div>
  270. <div class="detail-row card-style">
  271. <div class="record-condition-title">
  272. <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
  273. <path
  274. d="M14 13.1992C13.1667 13.1992 12.4583 12.9076 11.875 12.3242C11.2917 11.7409 11 11.0326 11 10.1992C11 9.36589 11.2917 8.65755 11.875 8.07422C12.4583 7.49089 13.1667 7.19922 14 7.19922C14.8333 7.19922 15.5417 7.49089 16.125 8.07422C16.7083 8.65755 17 9.36589 17 10.1992C17 11.0326 16.7083 11.7409 16.125 12.3242C15.5417 12.9076 14.8333 13.1992 14 13.1992ZM7 16.1992C6.45 16.1992 5.97917 16.0034 5.5875 15.6117C5.19583 15.2201 5 14.7492 5 14.1992V6.19922C5 5.64922 5.19583 5.17839 5.5875 4.78672C5.97917 4.39505 6.45 4.19922 7 4.19922H21C21.55 4.19922 22.0208 4.39505 22.4125 4.78672C22.8042 5.17839 23 5.64922 23 6.19922V14.1992C23 14.7492 22.8042 15.2201 22.4125 15.6117C22.0208 16.0034 21.55 16.1992 21 16.1992H7ZM9 14.1992H19C19 13.6492 19.1958 13.1784 19.5875 12.7867C19.9792 12.3951 20.45 12.1992 21 12.1992V8.19922C20.45 8.19922 19.9792 8.00339 19.5875 7.61172C19.1958 7.22005 19 6.74922 19 6.19922H9C9 6.74922 8.80417 7.22005 8.4125 7.61172C8.02083 8.00339 7.55 8.19922 7 8.19922V12.1992C7.55 12.1992 8.02083 12.3951 8.4125 12.7867C8.80417 13.1784 9 13.6492 9 14.1992ZM20 20.1992H3C2.45 20.1992 1.97917 20.0034 1.5875 19.6117C1.19583 19.2201 1 18.7492 1 18.1992V7.19922H3V18.1992H20V20.1992ZM7 14.1992V6.19922V14.1992Z"
  275. fill="#3525CD" />
  276. </svg>
  277. <span>支持方式和标准</span>
  278. </div>
  279. <div class="card-content" style="font-size: 14px; line-height: 1.6; color: #464555;">
  280. <template v-if="Array.isArray(selectedPolicyItem?.support_measures)">
  281. <div v-for="(measure, index) in selectedPolicyItem?.support_measures" :key="index"
  282. style="margin-bottom: 8px;">
  283. {{ measure }}
  284. </div>
  285. </template>
  286. <template
  287. v-else-if="typeof selectedPolicyItem?.support_measures === 'string' && selectedPolicyItem?.support_measures.startsWith('[') && selectedPolicyItem?.support_measures.endsWith(']')">
  288. <div v-for="(measure, index) in parseStringArray(selectedPolicyItem?.support_measures)" :key="index"
  289. style="margin-bottom: 8px;">
  290. {{ measure }}
  291. </div>
  292. </template>
  293. <template v-else>
  294. {{ selectedPolicyItem?.support_measures }}
  295. </template>
  296. </div>
  297. </div>
  298. </div>
  299. <div class="detail-row" :style="recordMode ? 'margin-top: 23.69px;' : ''"
  300. :class="{ 'condition-section': recordMode }">
  301. <div class="row-label" v-if="!recordMode">
  302. <div class="dot-icon">
  303. <div class="dot-inner"></div>
  304. </div>
  305. <span class="label-text">申报条件</span>
  306. </div>
  307. <div class="row-value" v-if="!recordMode">{{ selectedPolicyItem?.apply_conditions }}</div>
  308. <template v-if="recordMode">
  309. <div class="record-condition-title" style="justify-content: space-between;">
  310. <div style="display: flex; align-items: center; gap: 8px;">
  311. <svg width="18" height="20" viewBox="0 0 18 20" fill="none" xmlns="http://www.w3.org/2000/svg">
  312. <path fill="currentColor"
  313. d="M7.6 15.05l7.05-7.05-1.4-1.4-5.65 5.65-2.85-2.85-1.4 1.4 4.25 4.25 0 0m-5.6 4.95c-0.55 0-1.02083-0.19583-1.4125-0.5875-0.39167-0.39167-0.5875-0.8625-0.5875-1.4125l0-14c0-0.55 0.19583-1.02083 0.5875-1.4125 0.39167-0.39167 0.8625-0.5875 1.4125-0.5875l4.2 0c0.21667-0.6 0.57917-1.08333 1.0875-1.45 0.50833-0.36667 1.07917-0.55 1.7125-0.55 0.63333 0 1.20417 0.18333 1.7125 0.55 0.50833 0.36667 0.87083 0.85 1.0875 1.45l4.2 0c0.55 0 1.02083 0.19583 1.4125 0.5875 0.39167 0.39167 0.5875 0.8625 0.5875 1.4125l0 14c0 0.55-0.19583 1.02083-0.5875 1.4125-0.39167 0.39167-0.8625 0.5875-1.4125 0.5875l-14 0 0 0m0-2l14 0 0 0 0 0 0-14 0 0 0 0-14 0 0 0 0 0 0 14 0 0 0 0 0 0m7-14.75c0.21667 0 0.39583-0.07083 0.5375-0.2125 0.14167-0.14167 0.2125-0.32083 0.2125-0.5375 0-0.21667-0.07083-0.39583-0.2125-0.5375-0.14167-0.14167-0.32083-0.2125-0.5375-0.2125-0.21667 0-0.39583 0.07083-0.5375 0.2125-0.14167 0.14167-0.2125 0.32083-0.2125 0.5375 0 0.21667 0.07083 0.39583 0.2125 0.5375 0.14167 0.14167 0.32083 0.2125 0.5375 0.2125l0 0m-7 14.75l0 0 0 0 0-14 0 0 0 0 0 0 0 0 0 0 0 14 0 0 0 0 0 0 0 0" />
  314. </svg>
  315. <span>申报条件匹配分析</span>
  316. </div>
  317. </div>
  318. <div class="condition-content-wrapper">
  319. <div class="condition-text-box">
  320. 申报条件:<br />{{ selectedPolicyItem?.apply_conditions }}
  321. </div>
  322. <div v-if="selectedPolicyItem?.match_degree" class="condition-match-card"
  323. :class="getMatchLevelClass(selectedPolicyItem?.match_degree)">
  324. <div class="match-card-header">
  325. <div class="match-card-title">
  326. <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
  327. <path fill="currentColor"
  328. d="M8.6 14.6l7.05-7.05-1.4-1.4-5.65 5.65-2.85-2.85-1.4 1.4 4.25 4.25 0 0m1.4 5.4c-1.38333 0-2.68333-0.2625-3.9-0.7875-1.21667-0.525-2.275-1.2375-3.175-2.1375-0.9-0.9-1.6125-1.95833-2.1375-3.175-0.525-1.21667-0.7875-2.51667-0.7875-3.9 0-1.38333 0.2625-2.68333 0.7875-3.9 0.525-1.21667 1.2375-2.275 2.1375-3.175 0.9-0.9 1.95833-1.6125 3.175-2.1375 1.21667-0.525 2.51667-0.7875 3.9-0.7875 1.38333 0 2.68333 0.2625 3.9 0.7875 1.21667 0.525 2.275 1.2375 3.175 2.1375 0.9 0.9 1.6125 1.95833 2.1375 3.175 0.525 1.21667 0.7875 2.51667 0.7875 3.9 0 1.38333-0.2625 2.68333-0.7875 3.9-0.525 1.21667-1.2375 2.275-2.1375 3.175-0.9 0.9-1.95833 1.6125-3.175 2.1375-1.21667 0.525-2.51667 0.7875-3.9 0.7875l0 0m0-2c2.23333 0 4.125-0.775 5.675-2.325 1.55-1.55 2.325-3.44167 2.325-5.675 0-2.23333-0.775-4.125-2.325-5.675-1.55-1.55-3.44167-2.325-5.675-2.325-2.23333 0-4.125 0.775-5.675 2.325-1.55 1.55-2.325 3.44167-2.325 5.675 0 2.23333 0.775 4.125 2.325 5.675 1.55 1.55 3.44167 2.325 5.675 2.325l0 0m0-8l0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0" />
  329. </svg>
  330. <span>匹配原因</span>
  331. </div>
  332. <!-- <div class="match-badge">
  333. {{ formatMatchDegree(selectedPolicyItem?.match_degree) }}
  334. </div> -->
  335. </div>
  336. <div class="match-card-body">
  337. {{ selectedPolicyItem?.match_reason }}
  338. </div>
  339. </div>
  340. </div>
  341. </template>
  342. </div>
  343. <!-- 条件核验(来自 /api/chat 的 item.conditions) -->
  344. <div v-if="recordMode && selectedConditions.length" class="detail-row card-style">
  345. <div class="record-condition-title">
  346. <span>条件核验</span>
  347. </div>
  348. <div class="condition-content-wrapper">
  349. <div v-for="(condition, index) in selectedConditions" :key="`condition-${index}`"
  350. class="condition-match-card" :class="getConditionLevelClass(condition?.status)">
  351. <div class="match-card-header">
  352. <div class="match-card-title">
  353. <span>{{ condition?.dimension || '其他条件' }} · {{ condition?.status || '待确认' }}</span>
  354. </div>
  355. </div>
  356. <div class="match-card-body">
  357. <div v-if="condition?.requirement">要求:{{ condition.requirement }}</div>
  358. <div v-if="condition?.quote">原文:{{ condition.quote }}</div>
  359. <div v-if="condition?.user_evidence">已知信息:{{ condition.user_evidence }}</div>
  360. <div v-if="condition?.question">待确认:{{ condition.question }}</div>
  361. </div>
  362. </div>
  363. </div>
  364. </div>
  365. <!-- 匹配依据(来自 /api/chat 的 item.match_basis) -->
  366. <div v-if="recordMode && selectedMatchBasis.length" class="detail-row card-style">
  367. <div class="record-condition-title">
  368. <span>匹配依据</span>
  369. </div>
  370. <div class="condition-content-wrapper">
  371. <div v-for="(basis, index) in selectedMatchBasis" :key="`basis-${index}`"
  372. class="condition-match-card" :class="getBasisLevelClass(basis?.status)">
  373. <div class="match-card-header">
  374. <div class="match-card-title">
  375. <span>{{ basis?.dimension || '其他' }} · {{ formatBasisStatus(basis?.status) }}</span>
  376. </div>
  377. </div>
  378. <div class="match-card-body">
  379. <div v-if="basis?.explanation">{{ basis.explanation }}</div>
  380. <div v-if="basis?.fact_quote">主体:{{ basis.fact_quote }}</div>
  381. <div v-if="basis?.policy_quote">政策:{{ basis.policy_quote }}</div>
  382. </div>
  383. </div>
  384. </div>
  385. </div>
  386. <div class="detail-row" v-if="!recordMode">
  387. <div class="row-label">
  388. <div class="dot-icon" v-if="!recordMode">
  389. <div class="dot-inner"></div>
  390. </div>
  391. <span class="label-text">申报材料</span>
  392. </div>
  393. <div class="row-value">{{ selectedPolicyItem?.apply_materials }}</div>
  394. </div>
  395. <div class="detail-row" v-if="!recordMode">
  396. <div class="row-label">
  397. <div class="dot-icon" v-if="!recordMode">
  398. <div class="dot-inner"></div>
  399. </div>
  400. <span class="label-text">附件</span>
  401. </div>
  402. <div class="row-value">
  403. <div class="attachment-item" v-for="(attachment, index) in selectedPolicyItem?.attachments"
  404. :key="index">
  405. <a :href="attachment.url" target="_blank">{{ attachment.name }}</a>
  406. </div>
  407. </div>
  408. </div>
  409. <div class="divider" v-if="!recordMode"></div>
  410. <div class="detail-footer" v-if="!recordMode">
  411. <div class="footer-title">匹配原因</div>
  412. <div class="footer-desc">{{ selectedPolicyItem?.match_reason }}</div>
  413. </div>
  414. </div>
  415. </div>
  416. </transition>
  417. </div>
  418. </Teleport>
  419. </template>
  420. <script setup lang="ts">
  421. import { computed, inject, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue";
  422. import { parsePolicyTableContent } from "@/utils/stream-xml-filter";
  423. import { isZhaoshangLLM as isZhaoshangLLMModel } from "@/utils";
  424. import {
  425. buildLibraryNameSet,
  426. buildPolicyDetailUrl,
  427. collectMatchedLibraryNames,
  428. isPolicyLibraryItem,
  429. pinMatchedFirst,
  430. } from "./policy-library-utils";
  431. import {
  432. resolvePolicyDetailIndex,
  433. buildPolicyCardDedupKey,
  434. dedupePolicyItems,
  435. matchesPolicyCardKeyword,
  436. sortPolicyCardsByMatchScore,
  437. type PolicyListFilterOption,
  438. } from "./policy-match-utils";
  439. import { MODAL_MUTEX_KEY } from "../business-assistant/modal-mutex";
  440. type RawPolicyItem = Record<string, any>;
  441. type PolicyItem = {
  442. title: string;
  443. desc: string;
  444. declaration_item: string;
  445. support_objects: string;
  446. support_measures: string;
  447. apply_conditions: string;
  448. apply_materials: string;
  449. attachments: string;
  450. match_reason: string;
  451. tag: string;
  452. source: string;
  453. match_score: number;
  454. match_degree: string;
  455. enterprise_benefit: string;
  456. isRecommended: boolean;
  457. date: string;
  458. end_date: string;
  459. apply_link?: string;
  460. originalData: RawPolicyItem;
  461. };
  462. const props = withDefaults(
  463. defineProps<{
  464. visible?: boolean;
  465. policyData?: RawPolicyItem[] | null;
  466. policyContent?: string;
  467. recordMode?: boolean;
  468. sessionId?: string;
  469. }>(),
  470. {
  471. visible: true,
  472. policyData: null,
  473. policyContent: "",
  474. recordMode: false,
  475. sessionId: "",
  476. }
  477. );
  478. const emit = defineEmits<{
  479. (e: "update:visible", value: boolean): void;
  480. }>();
  481. const policies = ref<PolicyItem[]>([]);
  482. const policyListItems = ref<PolicyItem[]>([]);
  483. const policyListTotal = ref(0);
  484. const panelVisible = ref(false);
  485. const policyModalId = Symbol("policy-viewer");
  486. const modalMutex = inject(MODAL_MUTEX_KEY, null);
  487. let unregisterPolicyModal: (() => void) | undefined;
  488. // list = 旧列表(惠企政策库 + 部门/分类筛选);cardList = 卡片区「更多」侧边栏;detail = 单条详情
  489. const panelMode = ref<"list" | "cardList" | "detail">("list");
  490. const selectedPolicy = ref<number>(0);
  491. const selectedPolicyOverride = ref<PolicyItem | null>(null);
  492. const isWide = ref(false);
  493. const isMobileViewer = ref(false);
  494. const isZhaoshangLLM = computed(() => isZhaoshangLLMModel(globalThis.llmModel));
  495. const widthLevel = 1200;
  496. const panelWidth = 700;
  497. const itemsPerPage = 20;
  498. const currentPage = ref(1);
  499. const listBodyRef = ref<HTMLElement | null>(null);
  500. const filterOptions: PolicyListFilterOption[] = ["所有分类", "只看免申即享", "非免申即享"];
  501. const currentFilterOption = ref<PolicyListFilterOption>("所有分类");
  502. const showFilterMenu = ref(false);
  503. /**
  504. * 部门筛选项:只列数据里实际存在的部门。
  505. *
  506. * ⚠️ 用 ref + 手动刷新,**不用 computed**:`globalThis.policiesPublic` 由
  507. * `ensurePolicyLibrary` 异步加载、不是响应式依赖(见 getLibraryPolicies),
  508. * computed 会把首次读到的空列表缓存住。
  509. * 所以在打开菜单时现算一次(那时数据早已加载完)。
  510. */
  511. const deptOptions = ref<string[]>(["全部部门"]);
  512. const collectDeptOptions = (): string[] => {
  513. /**
  514. * 只取**政策库**里 `政策所属部门` 的取值:卡片的 `source` 可能是**多部门串**
  515. * (含顿号/逗号/换行),并进来会变成又长又筛不出东西的假部门,一并挡掉。
  516. */
  517. const looksLikeSingleDept = (value: string) => value.length <= 12 && !/[,,、\s]/.test(value);
  518. const unique = [
  519. ...new Set(
  520. getLibraryPolicies()
  521. .map((item) => String(item?.["政策所属部门"] ?? "").trim())
  522. .filter((value) => value && looksLikeSingleDept(value))
  523. ),
  524. ].sort((a, b) => a.localeCompare(b, "zh-CN"));
  525. return ["全部部门", ...unique];
  526. };
  527. const refreshDeptOptions = () => {
  528. const next = collectDeptOptions();
  529. deptOptions.value = next;
  530. // 选中的部门若已不在数据里(数据换了),退回"全部部门"
  531. if (!next.includes(currentDeptFilter.value)) {
  532. currentDeptFilter.value = "全部部门";
  533. }
  534. };
  535. const currentDeptFilter = ref("全部部门");
  536. const showDeptMenu = ref(false);
  537. const searchKeyword = ref("");
  538. const quickSearchKeywords = ["金融", "发展扶持", "专精特新", "高新技术", "先进制造"];
  539. const handleQuickSearch = (keyword: string) => {
  540. searchKeyword.value = keyword;
  541. };
  542. const toggleFilterMenu = () => {
  543. showFilterMenu.value = !showFilterMenu.value;
  544. showDeptMenu.value = false;
  545. };
  546. const toggleDeptMenu = () => {
  547. const willOpen = !showDeptMenu.value;
  548. showDeptMenu.value = willOpen;
  549. showFilterMenu.value = false;
  550. // 打开时才现算部门清单(数据是异步加载的、且没有响应式依赖)
  551. if (willOpen) refreshDeptOptions();
  552. };
  553. const selectFilter = (opt: PolicyListFilterOption) => {
  554. currentFilterOption.value = opt;
  555. showFilterMenu.value = false;
  556. };
  557. const selectDeptFilter = (dept: string) => {
  558. currentDeptFilter.value = dept;
  559. showDeptMenu.value = false;
  560. };
  561. // 点击外部关闭下拉菜单
  562. const closeMenus = () => {
  563. if (showFilterMenu.value) showFilterMenu.value = false;
  564. if (showDeptMenu.value) showDeptMenu.value = false;
  565. };
  566. onMounted(() => {
  567. document.addEventListener("click", closeMenus);
  568. });
  569. onBeforeUnmount(() => {
  570. document.removeEventListener("click", closeMenus);
  571. });
  572. // 卡片区「更多」→ 政策列表侧边栏:只列本条回答里出现的政策,数据源同 syncPolicies
  573. const cardListItems = ref<PolicyItem[]>([]);
  574. /** 重建列表:取本条回答的卡片 → 与政策库合并补全 → 规范化 → 去重(首见优先)→ 按匹配分排序 */
  575. const rebuildCardList = () => {
  576. const raw = props.policyData?.length
  577. ? props.policyData
  578. : parsePolicyTableContent(props.policyContent);
  579. const normalized = (raw || []).map(mergeWithPublicPolicies).map(normalizePolicy);
  580. cardListItems.value = sortPolicyCardsByMatchScore(
  581. dedupePolicyItems(normalized, buildPolicyCardDedupKey)
  582. );
  583. };
  584. const cardListFiltered = computed(() =>
  585. cardListItems.value.filter((item) => matchesPolicyCardKeyword(item, searchKeyword.value))
  586. );
  587. const cardListTotal = computed(() => cardListFiltered.value.length);
  588. const cardListPaginated = computed(() => {
  589. const start = (currentPage.value - 1) * itemsPerPage;
  590. return cardListFiltered.value.slice(start, start + itemsPerPage);
  591. });
  592. const filteredPolicies = computed(() => {
  593. if (props.recordMode) {
  594. return listPolicyItems.value;
  595. }
  596. return listPolicyItems.value.filter(p => {
  597. if (searchKeyword.value) {
  598. const keyword = searchKeyword.value.toLowerCase();
  599. const matchTitle = p.title && p.title.toLowerCase().includes(keyword);
  600. const matchDesc = p.desc && p.desc.toLowerCase().includes(keyword);
  601. const matchItem = p.declaration_item && p.declaration_item.toLowerCase().includes(keyword);
  602. if (!matchTitle && !matchDesc && !matchItem) {
  603. return false;
  604. }
  605. }
  606. if (currentFilterOption.value === "只看免申即享" && p.tag !== "免申即享") {
  607. return false;
  608. } else if (currentFilterOption.value === "非免申即享" && p.tag === "免申即享") {
  609. return false;
  610. }
  611. if (currentDeptFilter.value !== "全部部门" && p.source !== currentDeptFilter.value) {
  612. return false;
  613. }
  614. return true;
  615. });
  616. });
  617. watch([currentFilterOption, currentDeptFilter, searchKeyword], () => {
  618. if (currentPage.value === 1) {
  619. fetchPolicyList();
  620. return;
  621. }
  622. currentPage.value = 1;
  623. });
  624. watch(currentPage, () => {
  625. fetchPolicyList();
  626. });
  627. const totalPages = computed(() => {
  628. if (panelMode.value === "cardList") {
  629. return Math.ceil(cardListTotal.value / itemsPerPage);
  630. }
  631. const total = props.recordMode ? policyListTotal.value : filteredPolicies.value.length;
  632. return Math.ceil(total / itemsPerPage);
  633. });
  634. const paginatedPolicies = computed(() => {
  635. // 两种列表模式互斥(由 panelMode 单一开关),共用 currentPage/itemsPerPage 安全
  636. if (panelMode.value === "cardList") {
  637. return cardListPaginated.value;
  638. }
  639. if (props.recordMode) {
  640. return filteredPolicies.value;
  641. }
  642. const start = (currentPage.value - 1) * itemsPerPage;
  643. return filteredPolicies.value.slice(start, start + itemsPerPage);
  644. });
  645. const changePage = (page: number) => {
  646. if (page < 1 || page > totalPages.value) return;
  647. currentPage.value = page;
  648. // 切换分页时滚动回顶部
  649. if (listBodyRef.value) {
  650. listBodyRef.value.scrollTop = 0;
  651. }
  652. };
  653. const visiblePages = computed(() => {
  654. const total = totalPages.value;
  655. const current = currentPage.value;
  656. const delta = 2;
  657. const range = [];
  658. const rangeWithDots = [];
  659. let l;
  660. range.push(1);
  661. for (let i = current - delta; i <= current + delta; i++) {
  662. if (i < total && i > 1) {
  663. range.push(i);
  664. }
  665. }
  666. if (total > 1) {
  667. range.push(total);
  668. }
  669. for (let i of range) {
  670. if (l) {
  671. if (i - l === 2) {
  672. rangeWithDots.push(l + 1);
  673. } else if (i - l !== 1) {
  674. rangeWithDots.push('...');
  675. }
  676. }
  677. rangeWithDots.push(i);
  678. l = i;
  679. }
  680. return rangeWithDots;
  681. });
  682. /**
  683. * 是否为精简数据格式(仅含 match_degree / match_reason / match_score / id)
  684. * —— 来自新版 chat 接口的 POLICY_TABLE 块
  685. */
  686. const isMinimalFormat = (item: RawPolicyItem): boolean => {
  687. return (
  688. !item.name &&
  689. !item.title &&
  690. !item.declaration_item &&
  691. (item.match_degree || item.match_reason)
  692. );
  693. };
  694. const parsePolicyDetails = (item: RawPolicyItem) => {
  695. const details = item?.['详情'];
  696. if (!details) return {};
  697. if (typeof details === 'object') return details;
  698. try {
  699. return JSON.parse(details);
  700. } catch (error) {
  701. return {};
  702. }
  703. };
  704. const stripPolicyIdText = (value: unknown) => {
  705. if (typeof value !== 'string') {
  706. return value;
  707. }
  708. return value
  709. .replace(/政策ID\s*:\s*[0-9a-fA-F-]+/gi, '')
  710. .replace(/\s{2,}/g, ' ')
  711. .trim();
  712. };
  713. const normalizePolicy = (item: RawPolicyItem): PolicyItem => {
  714. const minimal = isMinimalFormat(item);
  715. const suggestedConditions = item.data?.["梳理后建议申报条件"] || item?.['申报条件'] || item.data?.["申报条件"] || [];
  716. const policyDetails = parsePolicyDetails(item);
  717. // 处理申报条件:兼容 JSON 字符串、数组、普通字符串三种格式
  718. let processedConditions = suggestedConditions || [];
  719. if (typeof suggestedConditions === 'string') {
  720. try {
  721. const parsed = JSON.parse(suggestedConditions.replace(/'/g, '"'));
  722. if (Array.isArray(parsed)) {
  723. processedConditions = parsed;
  724. } else {
  725. processedConditions = suggestedConditions;
  726. }
  727. } catch (e) {
  728. processedConditions = suggestedConditions;
  729. }
  730. }
  731. processedConditions = Array.isArray(processedConditions) ? processedConditions.join(";\n") : processedConditions;
  732. return {
  733. title: stripPolicyIdText(
  734. item.name ||
  735. item.title || item?.['政策名称'] ||
  736. (minimal
  737. ? `${formatMatchDegree(item.match_degree)} · ${(item.id || "").slice(0, 8)}`
  738. : "暂无政策名称")
  739. ),
  740. desc: stripPolicyIdText(item.desc || (minimal ? item.match_reason || "" : "")),
  741. declaration_item: stripPolicyIdText(
  742. item.declaration_item ||
  743. item.originalData?.declaration_item || item?.['申报事项'] ||
  744. // v2 数据的字段名(一个政策下多个申报事项,这两列实测 170 条全一致)
  745. item?.['部门上报事项'] || item?.['大模型拆解申报事项'] ||
  746. (minimal ? "查看政策详情" : "暂无申报项")
  747. ),
  748. support_objects: stripPolicyIdText(
  749. item.data?.["支持对象"] || item?.['支持对象'] || item.support_objects || (minimal ? "" : "暂无数据")
  750. ),
  751. support_measures: stripPolicyIdText(
  752. item.data?.["支持方式和标准"] || item?.['支持方式和标准'] || item.support_measures || (minimal ? "" : "暂无数据")
  753. ),
  754. apply_conditions: stripPolicyIdText(processedConditions) ||
  755. item.apply_conditions ||
  756. (minimal ? "" : "暂无数据"),
  757. match_reason: stripPolicyIdText(item.match_reason || "暂无匹配理由"),
  758. tag:
  759. (item.auto_granted || !!item?.['是否免申即享'])
  760. ? "免申即享"
  761. : item.tag || (minimal ? item.match_degree || "" : ""),
  762. source: stripPolicyIdText(
  763. item.department || item?.['部门'] || item.source ||
  764. // v2 数据的部门字段名(取值形如「区经委」,与筛选列表口径一致)
  765. item?.['政策所属部门'] || ""
  766. ),
  767. match_score: Number(item.match_score || 0),
  768. match_degree: item.match_degree || "",
  769. enterprise_benefit: stripPolicyIdText(
  770. item.enterprise_benefit ||
  771. item?.['企业收益'] ||
  772. item.metadata?.['企业收益'] ||
  773. policyDetails?.['企业收益'] ||
  774. ""
  775. ),
  776. apply_materials: stripPolicyIdText(
  777. item.data?.["申报材料"] || item?.['申报材料'] || item.apply_materials || (minimal ? "" : "暂无数据")
  778. ),
  779. attachments: stripPolicyIdText(item.data?.["相关附件"] || item?.['相关附件'] || item.attachments || ""),
  780. isRecommended: ["高", "中"].includes(item.match_degree),
  781. date:
  782. item.date || item?.declaration_end_time || item?.['结束时间'] ||
  783. (item.declaration_start_time && item.declaration_end_time)
  784. ? `${item.declaration_start_time || item?.['开始时间'] || ""} 至 ${item.declaration_end_time || item?.['结束时间'] || ""}`
  785. : "",
  786. // v2 数据的日期字段是「开始时间 / 结束时间」(结束时间可能是"未标明"这类文本)
  787. end_date: item.declaration_end_time || item.date || item?.['申报结束时间'] || item?.['结束时间'] || "",
  788. apply_link:
  789. item.apply_link ||
  790. item?.['政策原文地址'] ||
  791. item.data?.['政策原文地址'] ||
  792. // 库里 apply_link 全空,用「市级政策id」拼政策详情页地址
  793. buildPolicyDetailUrl(item) ||
  794. "",
  795. originalData: item,
  796. };
  797. };
  798. /**
  799. * chat 增量政策数据与预下载的完整列表(globalThis.policiesPublic)合并:
  800. * 以完整列表中的条目为基础,chat 返回的字段(匹配度等)覆盖其上。
  801. */
  802. const mergeWithPublicPolicies = (item: RawPolicyItem): RawPolicyItem => {
  803. const fullList = globalThis.policiesPublic;
  804. if (!fullList || !Array.isArray(fullList)) return item;
  805. const id = item.id || item.policy_id;
  806. if (!id) return item;
  807. const fullEntry = fullList.find(
  808. (entry: any) => entry.id === id || entry.policy_id === id
  809. );
  810. if (!fullEntry) return item;
  811. return { ...fullEntry, ...item };
  812. };
  813. const listPolicyItems = computed(() => {
  814. return props.recordMode ? policyListItems.value : policies.value;
  815. });
  816. /* 惠企政策库:policies_public.v2.json 里的政策(由 index.html 预加载到 globalThis.policiesPublic)。
  817. * 判定优先用接口下发的 is_policy_library 字段,缺失时回退到"标题能在库里找到"。 */
  818. /**
  819. * ⚠️ 这里是**函数**不是 computed,是刻意的。
  820. *
  821. * `globalThis.policiesPublic` 由 `ensurePolicyLibrary` 异步加载(失败时退到本地兜底),
  822. * 且应用**不等待**它——不是响应式依赖。写成 computed 会把首次读到的值
  823. * (很可能是空数组)缓存住,加载完成后也不会更新。
  824. * 所以每次调用时现读。
  825. */
  826. const getLibraryPolicies = (): RawPolicyItem[] =>
  827. Array.isArray(globalThis.policiesPublic) ? globalThis.policiesPublic : [];
  828. const getLibraryNameSet = (): Set<string> => buildLibraryNameSet(getLibraryPolicies());
  829. /** 本轮卡片里匹配到的惠企政策名(用于在政策库列表里置顶) */
  830. const matchedLibraryNames = computed(() =>
  831. collectMatchedLibraryNames(policies.value, getLibraryNameSet())
  832. );
  833. /** 当前详情页正在看的政策是否惠企政策——决定是否显示"返回政策列表" */
  834. const selectedIsPolicyLibrary = computed(() =>
  835. isPolicyLibraryItem(selectedPolicyItem.value, getLibraryNameSet())
  836. );
  837. /** 构建惠企政策库列表:本轮匹配到的置顶,其余保持库内原顺序 */
  838. const buildLibraryPolicyList = () => {
  839. const keyword = searchKeyword.value.trim().toLowerCase();
  840. const dept = currentDeptFilter.value === "全部部门" ? "" : currentDeptFilter.value;
  841. const matched = matchedLibraryNames.value;
  842. const filtered = getLibraryPolicies().map(normalizePolicy).filter((item) => {
  843. // 与卡片列表保持同一套筛选逻辑
  844. if (dept && String(item.source || "") !== dept) {
  845. return false;
  846. }
  847. const autoGranted = !!item.originalData?.auto_granted;
  848. if (currentFilterOption.value === "只看免申即享" && !autoGranted) return false;
  849. if (currentFilterOption.value === "非免申即享" && autoGranted) return false;
  850. if (keyword) {
  851. const haystack = [item.title, item.declaration_item, item.source, item.desc]
  852. .join(" ")
  853. .toLowerCase();
  854. if (!haystack.includes(keyword)) return false;
  855. }
  856. return true;
  857. });
  858. const ordered = pinMatchedFirst(filtered, matched, (item) => item.title);
  859. const start = (currentPage.value - 1) * itemsPerPage;
  860. policyListItems.value = ordered.slice(start, start + itemsPerPage);
  861. policyListTotal.value = ordered.length;
  862. };
  863. const buildRecordPolicyList = () => {
  864. const keyword = searchKeyword.value.trim().toLowerCase();
  865. const dept = currentDeptFilter.value === "全部部门" ? "" : currentDeptFilter.value;
  866. const filtered = policies.value.filter((item) => {
  867. // 部门按字面完全相等比较
  868. if (dept && String(item.source || "") !== dept) {
  869. return false;
  870. }
  871. const autoGranted = !!item.originalData?.auto_granted;
  872. if (currentFilterOption.value === "只看免申即享" && !autoGranted) return false;
  873. if (currentFilterOption.value === "非免申即享" && autoGranted) return false;
  874. if (keyword) {
  875. const haystack = [
  876. item.title,
  877. item.desc,
  878. item.declaration_item,
  879. item.match_reason,
  880. item.source,
  881. item.enterprise_benefit,
  882. ]
  883. .join(" ")
  884. .toLowerCase();
  885. if (!haystack.includes(keyword)) return false;
  886. }
  887. return true;
  888. });
  889. const start = (currentPage.value - 1) * itemsPerPage;
  890. policyListItems.value = filtered.slice(start, start + itemsPerPage);
  891. policyListTotal.value = filtered.length;
  892. };
  893. const syncPolicies = () => {
  894. const raw = props.policyData?.length
  895. ? props.policyData
  896. : parsePolicyTableContent(props.policyContent);
  897. const merged = (raw || []).map(mergeWithPublicPolicies);
  898. policies.value = merged.map(normalizePolicy);
  899. currentPage.value = 1; // 重置页码
  900. selectedPolicyOverride.value = null;
  901. // 换了一批卡片后让“更多”列表重建
  902. if (props.recordMode) {
  903. if (panelVisible.value && panelMode.value === "list") {
  904. buildRecordPolicyList();
  905. }
  906. }
  907. // 「政策列表」侧边栏开着时,流式新增的卡片也同步进去
  908. if (panelVisible.value && panelMode.value === "cardList") {
  909. rebuildCardList();
  910. }
  911. };
  912. /**
  913. * 刷新「政策事项列表」:主来源是惠企政策库,库还没加载完时退回本会话卡片(避免列表空白)。
  914. */
  915. const fetchPolicyList = async () => {
  916. if (!props.recordMode || !panelVisible.value || panelMode.value !== "list") return;
  917. if (getLibraryPolicies().length) {
  918. buildLibraryPolicyList();
  919. return;
  920. }
  921. buildRecordPolicyList();
  922. };
  923. const panelData = computed(() => {
  924. return [...policies.value]
  925. .sort((a, b) => b.match_score - a.match_score)
  926. .slice(0, 6)
  927. .map((item) => ({
  928. // 卡片区标题用「政策名」:declaration_item 表示具体申报事项,优先取它会改掉卡片区观感
  929. title: item.title || item.originalData?.declaration_item || item.declaration_item,
  930. desc: item.desc || item.match_reason || "",
  931. match_score: item.match_score,
  932. match_degree: item.match_degree,
  933. enterprise_benefit: item.enterprise_benefit,
  934. // 来源标识(惠企政策库)
  935. is_policy_library: item.originalData?.is_policy_library === true,
  936. originalData: item.originalData,
  937. }));
  938. });
  939. const showSummary = computed(() => {
  940. return props.visible && policies.value.length > 0;
  941. });
  942. const selectedPolicyItem = computed(() => {
  943. return selectedPolicyOverride.value || policies.value[selectedPolicy.value] || null;
  944. });
  945. /**
  946. * /api/chat 协议独有、随 POLICY_TABLE 一起传入的附加数据(由 normalizePolicy 留在 originalData 里),
  947. * 仅记录模式(对话内)展示。
  948. */
  949. const selectedConditions = computed<any[]>(() => {
  950. const list = selectedPolicyItem.value?.originalData?.conditions;
  951. return Array.isArray(list) ? list : [];
  952. });
  953. const selectedMatchBasis = computed<any[]>(() => {
  954. const list = selectedPolicyItem.value?.originalData?.match_basis;
  955. return Array.isArray(list) ? list : [];
  956. });
  957. // conditions.status 是中文,复用面板已有的匹配等级配色
  958. const getConditionLevelClass = (status?: string) => {
  959. if (status === '满足') return 'match-high';
  960. if (status === '不满足') return 'match-low';
  961. if (status === '待确认') return 'match-medium';
  962. return 'match-default';
  963. };
  964. // match_basis.status 是英文(supports / conflicts / unknown)
  965. const getBasisLevelClass = (status?: string) => {
  966. if (status === 'supports') return 'match-high';
  967. if (status === 'conflicts') return 'match-low';
  968. if (status === 'unknown') return 'match-medium';
  969. return 'match-default';
  970. };
  971. const formatBasisStatus = (status?: string) => {
  972. if (status === 'supports') return '有相关依据';
  973. if (status === 'conflicts') return '存在冲突';
  974. if (status === 'unknown') return '无法判断';
  975. return status || '无法判断';
  976. };
  977. const getMatchProgress = (degree: string) => {
  978. if (degree === '高' || degree === '高匹配') return 0.8;
  979. if (degree === '中' || degree === '中匹配') return 0.5;
  980. if (degree === '低' || degree === '低匹配') return 0.2;
  981. return 0;
  982. };
  983. const getMatchLevelClass = (degree: string | undefined) => {
  984. if (degree === '高' || degree === '高匹配') return 'match-high';
  985. if (degree === '中' || degree === '中匹配') return 'match-medium';
  986. if (degree === '低' || degree === '低匹配') return 'match-low';
  987. return 'match-default';
  988. };
  989. const formatMatchDegree = (degree?: string) => {
  990. if (!degree) return '未匹配';
  991. return degree.includes('匹配') ? degree : `${degree}匹配`;
  992. };
  993. const parseStringArray = (str: string) => {
  994. try {
  995. const parsed = JSON.parse(str.replace(/'/g, '"'));
  996. if (Array.isArray(parsed)) {
  997. return parsed;
  998. }
  999. } catch (e) {
  1000. /* console.error('Failed to parse array string:', e); */
  1001. }
  1002. // Fallback to basic string split if JSON parse fails
  1003. return str.slice(1, -1).split(',').map(s => s.trim().replace(/^['"]|['"]$/g, ''));
  1004. };
  1005. const handleResize = () => {
  1006. isWide.value = window.innerWidth > widthLevel;
  1007. isMobileViewer.value = window.innerWidth <= 767;
  1008. emitPanelWidth();
  1009. };
  1010. const emitPanelWidth = () => {
  1011. const width = panelVisible.value && isWide.value ? (props.recordMode ? 600 : 778) : 0;
  1012. document.dispatchEvent(
  1013. new CustomEvent("policyPanelWidthChange", {
  1014. detail: { width },
  1015. })
  1016. );
  1017. };
  1018. const openDetail = async (index: number) => {
  1019. selectedPolicyOverride.value = null;
  1020. selectedPolicy.value = index;
  1021. panelMode.value = "detail";
  1022. const openTicket = modalMutex?.requestOpen(policyModalId);
  1023. await nextTick();
  1024. if (modalMutex && openTicket !== undefined && !modalMutex.isCurrentRequest(policyModalId, openTicket)) return;
  1025. panelVisible.value = true;
  1026. emitPanelWidth();
  1027. };
  1028. const openDetailByItem = async (item: { originalData?: RawPolicyItem; support_objects?: string }) => {
  1029. // 反查下标(唯一命中才采信),详见 policy-match-utils.ts 的 resolvePolicyDetailIndex
  1030. const index = resolvePolicyDetailIndex(policies.value, item);
  1031. if (index >= 0) {
  1032. openDetail(index);
  1033. return;
  1034. }
  1035. selectedPolicyOverride.value = "support_objects" in item ? item as PolicyItem : null;
  1036. selectedPolicy.value = 0;
  1037. panelMode.value = "detail";
  1038. const openTicket = modalMutex?.requestOpen(policyModalId);
  1039. await nextTick();
  1040. if (modalMutex && openTicket !== undefined && !modalMutex.isCurrentRequest(policyModalId, openTicket)) return;
  1041. panelVisible.value = true;
  1042. emitPanelWidth();
  1043. };
  1044. /**
  1045. * 打开「政策事项列表」(旧列表)。数据来自惠企政策库,本轮匹配到的置顶。
  1046. * 卡片区的「更多 >>」走 openCardList,不走这里。
  1047. */
  1048. const openPolicyList = async () => {
  1049. selectedPolicyOverride.value = null;
  1050. panelMode.value = "list";
  1051. // 面板打开即刷新部门清单(库是异步加载的,见 deptOptions 的说明)
  1052. refreshDeptOptions();
  1053. // 每次进入都重置筛选:否则上次的筛选残留会表现为"条目少"或"筛不出东西"
  1054. currentPage.value = 1;
  1055. searchKeyword.value = "";
  1056. currentDeptFilter.value = "全部部门";
  1057. currentFilterOption.value = "所有分类";
  1058. const openTicket = modalMutex?.requestOpen(policyModalId);
  1059. await nextTick();
  1060. if (modalMutex && openTicket !== undefined && !modalMutex.isCurrentRequest(policyModalId, openTicket)) return;
  1061. panelVisible.value = true;
  1062. fetchPolicyList();
  1063. emitPanelWidth();
  1064. };
  1065. /**
  1066. * 打开「政策列表」侧边栏 —— 卡片区「更多 >>」的入口。
  1067. * 只有搜索框与热门搜索,没有部门/分类筛选。
  1068. *
  1069. * ⚠️ 必须**先切 panelMode 再重置共享状态**:重置会触发 watch → fetchPolicyList,
  1070. * 而它开头有 `panelMode !== "list"` 的守卫,切了模式才能让旧列表逻辑短路,
  1071. * 不污染 policyListItems / policyListTotal。
  1072. */
  1073. const openCardList = async () => {
  1074. selectedPolicyOverride.value = null;
  1075. panelMode.value = "cardList";
  1076. refreshDeptOptions();
  1077. currentPage.value = 1;
  1078. searchKeyword.value = "";
  1079. currentDeptFilter.value = "全部部门";
  1080. currentFilterOption.value = "所有分类";
  1081. showFilterMenu.value = false;
  1082. showDeptMenu.value = false;
  1083. rebuildCardList();
  1084. const openTicket = modalMutex?.requestOpen(policyModalId);
  1085. await nextTick();
  1086. if (modalMutex && openTicket !== undefined && !modalMutex.isCurrentRequest(policyModalId, openTicket)) return;
  1087. panelVisible.value = true;
  1088. emitPanelWidth();
  1089. };
  1090. const closePanel = () => {
  1091. panelVisible.value = false;
  1092. modalMutex?.release(policyModalId);
  1093. emitPanelWidth();
  1094. emit("update:visible", false);
  1095. };
  1096. onMounted(() => {
  1097. unregisterPolicyModal = modalMutex?.register(policyModalId, {
  1098. close: closePanel,
  1099. isOpen: () => panelVisible.value,
  1100. });
  1101. });
  1102. defineExpose({ closePanel });
  1103. watch(
  1104. () => [props.policyData, props.policyContent],
  1105. () => {
  1106. syncPolicies();
  1107. },
  1108. { immediate: true, deep: true }
  1109. );
  1110. watch(
  1111. () => props.visible,
  1112. (visible) => {
  1113. if (!visible) {
  1114. panelVisible.value = false;
  1115. emitPanelWidth();
  1116. }
  1117. }
  1118. );
  1119. onMounted(() => {
  1120. handleResize();
  1121. window.addEventListener("resize", handleResize);
  1122. });
  1123. onBeforeUnmount(() => {
  1124. unregisterPolicyModal?.();
  1125. document.dispatchEvent(
  1126. new CustomEvent("policyPanelWidthChange", {
  1127. detail: { width: 0 },
  1128. })
  1129. );
  1130. window.removeEventListener("resize", handleResize);
  1131. });
  1132. </script>
  1133. <style lang="scss">
  1134. .policy-match-summary {
  1135. display: flex;
  1136. flex-direction: column;
  1137. align-items: flex-start;
  1138. gap: 8px;
  1139. pointer-events: auto;
  1140. max-width: 360px;
  1141. &.record-mode {
  1142. margin-top: 12px;
  1143. margin-bottom: 12px;
  1144. }
  1145. }
  1146. .policy-header {
  1147. font-size: 14px;
  1148. color: #141b2b;
  1149. line-height: 24px;
  1150. }
  1151. .policy-content-wrapper {
  1152. display: flex;
  1153. flex-direction: column;
  1154. width: 100%;
  1155. }
  1156. .policy-line {
  1157. display: none;
  1158. }
  1159. .policy-box {
  1160. width: 100%;
  1161. background: transparent;
  1162. border: none;
  1163. padding: 0;
  1164. display: flex;
  1165. flex-direction: column;
  1166. gap: 12px;
  1167. }
  1168. .policy-item {
  1169. background: #ffffff;
  1170. border: 1px solid rgba(199, 196, 216, 0.3);
  1171. border-radius: 12px;
  1172. box-shadow: 0 1px 1.75px rgba(0, 0, 0, 0.05);
  1173. padding: 16px;
  1174. display: flex;
  1175. flex-direction: column;
  1176. }
  1177. .policy-item-header {
  1178. display: flex;
  1179. justify-content: space-between;
  1180. align-items: center;
  1181. width: 100%;
  1182. margin-bottom: 12px;
  1183. }
  1184. .policy-badge {
  1185. display: flex;
  1186. align-items: center;
  1187. gap: 8px;
  1188. background: rgba(53, 37, 205, 0.05);
  1189. border-radius: 6px;
  1190. padding: 4px 8px;
  1191. color: #3525cd;
  1192. font-size: 10px;
  1193. font-weight: 500;
  1194. line-height: 1.5;
  1195. }
  1196. /* 来源标识:惠企政策库。与左侧“企业收益”徽标同形,仅换配色以便一眼区分 */
  1197. .policy-source-badge {
  1198. flex-shrink: 0;
  1199. display: flex;
  1200. align-items: center;
  1201. background: rgba(230, 126, 34, 0.1);
  1202. border-radius: 6px;
  1203. padding: 4px 8px;
  1204. color: #c2410c;
  1205. font-size: 10px;
  1206. font-weight: 500;
  1207. line-height: 1.5;
  1208. }
  1209. .policy-match-degree {
  1210. display: flex;
  1211. flex-direction: column;
  1212. align-items: flex-end;
  1213. }
  1214. .policy-match-degree .label {
  1215. font-size: 10px;
  1216. font-weight: 500;
  1217. color: #464555;
  1218. line-height: 1.5;
  1219. }
  1220. .policy-match-degree .value {
  1221. font-size: 16px;
  1222. font-weight: 700;
  1223. color: #3525cd;
  1224. line-height: 1.5;
  1225. font-family: "Inter", sans-serif;
  1226. }
  1227. .policy-title {
  1228. color: #141b2b;
  1229. font-size: 16px;
  1230. font-weight: 500;
  1231. line-height: 1.5;
  1232. margin-bottom: 8px;
  1233. }
  1234. .policy-desc {
  1235. color: #464555;
  1236. font-size: 12px;
  1237. font-weight: 500;
  1238. line-height: 1.4;
  1239. letter-spacing: 0.12px;
  1240. margin-bottom: 16px;
  1241. display: -webkit-box;
  1242. -webkit-line-clamp: 3;
  1243. -webkit-box-orient: vertical;
  1244. overflow: hidden;
  1245. text-overflow: ellipsis;
  1246. }
  1247. .policy-item-action {
  1248. display: flex;
  1249. justify-content: center;
  1250. align-items: center;
  1251. background: #3525cd;
  1252. color: #ffffff;
  1253. border-radius: 8px;
  1254. padding: 8px 0;
  1255. font-size: 12px;
  1256. font-weight: 500;
  1257. letter-spacing: 0.12px;
  1258. line-height: 1.4;
  1259. cursor: pointer;
  1260. transition: background-color 0.2s ease;
  1261. }
  1262. .policy-item-action:hover {
  1263. background: #2b1ea8;
  1264. }
  1265. .view-all-btn {
  1266. color: #3525cd;
  1267. text-align: center;
  1268. padding-top: 4px;
  1269. cursor: pointer;
  1270. font-size: 14px;
  1271. font-weight: 500;
  1272. }
  1273. .policy-viewer-modal {
  1274. position: fixed;
  1275. inset: 0;
  1276. z-index: 2100;
  1277. pointer-events: none;
  1278. }
  1279. .policy-viewer-content {
  1280. position: absolute;
  1281. left: 50%;
  1282. top: 50%;
  1283. transform: translate(-50%, -50%);
  1284. width: min(100vw, 760px);
  1285. height: 78vh;
  1286. background: #ffffff;
  1287. border-radius: 20px 20px 0 0;
  1288. display: flex;
  1289. flex-direction: column;
  1290. pointer-events: auto;
  1291. box-shadow: 0 -8px 28px rgba(0, 0, 0, 0.22);
  1292. will-change: transform, opacity;
  1293. transition: transform 240ms cubic-bezier(0.22, 1, 0.36, 1), opacity 240ms cubic-bezier(0.22, 1, 0.36, 1);
  1294. &.right-fixed {
  1295. left: auto;
  1296. right: 0;
  1297. top: 0;
  1298. bottom: 0;
  1299. transform: none;
  1300. width: 778px;
  1301. max-width: 100vw;
  1302. height: 100vh;
  1303. border-radius: 0;
  1304. box-shadow: -4px 0 20px rgba(0, 0, 0, 0.16);
  1305. }
  1306. }
  1307. .policy-panel-enter-active,
  1308. .policy-panel-leave-active {
  1309. transition: opacity 240ms cubic-bezier(0.22, 1, 0.36, 1), transform 240ms cubic-bezier(0.22, 1, 0.36, 1);
  1310. }
  1311. .policy-panel-enter-from,
  1312. .policy-panel-leave-to {
  1313. opacity: 0;
  1314. transform: translate(-50%, calc(-50% + 14px)) scale(0.985);
  1315. }
  1316. .policy-viewer-content.right-fixed.policy-panel-enter-from,
  1317. .policy-viewer-content.right-fixed.policy-panel-leave-to {
  1318. transform: translateX(12px) scale(0.985);
  1319. }
  1320. .record-theme .policy-viewer-content.right-fixed {
  1321. width: 600px;
  1322. border-left: 1px solid rgba(199, 196, 216, 0.3);
  1323. box-shadow: none;
  1324. }
  1325. /* \u5339\u914d\u6761\u4ef6\u533a\u57df\u6837\u5f0f */
  1326. .condition-content-wrapper {
  1327. display: flex;
  1328. flex-direction: column;
  1329. gap: 16px;
  1330. width: 100%;
  1331. }
  1332. .condition-text-box {
  1333. width: 100%;
  1334. padding: 0 0 0 24px;
  1335. border-left: 1px solid #a1a1a1;
  1336. // border-radius: 8px;
  1337. font-size: 14px;
  1338. line-height: 1.6;
  1339. color: rgba(70, 69, 85, 0.5);
  1340. white-space: pre-line;
  1341. }
  1342. .condition-match-card {
  1343. width: 100%;
  1344. border-radius: 12px;
  1345. padding: 20px;
  1346. border-left-width: 4px;
  1347. border-left-style: solid;
  1348. display: flex;
  1349. flex-direction: column;
  1350. gap: 16px;
  1351. background: #ffffff;
  1352. box-shadow: 0 1px 1.75px rgba(0, 0, 0, 0.05);
  1353. }
  1354. .match-card-header {
  1355. display: flex;
  1356. justify-content: space-between;
  1357. align-items: center;
  1358. }
  1359. .match-card-title {
  1360. display: flex;
  1361. align-items: center;
  1362. gap: 12px;
  1363. font-size: 14px;
  1364. font-weight: 500;
  1365. color: #464555;
  1366. }
  1367. .match-badge {
  1368. padding: 4px 8px;
  1369. border-radius: 4px;
  1370. font-size: 12px;
  1371. font-weight: 500;
  1372. line-height: 1.4;
  1373. letter-spacing: 0.12px;
  1374. }
  1375. .match-card-body {
  1376. font-size: 14px;
  1377. line-height: 1.6;
  1378. color: #141b2b;
  1379. }
  1380. /* \u9ad8\u5339\u914d\u6837\u5f0f */
  1381. .match-high {
  1382. border-left-color: #22c55e;
  1383. border-top: 1px solid rgba(199, 196, 216, 0.5);
  1384. border-right: 1px solid rgba(199, 196, 216, 0.5);
  1385. border-bottom: 1px solid rgba(199, 196, 216, 0.5);
  1386. }
  1387. .match-high .match-badge {
  1388. background: #dcfce7;
  1389. color: #15803d;
  1390. }
  1391. .match-high .match-card-title svg {
  1392. color: #16a34a;
  1393. }
  1394. /* \u4e2d\u5339\u914d\u6837\u5f0f */
  1395. .match-medium {
  1396. border-left-color: #eab308;
  1397. border-top: 1px solid rgba(199, 196, 216, 0.5);
  1398. border-right: 1px solid rgba(199, 196, 216, 0.5);
  1399. border-bottom: 1px solid rgba(199, 196, 216, 0.5);
  1400. }
  1401. .match-medium .match-badge {
  1402. background: #fef9c3;
  1403. color: #a16207;
  1404. }
  1405. .match-medium .match-card-title svg {
  1406. color: #ca8a04;
  1407. }
  1408. /* \u4f4e\u5339\u914d\u6837\u5f0f */
  1409. .match-low {
  1410. border-left-color: #ef4444;
  1411. border-top: 1px solid rgba(199, 196, 216, 0.5);
  1412. border-right: 1px solid rgba(199, 196, 216, 0.5);
  1413. border-bottom: 1px solid rgba(199, 196, 216, 0.5);
  1414. }
  1415. .match-low .match-badge {
  1416. background: #fee2e2;
  1417. color: #b91c1c;
  1418. }
  1419. .match-low .match-card-title svg {
  1420. color: #dc2626;
  1421. }
  1422. /* \u9ed8\u8ba4\u5339\u914d\u6837\u5f0f */
  1423. .match-default {
  1424. border-left-color: #94a3b8;
  1425. border-top: 1px solid rgba(199, 196, 216, 0.5);
  1426. border-right: 1px solid rgba(199, 196, 216, 0.5);
  1427. border-bottom: 1px solid rgba(199, 196, 216, 0.5);
  1428. }
  1429. .match-default .match-badge {
  1430. background: #f1f5f9;
  1431. color: #475569;
  1432. }
  1433. .match-default .match-card-title svg {
  1434. color: #64748b;
  1435. }
  1436. .record-close-btn {
  1437. color: #464555;
  1438. background: transparent;
  1439. width: 40px;
  1440. height: 40px;
  1441. border-radius: 8px;
  1442. display: flex;
  1443. align-items: center;
  1444. justify-content: center;
  1445. transition: background-color 0.2s;
  1446. &:hover {
  1447. background-color: rgba(195, 192, 255, 0.1);
  1448. }
  1449. }
  1450. .policy-viewer-header {
  1451. display: flex;
  1452. align-items: center;
  1453. justify-content: flex-end;
  1454. gap: 16px;
  1455. padding: 24px 28px;
  1456. background-color: #3cb4be;
  1457. background-image: url("../../assets/header-bg.png");
  1458. background-position: center;
  1459. background-size: cover;
  1460. flex-shrink: 0;
  1461. position: relative;
  1462. }
  1463. .policy-viewer-header-record {
  1464. justify-content: flex-end;
  1465. padding: 32px 40px 0 40px;
  1466. background-color: #ffffff;
  1467. background-image: none;
  1468. }
  1469. .header-title {
  1470. position: absolute;
  1471. left: 50%;
  1472. transform: translateX(-50%);
  1473. color: #ffffff;
  1474. font-size: 20px;
  1475. font-weight: 700;
  1476. line-height: 1.2;
  1477. }
  1478. .record-theme .header-title {
  1479. display: none;
  1480. }
  1481. .header-actions {
  1482. display: flex;
  1483. align-items: center;
  1484. gap: 8px;
  1485. flex-shrink: 0;
  1486. }
  1487. .header-back,
  1488. .header-close {
  1489. border: none;
  1490. background: transparent;
  1491. padding: 0;
  1492. cursor: pointer;
  1493. display: inline-flex;
  1494. align-items: center;
  1495. justify-content: center;
  1496. }
  1497. .header-close-record {
  1498. background: rgba(79, 70, 229, 0.1);
  1499. padding: 8px 16px;
  1500. border-radius: 9999px;
  1501. color: #4f46e5;
  1502. font-size: 14px;
  1503. font-weight: 500;
  1504. gap: 4px;
  1505. }
  1506. .panel-scroll {
  1507. overflow: hidden;
  1508. min-height: 0;
  1509. scrollbar-width: auto;
  1510. }
  1511. .list-mode-bg {
  1512. background: #f6f6f6;
  1513. }
  1514. .record-theme .list-mode-bg {
  1515. background: #ffffff;
  1516. }
  1517. .list-body {
  1518. display: flex;
  1519. flex-direction: column;
  1520. min-height: 0;
  1521. background: #f6f6f6;
  1522. overflow: hidden;
  1523. }
  1524. .record-theme .list-body {
  1525. background: #ffffff;
  1526. }
  1527. .list-content {
  1528. margin: 19px 25px 25px;
  1529. padding: 0 19px;
  1530. background: #ffffff;
  1531. flex: 1;
  1532. display: flex;
  1533. flex-direction: column;
  1534. min-height: 0;
  1535. overflow: hidden;
  1536. }
  1537. .record-theme .list-content {
  1538. margin: 0;
  1539. padding: 0 40px 0 40px;
  1540. display: flex;
  1541. flex-direction: column;
  1542. gap: 24px;
  1543. min-height: 0;
  1544. }
  1545. .list-items-container {
  1546. flex: 1;
  1547. min-height: 0;
  1548. overflow-y: auto;
  1549. overflow-x: hidden;
  1550. display: flex;
  1551. flex-direction: column;
  1552. gap: 12px;
  1553. margin-right: -8px;
  1554. scrollbar-width: auto;
  1555. }
  1556. .record-theme .list-item-wrapper {
  1557. background: #ffffff;
  1558. border: 1px solid #e9edff;
  1559. border-radius: 12px;
  1560. padding: 24px;
  1561. transition: transform 0.2s;
  1562. box-shadow: 0 1px 1.75px rgba(0, 0, 0, 0.05);
  1563. }
  1564. .record-theme .list-item-content {
  1565. gap: 8px;
  1566. display: flex;
  1567. flex-direction: column;
  1568. align-items: flex-start;
  1569. flex: 1;
  1570. min-width: 0;
  1571. }
  1572. .record-theme .record-list-item {
  1573. display: flex;
  1574. justify-content: space-between;
  1575. align-items: center;
  1576. gap: 24px;
  1577. }
  1578. .record-item-top {
  1579. display: flex;
  1580. align-items: center;
  1581. gap: 12px;
  1582. }
  1583. .record-item-tag {
  1584. background: #e2dfff;
  1585. color: #3323cc;
  1586. font-size: 12px;
  1587. font-weight: 500;
  1588. padding: 2px 8px;
  1589. border-radius: 4px;
  1590. line-height: 1.4;
  1591. white-space: nowrap;
  1592. }
  1593. .record-item-date {
  1594. color: #464555;
  1595. font-size: 12px;
  1596. font-weight: 500;
  1597. line-height: 1.4;
  1598. }
  1599. .record-theme .list-item-title {
  1600. font-size: 22px;
  1601. font-weight: 500;
  1602. color: #141b2b;
  1603. line-height: 1.3;
  1604. letter-spacing: -0.24px;
  1605. margin-bottom: 0;
  1606. display: -webkit-box;
  1607. -webkit-line-clamp: 2;
  1608. -webkit-box-orient: vertical;
  1609. overflow: hidden;
  1610. text-overflow: ellipsis;
  1611. }
  1612. .record-item-source {
  1613. color: #464555;
  1614. font-size: 14px;
  1615. font-weight: 500;
  1616. line-height: 1.6;
  1617. }
  1618. .record-item-actions {
  1619. display: flex;
  1620. flex-direction: column;
  1621. align-items: center;
  1622. width: 98px;
  1623. flex-shrink: 0;
  1624. gap: 8px;
  1625. }
  1626. .record-match-box {
  1627. position: relative;
  1628. width: 64px;
  1629. height: 64px;
  1630. display: flex;
  1631. align-items: center;
  1632. justify-content: center;
  1633. }
  1634. .record-match-icon {
  1635. position: absolute;
  1636. inset: 0;
  1637. width: 100%;
  1638. height: 100%;
  1639. }
  1640. .record-match-text {
  1641. color: #3525cd;
  1642. font-size: 14px;
  1643. font-weight: 700;
  1644. line-height: 1.8;
  1645. z-index: 1;
  1646. }
  1647. .record-match-meta {
  1648. color: #464555;
  1649. font-size: 12px;
  1650. font-weight: 500;
  1651. line-height: 1.4;
  1652. margin-top: 3px;
  1653. margin-bottom: 12px;
  1654. }
  1655. .record-detail-btn {
  1656. background: transparent;
  1657. color: #3525cd;
  1658. font-size: 12px;
  1659. font-weight: 500;
  1660. border: 1px solid #3525cd;
  1661. border-radius: 8px;
  1662. padding: 8px 24px;
  1663. cursor: pointer;
  1664. white-space: nowrap;
  1665. transition: all 0.2s;
  1666. }
  1667. .record-detail-btn:hover {
  1668. background: rgba(53, 37, 205, 0.05);
  1669. }
  1670. .record-theme .list-item-source {
  1671. color: #777587;
  1672. font-size: 12px;
  1673. font-weight: 500;
  1674. line-height: 1.4;
  1675. letter-spacing: 0.6px;
  1676. }
  1677. .record-theme .list-item-date {
  1678. display: none;
  1679. }
  1680. .record-list-header {
  1681. display: flex;
  1682. flex-direction: column;
  1683. gap: 7px;
  1684. }
  1685. .record-list-title {
  1686. color: #141b2b;
  1687. font-size: 24px;
  1688. font-weight: 500;
  1689. line-height: 1.2;
  1690. letter-spacing: -0.64px;
  1691. }
  1692. .record-list-subtitle {
  1693. color: #464555;
  1694. font-size: 16px;
  1695. font-weight: 500;
  1696. line-height: 1.6;
  1697. }
  1698. .search-filter-bar {
  1699. background: #ffffff;
  1700. border: 1px solid #e9edff;
  1701. border-radius: 12px;
  1702. padding: 24px;
  1703. display: flex;
  1704. flex-direction: column;
  1705. gap: 16px;
  1706. box-shadow: 0 1px 1.75px rgba(0, 0, 0, 0.05);
  1707. }
  1708. .search-filter-top {
  1709. display: flex;
  1710. gap: 16px;
  1711. width: 100%;
  1712. height: 49.19px;
  1713. }
  1714. .search-input-wrapper {
  1715. flex: 1;
  1716. position: relative;
  1717. display: flex;
  1718. align-items: center;
  1719. background: #f9f9ff;
  1720. border: 1px solid #c7c4d8;
  1721. border-radius: 8px;
  1722. padding: 0 16px 0 48px;
  1723. }
  1724. .search-icon {
  1725. position: absolute;
  1726. left: 16px;
  1727. }
  1728. .search-input {
  1729. width: 100%;
  1730. border: none;
  1731. background: transparent;
  1732. outline: none;
  1733. color: #141b2b;
  1734. font-size: 14px;
  1735. font-weight: 500;
  1736. }
  1737. .search-input::placeholder {
  1738. color: #6b7280;
  1739. }
  1740. .filter-selects {
  1741. display: flex;
  1742. gap: 16px;
  1743. height: 100%;
  1744. }
  1745. .filter-select-wrapper {
  1746. position: relative;
  1747. height: 100%;
  1748. }
  1749. .filter-option {
  1750. display: flex;
  1751. align-items: center;
  1752. justify-content: space-between;
  1753. background: #f9f9ff;
  1754. border: 1px solid #c7c4d8;
  1755. border-radius: 8px;
  1756. padding: 0 9px 0 16px;
  1757. color: #141b2b;
  1758. font-size: 14px;
  1759. font-weight: 500;
  1760. width: 150px;
  1761. height: 100%;
  1762. cursor: pointer;
  1763. user-select: none;
  1764. transition: all 0.2s ease;
  1765. }
  1766. .filter-option:hover {
  1767. background: #f0f0f5;
  1768. }
  1769. .filter-option svg {
  1770. transition: transform 0.2s ease;
  1771. }
  1772. .filter-option.is-open svg {
  1773. transform: rotate(180deg);
  1774. }
  1775. .filter-popover {
  1776. position: absolute;
  1777. top: calc(100% + 4px);
  1778. left: 0;
  1779. width: 150px;
  1780. background: #ffffff;
  1781. border-radius: 8px;
  1782. box-shadow: 0px 4px 10.5px rgba(0, 0, 0, 0.12);
  1783. padding: 0;
  1784. z-index: 10;
  1785. display: flex;
  1786. flex-direction: column;
  1787. overflow: hidden;
  1788. }
  1789. .filter-popover-item {
  1790. display: flex;
  1791. align-items: center;
  1792. justify-content: center;
  1793. flex-shrink: 0;
  1794. height: 40px;
  1795. padding: 6px 16px;
  1796. background: #fafbff;
  1797. color: #000000;
  1798. font-size: 14px;
  1799. font-weight: 500;
  1800. letter-spacing: 0.12px;
  1801. cursor: pointer;
  1802. transition: background 0.2s;
  1803. }
  1804. .filter-popover-item:hover {
  1805. background: #f0f4ff;
  1806. }
  1807. .filter-popover-item.active {
  1808. color: #4f46e5;
  1809. font-weight: 700;
  1810. }
  1811. .popover-fade-enter-active,
  1812. .popover-fade-leave-active {
  1813. transition: opacity 0.2s ease, transform 0.2s ease;
  1814. }
  1815. .popover-fade-enter-from,
  1816. .popover-fade-leave-to {
  1817. opacity: 0;
  1818. transform: translateY(-4px);
  1819. }
  1820. .dept-popover {
  1821. width: 158px;
  1822. max-height: 300px;
  1823. overflow-y: auto;
  1824. left: auto;
  1825. right: 0;
  1826. scrollbar-width: thin;
  1827. scrollbar-color: rgba(0, 0, 0, 0.2) transparent;
  1828. }
  1829. .dept-popover::-webkit-scrollbar {
  1830. width: 4px;
  1831. }
  1832. .dept-popover::-webkit-scrollbar-button {
  1833. display: none !important;
  1834. width: 0 !important;
  1835. height: 0 !important;
  1836. }
  1837. .dept-popover::-webkit-scrollbar-track {
  1838. background: transparent;
  1839. margin: 8px 0;
  1840. }
  1841. .dept-popover::-webkit-scrollbar-thumb {
  1842. background-color: rgba(0, 0, 0, 0.2);
  1843. border-radius: 4px;
  1844. }
  1845. .filter-btn {
  1846. display: flex;
  1847. align-items: center;
  1848. gap: 8px;
  1849. background: #4f46e5;
  1850. color: #ffffff;
  1851. border: none;
  1852. border-radius: 8px;
  1853. padding: 0 16px;
  1854. font-size: 14px;
  1855. font-weight: 500;
  1856. cursor: pointer;
  1857. height: 100%;
  1858. white-space: nowrap;
  1859. transition: all 0.2s ease;
  1860. }
  1861. .filter-btn:hover,
  1862. .filter-btn.is-open {
  1863. background: #291bac;
  1864. }
  1865. .quick-chips {
  1866. display: flex;
  1867. align-items: center;
  1868. gap: 8px;
  1869. }
  1870. .chip-label {
  1871. color: #464555;
  1872. font-size: 12px;
  1873. font-weight: 500;
  1874. margin-right: 8px;
  1875. }
  1876. .chip {
  1877. background: #e1e8fd;
  1878. color: #141b2b;
  1879. font-size: 12px;
  1880. font-weight: 500;
  1881. padding: 4px 12px;
  1882. border-radius: 9999px;
  1883. cursor: pointer;
  1884. }
  1885. .detail-body {
  1886. scrollbar-width: auto;
  1887. scrollbar-color: transparent transparent;
  1888. }
  1889. .detail-body:hover {
  1890. scrollbar-color: rgba(0, 0, 0, 0.2) transparent;
  1891. }
  1892. .list-item-wrapper {
  1893. background: #ffffff;
  1894. border-bottom: 2px solid #e4e4e4;
  1895. cursor: pointer;
  1896. transition: transform 0.2s;
  1897. padding: 19px 6px;
  1898. }
  1899. .list-item-wrapper:active {
  1900. transform: scale(0.98);
  1901. }
  1902. .list-item {
  1903. display: flex;
  1904. flex-direction: row;
  1905. align-items: center;
  1906. gap: 12px;
  1907. .list-item-content {
  1908. display: flex;
  1909. flex-direction: column;
  1910. gap: 6px;
  1911. flex: 1;
  1912. }
  1913. }
  1914. .list-item-tag {
  1915. display: inline-flex;
  1916. align-items: center;
  1917. gap: 3px;
  1918. background: linear-gradient(90deg, #3cd4e0, #2da4ae);
  1919. color: #ffffff;
  1920. padding: 2px 6px;
  1921. border-radius: 6px;
  1922. font-size: 12px;
  1923. width: max-content;
  1924. }
  1925. .tag-icon {
  1926. margin-right: 2px;
  1927. }
  1928. .list-item-title {
  1929. font-size: 16px;
  1930. font-weight: 600;
  1931. color: #000000;
  1932. line-height: 1.5;
  1933. display: -webkit-box;
  1934. -webkit-line-clamp: 2;
  1935. -webkit-box-orient: vertical;
  1936. overflow: hidden;
  1937. }
  1938. .list-item-source {
  1939. font-size: 14px;
  1940. color: #505050;
  1941. line-height: 1.5;
  1942. }
  1943. .list-item-date {
  1944. display: flex;
  1945. align-items: center;
  1946. gap: 6px;
  1947. font-size: 14px;
  1948. color: #505050;
  1949. margin-top: 6px;
  1950. }
  1951. .date-icon {
  1952. flex-shrink: 0;
  1953. transform: scale(0.85);
  1954. }
  1955. .list-item-recommend {
  1956. display: flex;
  1957. align-items: center;
  1958. justify-content: center;
  1959. height: 100%;
  1960. padding-left: 10px;
  1961. background: #ffffff;
  1962. color: #3cb4be;
  1963. font-size: 18px;
  1964. font-weight: 700;
  1965. flex-shrink: 0;
  1966. }
  1967. .detail-body {
  1968. display: flex;
  1969. flex-direction: column;
  1970. flex: 1;
  1971. min-height: 0;
  1972. padding: 0 32px 20px 32px;
  1973. background: #ffffff;
  1974. }
  1975. .record-detail-body {
  1976. padding: 32px 40px 64px 40px;
  1977. display: flex;
  1978. flex-direction: column;
  1979. gap: 32px;
  1980. width: 773px;
  1981. }
  1982. .record-header-section {
  1983. display: flex;
  1984. flex-direction: column;
  1985. gap: 16px;
  1986. width: 100%;
  1987. padding-bottom: 32px;
  1988. position: sticky;
  1989. top: 0;
  1990. z-index: 2;
  1991. background: #ffffff;
  1992. padding-top: 8px;
  1993. }
  1994. .detail-body-record {
  1995. overflow-y: auto;
  1996. overflow-x: hidden;
  1997. user-select: text;
  1998. }
  1999. .record-back-btn {
  2000. display: inline-flex;
  2001. align-items: center;
  2002. gap: 4px;
  2003. color: #3525cd;
  2004. font-size: 12px;
  2005. font-weight: 500;
  2006. cursor: pointer;
  2007. letter-spacing: 0.12px;
  2008. line-height: 1.4;
  2009. }
  2010. .record-main-title {
  2011. color: #141b2b;
  2012. font-size: 24px;
  2013. font-weight: 500;
  2014. line-height: 1.25;
  2015. letter-spacing: -0.64px;
  2016. }
  2017. .mobile-record-meta-info {
  2018. display: none;
  2019. }
  2020. .record-meta-info {
  2021. display: flex;
  2022. align-items: center;
  2023. gap: 32px;
  2024. padding-top: 8px;
  2025. /* 拉满整行:与下方卡片(.detail-body-grid,整行宽)右边缘对齐 */
  2026. width: 100%;
  2027. }
  2028. .record-meta-item {
  2029. display: flex;
  2030. flex-direction: column;
  2031. gap: 3px;
  2032. }
  2033. .record-meta-item.meta-name {
  2034. flex: 1 1 auto;
  2035. min-width: 0;
  2036. }
  2037. .record-meta-item.meta-no {
  2038. /* 可伸展宽度,下限 110px(部门写全称时才不会被挤成三行) */
  2039. flex: 0 1 340px;
  2040. min-width: 110px;
  2041. }
  2042. .meta-label {
  2043. color: #777587;
  2044. font-size: 12px;
  2045. font-weight: 500;
  2046. letter-spacing: 0.6px;
  2047. line-height: 1.4;
  2048. padding-bottom: 0.8px;
  2049. }
  2050. .meta-value {
  2051. color: #141b2b;
  2052. font-size: 14px;
  2053. font-weight: 500;
  2054. line-height: 1.6;
  2055. }
  2056. .record-meta-divider {
  2057. width: 1px;
  2058. height: 40px;
  2059. background: rgba(199, 196, 216, 0.5);
  2060. }
  2061. .detail-body-grid {
  2062. display: flex;
  2063. justify-content: space-between;
  2064. width: 100%;
  2065. gap: 20px;
  2066. }
  2067. .detail-header {
  2068. display: flex;
  2069. flex-direction: column;
  2070. gap: 12px;
  2071. }
  2072. .detail-title {
  2073. color: #000000;
  2074. font-size: 20px;
  2075. font-weight: 700;
  2076. line-height: 1.5;
  2077. }
  2078. .detail-subtitle {
  2079. color: #000000;
  2080. font-size: 16px;
  2081. font-weight: 700;
  2082. }
  2083. .divider {
  2084. width: 100%;
  2085. height: 1px;
  2086. background: #d9d9d9;
  2087. flex-shrink: 0;
  2088. margin: 24px 0;
  2089. }
  2090. .record-theme .divider {
  2091. width: 1px;
  2092. height: 40px;
  2093. background: rgba(199, 196, 216, 0.5);
  2094. margin: 0;
  2095. }
  2096. .detail-section-title {
  2097. color: #000000;
  2098. font-size: 16px;
  2099. font-weight: 700;
  2100. margin-bottom: 12px;
  2101. }
  2102. .detail-row {
  2103. display: flex;
  2104. flex-direction: row;
  2105. gap: 6px;
  2106. margin-bottom: 12px;
  2107. }
  2108. .record-theme .detail-row {
  2109. flex-direction: column;
  2110. gap: 16px;
  2111. margin-bottom: 23.69px;
  2112. border: 1px solid rgba(199, 196, 216, 0.5);
  2113. border-radius: 12px;
  2114. padding: 24px;
  2115. }
  2116. .record-theme .detail-body-grid .detail-row {
  2117. margin-bottom: 0;
  2118. }
  2119. .record-theme .detail-body-grid .row-label {
  2120. font-size: 18px;
  2121. }
  2122. .record-theme .condition-section {
  2123. border: none;
  2124. padding: 0;
  2125. gap: 23.69px;
  2126. }
  2127. .record-theme .condition-section .row-label {
  2128. color: #141b2b;
  2129. font-size: 18px;
  2130. font-weight: 500;
  2131. line-height: 1.4;
  2132. }
  2133. .record-theme .condition-value {
  2134. border: 1px solid rgba(199, 196, 216, 0.5);
  2135. border-radius: 12px;
  2136. padding: 20px;
  2137. background: #ffffff;
  2138. }
  2139. .row-label {
  2140. display: flex;
  2141. align-items: flex-start;
  2142. color: #000000;
  2143. font-size: 18px;
  2144. width: 120px;
  2145. flex-shrink: 0;
  2146. gap: 6px;
  2147. }
  2148. .record-theme .row-label {
  2149. align-items: center;
  2150. color: #141b2b;
  2151. font-size: 16px;
  2152. font-weight: 500;
  2153. width: 100%;
  2154. gap: 8px;
  2155. }
  2156. .dot-icon {
  2157. width: 24px;
  2158. height: 24px;
  2159. flex: 0;
  2160. display: flex;
  2161. align-items: center;
  2162. justify-content: center;
  2163. }
  2164. .label-text {
  2165. flex-grow: 1;
  2166. line-height: 1.6rem;
  2167. }
  2168. .record-theme .label-text {
  2169. line-height: 1.5;
  2170. }
  2171. .dot-inner {
  2172. width: 12px;
  2173. height: 12px;
  2174. border: 1px solid #3cb4be;
  2175. border-radius: 50%;
  2176. display: flex;
  2177. align-items: center;
  2178. justify-content: center;
  2179. }
  2180. .dot-inner::after {
  2181. content: "";
  2182. width: 6px;
  2183. height: 6px;
  2184. background: #3cb4be;
  2185. border-radius: 50%;
  2186. }
  2187. .row-value,
  2188. .detail-footer {
  2189. color: #000000;
  2190. font-size: 18px;
  2191. line-height: 1.5;
  2192. white-space: pre-wrap;
  2193. }
  2194. .record-theme .row-value {
  2195. color: #464555;
  2196. font-size: 14px;
  2197. line-height: 1.6;
  2198. font-weight: 500;
  2199. }
  2200. .attachment-item {
  2201. margin-bottom: 6px;
  2202. color: #3cb4be;
  2203. line-height: 1.5;
  2204. a {
  2205. color: #3cb4be;
  2206. text-decoration: underline;
  2207. }
  2208. }
  2209. .detail-footer {
  2210. display: flex;
  2211. flex-direction: column;
  2212. gap: 12px;
  2213. }
  2214. .footer-title {
  2215. font-size: 16px;
  2216. font-weight: 700;
  2217. margin-bottom: 0;
  2218. }
  2219. .footer-desc {
  2220. line-height: 1.5;
  2221. }
  2222. .record-theme .detail-header {
  2223. display: none;
  2224. }
  2225. .record-theme .detail-title {
  2226. display: none;
  2227. }
  2228. .record-theme .detail-subtitle {
  2229. display: none;
  2230. }
  2231. .record-theme .detail-section-title {
  2232. display: none;
  2233. }
  2234. .record-theme .dot-icon {
  2235. display: none;
  2236. }
  2237. .record-theme .detail-footer {
  2238. display: none;
  2239. }
  2240. .record-theme .footer-title {
  2241. display: none;
  2242. }
  2243. .record-theme .footer-desc {
  2244. display: none;
  2245. }
  2246. .policy-fade-enter-active,
  2247. .policy-fade-leave-active {
  2248. transition: opacity 0.2s ease;
  2249. }
  2250. .policy-fade-enter-from,
  2251. .policy-fade-leave-to {
  2252. opacity: 0;
  2253. }
  2254. @media (max-width: 1200px) {
  2255. .policy-match-summary {
  2256. max-width: 100%;
  2257. }
  2258. }
  2259. @media (max-width: 767px) {
  2260. .policy-viewer-content,
  2261. .policy-viewer-content.detail-mode-bg,
  2262. .policy-viewer-content.list-mode-bg {
  2263. left: 0;
  2264. right: 0;
  2265. top: 0;
  2266. bottom: 0;
  2267. transform: none;
  2268. width: 100vw;
  2269. max-width: none;
  2270. height: 100dvh;
  2271. border-radius: 0;
  2272. background: #ffffff;
  2273. box-shadow: none;
  2274. }
  2275. .policy-viewer-header {
  2276. position: absolute;
  2277. top: 27px;
  2278. right: 16px;
  2279. z-index: 4;
  2280. width: 40px;
  2281. height: 40px;
  2282. padding: 0;
  2283. background: transparent;
  2284. }
  2285. .policy-viewer-header-record {
  2286. padding: 0;
  2287. }
  2288. .header-actions {
  2289. width: 40px;
  2290. height: 40px;
  2291. justify-content: center;
  2292. }
  2293. .header-close,
  2294. .record-close-btn {
  2295. width: 40px;
  2296. height: 40px;
  2297. color: #464555;
  2298. border-radius: 999px;
  2299. }
  2300. .header-close svg path {
  2301. stroke: currentColor;
  2302. }
  2303. .list-body,
  2304. .record-theme .list-body {
  2305. background: #ffffff;
  2306. overflow-y: auto;
  2307. }
  2308. .list-content,
  2309. .record-theme .list-content {
  2310. margin: 0;
  2311. padding: 32px 16px 0;
  2312. gap: 24px;
  2313. min-height: 100%;
  2314. overflow: visible;
  2315. }
  2316. .record-list-header {
  2317. gap: 7px;
  2318. padding-right: 16px;
  2319. }
  2320. .record-list-title {
  2321. font-size: 24px;
  2322. line-height: 1.6;
  2323. letter-spacing: -0.64px;
  2324. }
  2325. .record-list-subtitle {
  2326. font-size: 16px;
  2327. line-height: 1.6;
  2328. }
  2329. .search-filter-bar {
  2330. padding: 16px;
  2331. gap: 16px;
  2332. border-radius: 12px;
  2333. }
  2334. .search-filter-top,
  2335. .filter-selects {
  2336. height: auto;
  2337. flex-direction: column;
  2338. }
  2339. .search-input-wrapper {
  2340. width: 100%;
  2341. height: 48px;
  2342. flex: none;
  2343. padding: 0 16px 0 48px;
  2344. }
  2345. .filter-selects {
  2346. display: grid;
  2347. grid-template-columns: 1fr 112px;
  2348. gap: 16px;
  2349. }
  2350. .filter-select-wrapper,
  2351. .filter-option,
  2352. .filter-btn {
  2353. width: 100%;
  2354. height: 48px;
  2355. }
  2356. .filter-btn {
  2357. justify-content: center;
  2358. padding: 0 18px;
  2359. background: #3525cd;
  2360. }
  2361. .quick-chips {
  2362. display: none;
  2363. }
  2364. .list-items-container {
  2365. overflow: scroll;
  2366. margin-right: 0;
  2367. gap: 12px;
  2368. }
  2369. .record-theme .list-item-wrapper {
  2370. padding: 16px;
  2371. border: 1px solid #e9edff;
  2372. border-radius: 12px;
  2373. box-shadow: none;
  2374. }
  2375. .record-theme .record-list-item {
  2376. align-items: center;
  2377. gap: 12px;
  2378. }
  2379. .record-item-top {
  2380. gap: 12px;
  2381. }
  2382. .record-theme .list-item-title {
  2383. font-size: 24px;
  2384. line-height: 1.3;
  2385. letter-spacing: -0.24px;
  2386. -webkit-line-clamp: 3;
  2387. }
  2388. .record-item-source {
  2389. font-size: 14px;
  2390. line-height: 1.6;
  2391. white-space: nowrap;
  2392. overflow: hidden;
  2393. text-overflow: ellipsis;
  2394. width: 100%;
  2395. }
  2396. .record-item-actions {
  2397. width: 98.48px;
  2398. }
  2399. .pagination-container {
  2400. display: grid;
  2401. grid-template-columns: repeat(7, minmax(0, 1fr));
  2402. gap: 8px;
  2403. padding: 0;
  2404. }
  2405. .page-btn {
  2406. width: 100%;
  2407. }
  2408. .detail-body,
  2409. .detail-body-record {
  2410. width: 100%;
  2411. padding: 32px 16px 64px;
  2412. gap: 32px;
  2413. background: #ffffff;
  2414. }
  2415. .record-header-section {
  2416. position: relative;
  2417. top: auto;
  2418. padding-top: 0;
  2419. padding-bottom: 0;
  2420. margin-bottom: 0;
  2421. gap: 12px;
  2422. }
  2423. .record-main-title {
  2424. font-size: 24px;
  2425. line-height: 1.3333333;
  2426. letter-spacing: -0.64px;
  2427. }
  2428. .record-meta-info {
  2429. display: none;
  2430. }
  2431. .mobile-record-meta-info {
  2432. display: flex;
  2433. .mobile-record-meta-item {
  2434. display: flex;
  2435. gap: 32px;
  2436. width: 100%;
  2437. min-height: 48px;
  2438. .meta-value {
  2439. font-family: "HarmonyOS Sans SC", sans-serif;
  2440. font-size: 14px;
  2441. font-weight: 500;
  2442. color: #777587;
  2443. line-height: 1.6;
  2444. padding-top: 3px;
  2445. width: 100%;
  2446. }
  2447. }
  2448. }
  2449. .detail-body-grid {
  2450. flex-direction: column;
  2451. gap: 16px;
  2452. }
  2453. .detail-row.card-style,
  2454. .record-theme .detail-row,
  2455. .record-theme .condition-value {
  2456. padding: 12px;
  2457. border: 1px solid rgba(199, 196, 216, 0.3);
  2458. border-radius: 12px;
  2459. }
  2460. .record-condition-title,
  2461. .record-theme .condition-section .row-label {
  2462. font-size: 16px;
  2463. }
  2464. }
  2465. .detail-row.card-style {
  2466. background: transparent;
  2467. border: 1px solid rgba(199, 196, 216, 0.3);
  2468. border-radius: 12px;
  2469. padding: 24px;
  2470. margin-bottom: 0;
  2471. display: flex;
  2472. flex-direction: column;
  2473. gap: 15px;
  2474. flex: 1;
  2475. box-sizing: border-box;
  2476. }
  2477. .record-condition-title {
  2478. display: flex;
  2479. align-items: center;
  2480. gap: 8px;
  2481. color: #141b2b;
  2482. font-size: 16px;
  2483. font-weight: 500;
  2484. line-height: 1.4;
  2485. margin-bottom: 0;
  2486. svg {
  2487. color: rgb(53, 37, 205);
  2488. }
  2489. }
  2490. .pagination-container {
  2491. display: flex;
  2492. align-items: center;
  2493. justify-content: center;
  2494. gap: 8px;
  2495. padding: 0 0 24px;
  2496. width: 100%;
  2497. }
  2498. .page-btn {
  2499. display: flex;
  2500. align-items: center;
  2501. justify-content: center;
  2502. width: 40px;
  2503. height: 40px;
  2504. border-radius: 8px;
  2505. background: transparent;
  2506. border: 1px solid #c7c4d8;
  2507. color: #464555;
  2508. font-family: Inter, sans-serif;
  2509. font-size: 16px;
  2510. cursor: pointer;
  2511. transition: all 0.2s ease;
  2512. }
  2513. .page-btn.active {
  2514. background: #3525cd;
  2515. color: #ffffff;
  2516. border-color: #3525cd;
  2517. }
  2518. .page-btn:hover:not(.active):not(:disabled) {
  2519. background: rgba(195, 192, 255, 0.1);
  2520. border-color: #3525cd;
  2521. color: #3525cd;
  2522. }
  2523. .page-btn:disabled {
  2524. opacity: 0.3;
  2525. cursor: not-allowed;
  2526. }
  2527. .page-ellipsis {
  2528. display: flex;
  2529. align-items: center;
  2530. justify-content: center;
  2531. padding: 0 8px;
  2532. color: #464555;
  2533. font-family: Inter, sans-serif;
  2534. font-size: 16px;
  2535. }
  2536. @media (max-width: 767px) {
  2537. .detail-row.card-style,
  2538. .record-theme .detail-row,
  2539. .record-theme .condition-value {
  2540. padding: 12px;
  2541. border: 1px solid rgba(199, 196, 216, 0.3);
  2542. border-radius: 12px;
  2543. }
  2544. .record-theme .detail-body-grid .detail-row {
  2545. margin-bottom: 0;
  2546. }
  2547. .record-theme .detail-body-grid .row-label,
  2548. .record-condition-title,
  2549. .record-theme .condition-section .row-label {
  2550. font-size: 16px;
  2551. }
  2552. }
  2553. .policy-link {
  2554. color: #3525cd;
  2555. text-decoration: underline;
  2556. text-underline-offset: 4px;
  2557. cursor: pointer;
  2558. transition: color 0.2s;
  2559. }
  2560. .policy-link:hover {
  2561. color: #2b1ea8;
  2562. }
  2563. </style>