PolicyMatch.vue 85 KB

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