detail.vue 53 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286
  1. <template>
  2. <view class="page">
  3. <view class="pageDecor">
  4. <view class="pageGlow glowA" />
  5. <view class="pageGlow glowB" />
  6. <view class="pageGlow glowC" />
  7. <view class="pageGrain" />
  8. </view>
  9. <!-- 顶部留白:兼容 iOS 刘海 -->
  10. <view class="safeTop" />
  11. <!-- 轻品牌页头:极克制、无导航栏,让独立扫码页有品牌收口 -->
  12. <view class="brandPageHeader">
  13. <view class="brandPageHeaderInner">
  14. <text class="brandPageHeaderName">佳友厚苑</text>
  15. <view class="brandPageHeaderDot" />
  16. <text class="brandPageHeaderLabel">官方溯源 品质可验</text>
  17. </view>
  18. </view>
  19. <view class="hero">
  20. <!-- 完整首屏大卡:品牌信任区 + 商品信息面板 -->
  21. <view class="heroCard">
  22. <!-- Hero 视觉区:商品图 + 品牌氛围 -->
  23. <view class="heroBg">
  24. <!-- 优先商品图,没有则用农场图 -->
  25. <image
  26. class="heroImage"
  27. :src="traceDetail.product?.image || traceDetail.farm?.image"
  28. mode="aspectFill"
  29. />
  30. <!-- 底部大字号水印,营造品牌氛围 -->
  31. <view class="heroBrandAnchor">
  32. <text class="heroBrandAnchorText">JIAYOU</text>
  33. </view>
  34. <!-- 光感层:多层次渐变 + 景深 + 底部暗化 -->
  35. <view class="heroMask" />
  36. </view>
  37. <!-- 商品信息面板:嵌入 Hero 底部,与 Hero 自然过渡 -->
  38. <view v-if="traceDetail.product?.name" class="heroInfoPanel">
  39. <view class="infoPanelInner">
  40. <view class="sectionHeader">
  41. <view class="sectionHeaderLeft">
  42. <text class="sectionEn">Product</text>
  43. <text class="sectionTitle">商品信息</text>
  44. </view>
  45. <view v-if="traceDetail.batch?.statusBadge" class="trustBadge" :class="traceDetail.batch.statusBadge.type">
  46. <!-- 认证徽章 SVG:双圆环 + 对勾,图标主视觉,文字辅助 -->
  47. <svg class="trustBadgeIcon" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
  48. <circle cx="10" cy="10" r="9" stroke="rgba(27,71,42,0.38)" stroke-width="0.9" />
  49. <circle cx="10" cy="10" r="5.5" stroke="rgba(27,71,42,0.22)" stroke-width="0.7" />
  50. <path d="M6.6 10.4L9.1 12.9L13.8 8" stroke="rgba(27,71,42,0.8)" stroke-width="1.15" stroke-linecap="round" stroke-linejoin="round" />
  51. </svg>
  52. <text class="trustBadgeText">{{ traceDetail.batch.statusBadge.text }}</text>
  53. </view>
  54. </view>
  55. <view class="infoPanelBody">
  56. <text class="infoPanelName">{{ traceDetail.product.name }}</text>
  57. <view v-if="traceDetail.product.spec" class="infoPanelSpec">
  58. <view class="specDot" />
  59. <text class="specText">{{ traceDetail.product.spec }}</text>
  60. </view>
  61. <view v-if="traceDetail.product?.intro" class="infoPanelIntro">
  62. <text>{{ traceDetail.product.intro }}</text>
  63. </view>
  64. </view>
  65. </view>
  66. </view>
  67. </view>
  68. </view>
  69. <scroll-view class="content" scroll-y>
  70. <!-- 溯源结论卡:先给结论,再给证据 -->
  71. <view v-if="traceDetail.batch?.exists && traceDetail.traceConclusionCard" class="card cardLevel1 cardConclusion">
  72. <view class="sectionHeader">
  73. <view class="sectionHeaderLeft">
  74. <text class="sectionEn">Conclusion</text>
  75. <text class="sectionTitle">溯源结论</text>
  76. </view>
  77. </view>
  78. <view class="conclusionBody">
  79. <text class="conclusionLead">{{ traceDetail.traceConclusionCard.conclusion }}</text>
  80. <text class="conclusionDesc">{{ traceDetail.traceConclusionCard.desc }}</text>
  81. </view>
  82. </view>
  83. <!-- 农场信息(合并为单卡:图片+名称+地区+介绍连续表达) -->
  84. <view v-if="traceDetail.farm?.name" class="card cardLevel2 cardFarm">
  85. <view class="sectionHeader">
  86. <view class="sectionHeaderLeft">
  87. <text class="sectionEn">Farm</text>
  88. <text class="sectionTitle">农场信息</text>
  89. </view>
  90. </view>
  91. <!-- 农场主体:左图右文结构,融入主体内容 -->
  92. <view class="farmBody">
  93. <view class="farmImageWrap">
  94. <image class="farmImage" :src="traceDetail.farm.image" mode="aspectFill" />
  95. </view>
  96. <view class="farmMeta">
  97. <text class="farmName">{{ traceDetail.farm.name }}</text>
  98. <view v-if="traceDetail.farm.location" class="farmLocation">
  99. <svg class="locationIcon" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
  100. <path d="M10 2C7.24 2 5 4.24 5 7c0 4 5 11 5 11s5-7 5-11c0-2.76-2.24-5-5-5zm0 6.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z" fill="rgba(39,113,76,0.6)"/>
  101. </svg>
  102. <text class="locationText">{{ traceDetail.farm.location }}</text>
  103. </view>
  104. </view>
  105. </view>
  106. <view v-if="traceDetail.farm?.intro" class="farmIntro">
  107. <text>{{ traceDetail.farm.intro }}</text>
  108. </view>
  109. </view>
  110. <!-- 农事时间轴:关键种植过程 -->
  111. <view v-if="traceDetail.batch?.exists && traceDetail.farmTimeline?.length" class="card cardLevel1 cardTimeline">
  112. <view class="sectionHeader">
  113. <view class="sectionHeaderLeft">
  114. <text class="sectionEn">Process</text>
  115. <text class="sectionTitle">种植过程记录</text>
  116. </view>
  117. </view>
  118. <view class="timelineList">
  119. <view
  120. v-for="(item, index) in traceDetail.farmTimeline"
  121. :key="`timeline-${index}`"
  122. :class="{
  123. 'timelineItem': true,
  124. 'is-key': item.stage === 'test',
  125. 'is-current': item.stage === 'pack'
  126. }"
  127. >
  128. <view class="timelineLeft">
  129. <view class="timelineDot" />
  130. <view v-if="index !== traceDetail.farmTimeline.length - 1" class="timelineLine" />
  131. </view>
  132. <view class="timelineRight">
  133. <text v-if="item.time" class="timelineTime">{{ item.time }}</text>
  134. <text class="timelineTitle">{{ item.title }}</text>
  135. <text class="timelineDesc">{{ item.desc }}</text>
  136. </view>
  137. </view>
  138. </view>
  139. <view class="timelineFootnote">
  140. <text>以上信息真实有效,可追溯验证</text>
  141. </view>
  142. </view>
  143. <!-- 种植现场实时画面 -->
  144. <view v-if="traceDetail.batch?.exists && traceDetail.camera" class="card cardLevel1 cardLive">
  145. <view class="sectionHeader">
  146. <view class="sectionHeaderLeft">
  147. <text class="sectionEn">Live Scene</text>
  148. <text class="sectionTitle">种植现场</text>
  149. </view>
  150. </view>
  151. <view class="liveMediaWrap">
  152. <image
  153. v-if="traceDetail.camera.coverImage"
  154. class="liveCover"
  155. :src="traceDetail.camera.coverImage"
  156. mode="aspectFill"
  157. />
  158. <view v-else class="livePlaceholder">
  159. <view class="livePlayIcon">
  160. <view class="livePlayDot" />
  161. <view class="livePlayDot dot2" />
  162. <view class="livePlayDot dot3" />
  163. </view>
  164. <text class="livePlaceholderMain">实时画面接入中</text>
  165. <text class="livePlaceholderSub">请稍候查看现场情况</text>
  166. </view>
  167. <view class="liveStatusPill" :class="cameraStatus.value">
  168. <view class="liveStatusDot" />
  169. <text>{{ cameraStatusText }}</text>
  170. </view>
  171. </view>
  172. <view class="liveDesc">
  173. <text>{{ traceDetail.camera.desc }}</text>
  174. </view>
  175. </view>
  176. <!-- 批次信息(可选:batch 可能不存在) -->
  177. <view v-if="traceDetail.batch?.exists" class="card cardLevel1 cardBatch">
  178. <view class="sectionHeader">
  179. <view class="sectionHeaderLeft">
  180. <text class="sectionEn">Batch</text>
  181. <text class="sectionTitle">批次信息</text>
  182. </view>
  183. </view>
  184. <!-- 批次信息主体:左右两列布局,左列标签固定宽度,右列内容左对齐 -->
  185. <view class="batchBody">
  186. <view v-if="traceDetail.batch?.no" class="batchRow">
  187. <text class="batchRowLabel">批次号</text>
  188. <text class="batchRowValue">{{ traceDetail.batch.no }}</text>
  189. </view>
  190. <view v-if="traceDetail.batch?.harvestTime" class="batchRow">
  191. <text class="batchRowLabel">生产/采收时间</text>
  192. <text class="batchRowValue">{{ traceDetail.batch.harvestTime }}</text>
  193. </view>
  194. <view v-if="traceDetail.batch?.packTime" class="batchRow">
  195. <text class="batchRowLabel">包装时间</text>
  196. <text class="batchRowValue">{{ traceDetail.batch.packTime }}</text>
  197. </view>
  198. </view>
  199. </view>
  200. <!-- 批次不存在/已下线的说明 -->
  201. <view v-else-if="traceDetail.batch?.statusBadge" class="card soft cardLevel1 cardBatchState">
  202. <view class="sectionHeader">
  203. <view class="sectionHeaderLeft">
  204. <text class="sectionEn">Batch</text>
  205. <text class="sectionTitle">当前批次状态</text>
  206. </view>
  207. <view class="statusBadge" :class="traceDetail.batch.statusBadge.type">
  208. {{ traceDetail.batch.statusBadge.text }}
  209. </view>
  210. </view>
  211. <view class="emptyText">{{ traceDetail.batch?.emptyMessage || '该批次暂不可查询' }}</view>
  212. <view class="btnRow">
  213. <button class="primaryBtn" @click="goBackToPurchase">返回购买渠道</button>
  214. </view>
  215. </view>
  216. <!-- 检测报告:横向卡片列表 -->
  217. <view v-if="traceDetail.batch?.exists" class="card cardLevel2 cardCredential">
  218. <view class="sectionHeader">
  219. <view class="sectionHeaderLeft">
  220. <text class="sectionEn">Report</text>
  221. <text class="sectionTitle">检测报告</text>
  222. </view>
  223. </view>
  224. <!-- 横向滑动卡片列表 -->
  225. <scroll-view
  226. v-if="reportItems.length"
  227. class="reportCardList"
  228. scroll-x
  229. :show-scrollbar="false"
  230. enhanced
  231. >
  232. <view class="reportCardTrack">
  233. <view
  234. v-for="(item, itemIdx) in reportItems"
  235. :key="`report-card-${itemIdx}`"
  236. class="reportCard"
  237. @click="previewDoc('report', getReportGlobalIndex(itemIdx, 0))"
  238. >
  239. <!-- 报告缩略图(带页数角标) -->
  240. <view class="reportCardThumb">
  241. <image
  242. v-if="item.images && item.images.length"
  243. class="reportCardThumbImg"
  244. :src="item.images[0]"
  245. mode="aspectFit"
  246. />
  247. <view v-else class="reportCardThumbEmpty">
  248. <text class="reportCardThumbEmptyText">暂无图片</text>
  249. </view>
  250. <view v-if="item.images && item.images.length > 1" class="reportCardPages">
  251. 共{{ item.images.length }}页
  252. </view>
  253. </view>
  254. <!-- 报告信息 -->
  255. <view class="reportCardMeta">
  256. <view v-if="item.detectDate" class="reportCardRow">
  257. <text class="reportCardLabel">检测日期</text>
  258. <text class="reportCardValue">{{ item.detectDate }}</text>
  259. </view>
  260. <view v-if="item.no" class="reportCardRow">
  261. <text class="reportCardLabel">报告编号</text>
  262. <text class="reportCardValue reportCardValueNo">{{ item.no }}</text>
  263. </view>
  264. </view>
  265. <!-- 点击提示 -->
  266. <text class="reportCardHint">点击查看大图</text>
  267. </view>
  268. </view>
  269. </scroll-view>
  270. <!-- 待补充状态 -->
  271. <view v-else class="docPlaceholder">
  272. <text class="docPlaceholderTitle">检测报告待补充</text>
  273. <text class="docPlaceholderText">
  274. {{ traceDetail.report?.emptyMessage || '检测报告待补充,请耐心等待。' }}
  275. </text>
  276. </view>
  277. </view>
  278. <!-- 合格证:信息摘要 + 缩略图一体化布局 -->
  279. <view v-if="traceDetail.batch?.exists" class="card cardLevel1 cardCredential">
  280. <view class="sectionHeader">
  281. <view class="sectionHeaderLeft">
  282. <text class="sectionEn">Certificate</text>
  283. <text class="sectionTitle">合格证</text>
  284. </view>
  285. </view>
  286. <!-- 信息与缩略图一体化区域 -->
  287. <view class="certSummary">
  288. <!-- 左侧:信息摘要 -->
  289. <view class="certSummaryInfo">
  290. <view v-if="traceDetail.certificate?.issueDate" class="certSummaryRow">
  291. <text class="certSummaryLabel">开具日期</text>
  292. <text class="certSummaryValue">{{ traceDetail.certificate.issueDate }}</text>
  293. </view>
  294. <view v-if="traceDetail.certificate?.no" class="certSummaryRow">
  295. <text class="certSummaryLabel">合格证编号</text>
  296. <text class="certSummaryValue">{{ traceDetail.certificate.no }}</text>
  297. </view>
  298. </view>
  299. <!-- 右侧:缩略图 -->
  300. <view
  301. v-if="traceDetail.certificate?.status === 'uploaded' && certificateImages.length"
  302. class="certThumbContainer"
  303. @click="previewDoc('certificate', 0)"
  304. >
  305. <view class="certThumbWrap">
  306. <image class="certThumbImage" :src="certificateImages[0]" mode="aspectFit" />
  307. </view>
  308. <text class="certPreviewHint">点击查看</text>
  309. </view>
  310. <!-- 待补充状态:仅显示提示 -->
  311. <view v-else class="certPendingHint">
  312. <text class="certPendingText">
  313. {{ traceDetail.certificate?.emptyMessage || '合格证待补充,请耐心等待。' }}
  314. </text>
  315. </view>
  316. </view>
  317. </view>
  318. <!-- 底部柔和收口渐变 -->
  319. <view class="footerFade" />
  320. <!-- 轻量页脚 -->
  321. <view class="traceFooter">
  322. <view class="traceFooterHint">如需帮助,联系客服</view>
  323. <view class="traceFooterPhone" @click="contactCustomer">
  324. <image class="traceFooterPhoneIcon" src="/static/icons/phone.svg" mode="aspectFit" />
  325. <text class="traceFooterPhoneNum">13379508760</text>
  326. </view>
  327. <view class="traceFooterBrand">佳友厚苑 · 安心之选</view>
  328. </view>
  329. </scroll-view>
  330. </view>
  331. </template>
  332. <script setup>
  333. import { computed, ref } from 'vue'
  334. import { onLoad } from '@dcloudio/uni-app'
  335. import {getTraceDetail } from '@/api/base/index.js'
  336. // 页面状态选择(H5 演示用):通过路由参数传入 state 即可切换 mock 场景
  337. // 例如:/pages/trace/detail?state=reportPending
  338. // 加载状态
  339. const loading = ref(false)
  340. // 数据存储
  341. const traceInfo = ref(null)
  342. const routeOptions = ref({})
  343. const mockStateKey = ref('normal')
  344. const MOCK_TRACE_DETAILS = {
  345. normal: {
  346. product: {
  347. name: '',
  348. spec: '',
  349. image: '',
  350. intro: ''
  351. },
  352. farm: {
  353. name: '',
  354. location: '',
  355. image: '',
  356. intro: ''
  357. },
  358. batch: {
  359. exists: false,
  360. statusBadge: { text: '', type: '' },
  361. no: '',
  362. harvestTime: '',
  363. packTime: ''
  364. },
  365. report: {
  366. status: '',
  367. statusBadge: { text: '', type: '' },
  368. // 每份报告独立 detectDate / no / images
  369. items: [
  370. {
  371. detectDate: '',
  372. no: '',
  373. images: ['']
  374. },
  375. {
  376. detectDate: '',
  377. no: '',
  378. images: ['']
  379. },
  380. {
  381. detectDate: '',
  382. no: '',
  383. images: ['']
  384. }
  385. ]
  386. },
  387. certificate: {
  388. status: '',
  389. statusBadge: { text: '', type: '' },
  390. issueDate: '',
  391. no: '',
  392. fileUrl: '',
  393. images: ['']
  394. },
  395. traceConclusionCard: {
  396. title: '溯源结论',
  397. conclusion: '本批次产品来源清晰,已完成安全检测,过程可查',
  398. desc: '检测材料与溯源数据已展示,可继续查看',
  399. passed: true
  400. },
  401. farmTimeline: [
  402. {
  403. time: '2026-03-01',
  404. stage: 'planting',
  405. title: '开始种植',
  406. desc: '本批次产品进入种植管理阶段'
  407. },
  408. {
  409. time: '2026-03-12',
  410. stage: 'care',
  411. title: '生长期养护',
  412. desc: '已完成灌溉、营养管理等日常养护'
  413. },
  414. {
  415. time: '2026-03-20',
  416. stage: 'inspection',
  417. title: '田间巡检',
  418. desc: '已完成长势检查与种植环境巡检'
  419. },
  420. {
  421. time: '2026-03-30',
  422. stage: 'harvest',
  423. title: '成熟采收',
  424. desc: '本批次产品进入成熟采收阶段'
  425. },
  426. {
  427. time: '2026-03-31',
  428. stage: 'test',
  429. title: '安全检测',
  430. desc: '已完成安全检测,相关结果可查'
  431. },
  432. {
  433. time: '2026-04-01',
  434. stage: 'pack',
  435. title: '包装出库',
  436. desc: '已完成包装整理,进入销售流通环节'
  437. }
  438. ],
  439. camera: {
  440. liveUrl: '',
  441. coverImage: '',
  442. status: 'online',
  443. desc: '来自农场实景画面,种植环境真实呈现'
  444. }
  445. },
  446. reportPending: {
  447. product: {
  448. name: '',
  449. spec: '',
  450. image: '',
  451. intro: ''
  452. },
  453. farm: {
  454. name: '',
  455. location: '',
  456. image: null,
  457. intro: ''
  458. },
  459. batch: {
  460. exists: false,
  461. statusBadge: { text: '', type: '' },
  462. no: '',
  463. harvestTime: '',
  464. packTime: ''
  465. },
  466. report: {
  467. status: '',
  468. statusBadge: { text: '', type: '' },
  469. emptyMessage: '',
  470. items: []
  471. },
  472. certificate: {
  473. status: '',
  474. statusBadge: { text: '', type: '' },
  475. issueDate: '',
  476. no: '',
  477. fileUrl: '',
  478. images: []
  479. },
  480. traceConclusionCard: {
  481. title: '溯源结论',
  482. conclusion: '本批次产品来源清晰,已完成安全检测,过程可查',
  483. desc: '检测材料与溯源数据已展示,可继续查看',
  484. passed: true
  485. },
  486. farmTimeline: []
  487. },
  488. certificatePending: {
  489. product: {
  490. name: '',
  491. spec: '',
  492. image: '',
  493. intro: ''
  494. },
  495. farm: {
  496. name: '',
  497. location: '',
  498. image: null,
  499. intro: ''
  500. },
  501. batch: {
  502. exists: false,
  503. statusBadge: { text: '', type: '' },
  504. no: '',
  505. harvestTime: '',
  506. packTime: ''
  507. },
  508. report: {
  509. status: '',
  510. statusBadge: { text: '', type: '' },
  511. items: [
  512. {
  513. detectDate: '',
  514. no: '',
  515. images: []
  516. }
  517. ]
  518. },
  519. certificate: {
  520. status: '',
  521. statusBadge: { text: '', type: '' },
  522. emptyMessage: ''
  523. },
  524. traceConclusionCard: {
  525. title: '溯源结论',
  526. conclusion: '本批次产品来源清晰,已完成安全检测,过程可查',
  527. desc: '检测材料与溯源数据已展示,可继续查看',
  528. passed: true
  529. },
  530. farmTimeline: []
  531. },
  532. batchNotFound: {
  533. product: {
  534. name:'',
  535. spec: '',
  536. image: '',
  537. intro: ''
  538. },
  539. farm: {
  540. name: '',
  541. location: '',
  542. image: null,
  543. intro: ''
  544. },
  545. batch: {
  546. exists: false,
  547. statusBadge: { text: '', type: '' },
  548. emptyMessage: ''
  549. },
  550. report: null,
  551. certificate: null,
  552. traceConclusionCard: {
  553. title: '溯源结论',
  554. conclusion: '本批次产品来源清晰,已完成安全检测,过程可查',
  555. desc: '检测材料与溯源数据已展示,可继续查看',
  556. passed: true
  557. },
  558. farmTimeline: []
  559. },
  560. batchOfflined: {
  561. product: {
  562. name: '',
  563. spec: '',
  564. image: '',
  565. intro: ''
  566. },
  567. farm: {
  568. name: '',
  569. location: '',
  570. image: null,
  571. intro: ''
  572. },
  573. batch: {
  574. exists: false,
  575. statusBadge: { text: '', type: '' },
  576. emptyMessage: ''
  577. },
  578. report: null,
  579. certificate: null,
  580. traceConclusionCard: {
  581. title: '溯源结论',
  582. conclusion: '本批次产品来源清晰,已完成安全检测,过程可查',
  583. desc: '检测材料与溯源数据已展示,可继续查看',
  584. passed: true
  585. },
  586. farmTimeline: []
  587. }
  588. }
  589. function resolveStateKey(opts) {
  590. const raw = (opts?.state || opts?.batchState || opts?.scene || '').toString().trim()
  591. if (!raw) return 'normal'
  592. const allowed = ['normal', 'reportPending', 'certificatePending', 'batchNotFound', 'batchOfflined']
  593. return allowed.includes(raw) ? raw : 'normal'
  594. }
  595. onLoad((opts) => {
  596. // 拿到浏览器路径参数
  597. const fullPath = window.location.pathname
  598. const batchId = fullPath.split('/').filter(Boolean).pop()
  599. // 优先使用路由参数中的 id,其次使用 URL 路径中的 id
  600. const finalId = batchId || 1
  601. loadData(finalId)
  602. routeOptions.value = opts || {}
  603. mockStateKey.value = resolveStateKey(opts || {})
  604. })
  605. const loadData = async (batchId) => {
  606. loading.value = true
  607. try {
  608. // 调用接口
  609. const res = await getTraceDetail(batchId)
  610. traceInfo.value = res.data.data
  611. console.log('接口返回数据:', res)
  612. } catch (error) {
  613. console.error('加载失败', error)
  614. // 错误提示已在拦截器中自动处理
  615. } finally {
  616. // 无论成功失败都关闭 loading
  617. loading.value = false
  618. }
  619. }
  620. const traceDetail = computed(() => {
  621. // 如果没有真实数据,返回 mock 数据
  622. if (!traceInfo.value) {
  623. return MOCK_TRACE_DETAILS[mockStateKey.value] || MOCK_TRACE_DETAILS.normal
  624. }
  625. const data = traceInfo.value
  626. // 解析 certFiles 和 reportFiles(JSON 字符串转数组)
  627. let certFiles = []
  628. let reportFiles = []
  629. try {
  630. if (data.certificate?.certFiles) {
  631. certFiles = JSON.parse(data.certificate.certFiles)
  632. }
  633. } catch (e) {
  634. console.error('解析 certFiles 失败', e)
  635. }
  636. // 构建返回数据结构
  637. return {
  638. product: {
  639. name: data.productName || '',
  640. spec: data.productSpec || '',
  641. image: data.productImage || '',
  642. intro: data.productDesc || ''
  643. },
  644. farm: {
  645. name: data.farmName || '',
  646. location: data.farmRegion || '',
  647. image: data.farmImage || '',
  648. intro: data.farmIntro || ''
  649. },
  650. batch: {
  651. exists: data.status === '2', // status 为 '2' 表示批次正常,已上线
  652. statusBadge: data.status === '2'
  653. ? { text: '检验合格', type: 'ok' }
  654. : { text: '批次不存在', type: 'muted' },
  655. no: data.batchNo || '',
  656. harvestTime: data.produceDate || '',
  657. packTime: data.packageDate || '',
  658. emptyMessage: data.status !== '2' ? '未找到对应溯源批次信息。请确认二维码是否为佳友厚苑正品批次。' : ''
  659. },
  660. report: {
  661. status: data.reports && data.reports.length > 0 ? 'uploaded' : 'pending',
  662. statusBadge: data.reports && data.reports.length > 0
  663. ? { text: '已上传', type: 'ok' }
  664. : { text: '待补充', type: 'wait' },
  665. emptyMessage: '检测报告待补充,上传完成后可查看大图。',
  666. items: (data.reports || []).map(report => {
  667. let images = []
  668. // 处理 reportFiles:可能是字符串 URL、JSON 字符串数组、或已解析的数组
  669. if (report.reportFiles) {
  670. if (typeof report.reportFiles === 'string') {
  671. // 尝试解析 JSON 字符串
  672. try {
  673. const parsed = JSON.parse(report.reportFiles)
  674. // 如果解析成功且是数组,提取 url 字段
  675. if (Array.isArray(parsed)) {
  676. images = parsed.map(item =>
  677. typeof item === 'string' ? item : (item.url || item)
  678. ).filter(Boolean)
  679. } else {
  680. // 解析后不是数组,可能是单个对象
  681. images = [parsed.url || parsed].filter(Boolean)
  682. }
  683. } catch (e) {
  684. // 解析失败,当作普通 URL 字符串处理
  685. images = [report.reportFiles]
  686. }
  687. } else if (Array.isArray(report.reportFiles)) {
  688. // 已经是数组,提取 url 字段
  689. images = report.reportFiles.map(item =>
  690. typeof item === 'string' ? item : (item.url || item)
  691. ).filter(Boolean)
  692. } else if (typeof report.reportFiles === 'object') {
  693. // 单个对象
  694. images = [report.reportFiles.url || report.reportFiles].filter(Boolean)
  695. }
  696. }
  697. return {
  698. detectDate: report.reportDate || '',
  699. no: report.reportNo || '',
  700. images: images
  701. }
  702. })
  703. },
  704. certificate: {
  705. status: data.certificate && certFiles.length > 0 ? 'uploaded' : 'pending',
  706. statusBadge: data.certificate && certFiles.length > 0
  707. ? { text: '已上传', type: 'ok' }
  708. : { text: '待补充', type: 'wait' },
  709. issueDate: data.certificate?.certIssueDate || '',
  710. no: data.certificate?.certNo || '',
  711. fileUrl: certFiles.length > 0 ? certFiles[0].url : '',
  712. images: certFiles.map(f => f.url),
  713. emptyMessage: '合格证待补充,上传完成后可查看大图。'
  714. },
  715. traceConclusionCard: {
  716. title: '溯源结论',
  717. conclusion: '本批次产品来源清晰,已完成安全检测,过程可查',
  718. desc: '检测材料与溯源数据已展示,可继续查看',
  719. passed: true
  720. },
  721. // 时间轴逻辑:演示态使用 mock + 真实态使用真实数据
  722. farmTimeline: (() => {
  723. // 判断是否有真实时间轴数据
  724. const hasRealTimeline = Array.isArray(data.farmTimeline) && data.farmTimeline.length > 0
  725. let timeline = []
  726. if (!hasRealTimeline) {
  727. // 演示态:使用完整 mock 时间轴(用于页面展示验证)
  728. timeline = [
  729. { time: '2026-03-01', stage: 'planting', title: '开始种植', desc: '本批次产品进入种植管理阶段' },
  730. { time: '2026-03-12', stage: 'care', title: '生长期养护', desc: '已完成灌溉、营养管理等日常养护' },
  731. { time: '2026-03-20', stage: 'inspection', title: '田间巡检', desc: '已完成长势检查与种植环境巡检' },
  732. { time: data.produceDate || '', stage: 'harvest', title: '成熟采收', desc: '本批次产品进入成熟采收阶段' },
  733. { time: (data.reports?.[0]?.reportDate) || '', stage: 'test', title: '安全检测', desc: '已完成安全检测,相关结果可查' },
  734. { time: data.packageDate || '', stage: 'pack', title: '包装出库', desc: '已完成包装整理,进入销售流通环节' }
  735. ]
  736. } else {
  737. // 真实态:使用真实数据 + 补充系统节点
  738. timeline = [...data.farmTimeline]
  739. const stageSet = new Set(timeline.map(n => n.stage))
  740. // 成熟采收
  741. if (data.produceDate && !stageSet.has('harvest')) {
  742. timeline.push({
  743. time: data.produceDate,
  744. stage: 'harvest',
  745. title: '成熟采收',
  746. desc: '本批次产品进入成熟采收阶段'
  747. })
  748. }
  749. // 安全检测
  750. if (data.reports && data.reports.length > 0 && !stageSet.has('test')) {
  751. const validDates = data.reports
  752. .map(r => r.reportDate)
  753. .filter(d => d && /^\d{4}-\d{2}-\d{2}/.test(d))
  754. .sort()
  755. if (validDates.length > 0) {
  756. timeline.push({
  757. time: validDates[0],
  758. stage: 'test',
  759. title: '安全检测',
  760. desc: '已完成安全检测,相关结果可查'
  761. })
  762. }
  763. }
  764. // 包装出库
  765. if (data.packageDate && !stageSet.has('pack')) {
  766. timeline.push({
  767. time: data.packageDate,
  768. stage: 'pack',
  769. title: '包装出库',
  770. desc: '已完成包装整理,进入销售流通环节'
  771. })
  772. }
  773. }
  774. // stage 顺序权重
  775. const stageOrder = {
  776. planting: 1,
  777. care: 2,
  778. inspection: 3,
  779. harvest: 4,
  780. test: 5,
  781. pack: 6
  782. }
  783. // 按业务阶段排序,同 stage 内再按时间
  784. timeline.sort((a, b) => {
  785. const orderA = stageOrder[a.stage] || 99
  786. const orderB = stageOrder[b.stage] || 99
  787. // 先按 stage 排序
  788. if (orderA !== orderB) {
  789. return orderA - orderB
  790. }
  791. // 同 stage 再按时间
  792. if (!a.time) return -1
  793. if (!b.time) return 1
  794. return a.time > b.time ? 1 : -1
  795. })
  796. return timeline.slice(0, 6)
  797. })(),
  798. camera: {
  799. liveUrl: data.camera?.liveUrl || '',
  800. coverImage: data.camera?.coverImage || '',
  801. status: data.camera?.status || 'online',
  802. desc: data.camera?.liveUrl
  803. ? '当前为种植现场实时画面,现场环境与种植状态可见'
  804. : '来自农场实景画面,种植环境真实呈现'
  805. }
  806. }
  807. })
  808. // 种植现场状态:三态逻辑
  809. const cameraStatus = computed(() => {
  810. const camera = traceDetail.value?.camera
  811. if (!camera) return 'offline'
  812. const hasLiveUrl = !!camera.liveUrl
  813. // 有 liveUrl 但无封面图 → loading(信号接入中)
  814. // 有 liveUrl 且有封面图 → online(可播放)
  815. // 无 liveUrl → offline
  816. if (hasLiveUrl && !camera.coverImage) {
  817. return 'loading'
  818. }
  819. if (hasLiveUrl || camera.coverImage) {
  820. return 'online'
  821. }
  822. return 'offline'
  823. })
  824. const cameraStatusText = computed(() => {
  825. const map = {
  826. loading: '连接中',
  827. online: '实时在线',
  828. offline: '暂未连接'
  829. }
  830. return map[cameraStatus.value] || '暂未连接'
  831. })
  832. const reportImages = computed(() => {
  833. const items = traceDetail.value?.report?.items
  834. if (Array.isArray(items)) {
  835. return items.flatMap(item => Array.isArray(item.images) ? item.images : [])
  836. }
  837. return []
  838. })
  839. const reportItems = computed(() => {
  840. return traceDetail.value?.report?.items || []
  841. })
  842. // 计算某份报告内某张图在全局 reportImages 扁平数组中的索引
  843. function getReportGlobalIndex(itemIndex, imgIndex) {
  844. const items = reportItems.value
  845. let offset = 0
  846. for (let i = 0; i < itemIndex; i++) {
  847. const imgArr = items[i]?.images
  848. offset += Array.isArray(imgArr) ? imgArr.length : 0
  849. }
  850. return offset + imgIndex
  851. }
  852. const certificateImages = computed(() => {
  853. const c = traceDetail.value?.certificate
  854. if (!c) return []
  855. if (Array.isArray(c.images) && c.images.length) return c.images
  856. return c.fileUrl ? [c.fileUrl] : []
  857. })
  858. function contactCustomer() {
  859. uni.makePhoneCall({
  860. phoneNumber: '13379508760'
  861. })
  862. }
  863. function goBackToPurchase() {
  864. uni.showModal({
  865. title: '返回购买渠道',
  866. content: 'mock 模式:后续可跳转到直播间/小店页面。',
  867. showCancel: true,
  868. confirmText: '确认返回'
  869. })
  870. }
  871. function previewDoc(kind, index) {
  872. const urls = kind === 'report' ? reportImages.value : certificateImages.value
  873. if (!urls.length) {
  874. uni.showToast({ title: '该文件暂未上传', icon: 'none' })
  875. return
  876. }
  877. uni.previewImage({
  878. urls,
  879. current: urls[index] || urls[0]
  880. })
  881. }
  882. </script>
  883. <style scoped>
  884. .page {
  885. position: relative;
  886. min-height: 100vh;
  887. --bg-cream-1: #f8f2e8;
  888. --bg-cream-2: #f3ebdd;
  889. --bg-cream-3: #ece1ce;
  890. --forest-900: #143a2b;
  891. --forest-800: #1f513b;
  892. --forest-700: #2d6849;
  893. --sage-500: #708f7d;
  894. --gold-320: #ccb086;
  895. --gold-260: #dcc4a0;
  896. --text-900: #233126;
  897. --text-700: #3c4d41;
  898. --text-500: #6b776d;
  899. background: linear-gradient(172deg,
  900. #FAF6EF 0%,
  901. #F4EEE3 38%,
  902. #EDF0EC 68%,
  903. #E8EFE9 100%
  904. );
  905. overflow: hidden;
  906. }
  907. /* 背景氛围层:三层光感(前景/中景/背景)+ grain */
  908. .pageDecor {
  909. position: fixed;
  910. inset: 0;
  911. pointer-events: none;
  912. z-index: 0;
  913. }
  914. .pageGlow {
  915. position: absolute;
  916. border-radius: 50%;
  917. filter: blur(14rpx);
  918. }
  919. /* 背景层:左上森林绿微光 */
  920. .glowA {
  921. width: 780rpx;
  922. height: 780rpx;
  923. top: -280rpx;
  924. left: -300rpx;
  925. background: radial-gradient(circle, rgba(26, 84, 58, 0.18) 0%, rgba(20, 72, 50, 0) 70%);
  926. opacity: 0.85;
  927. }
  928. /* 中景层:右下金色暖光 */
  929. .glowB {
  930. width: 720rpx;
  931. height: 720rpx;
  932. right: -280rpx;
  933. top: 480rpx;
  934. background: radial-gradient(circle, rgba(200, 170, 120, 0.2) 0%, rgba(200, 170, 120, 0) 72%);
  935. opacity: 0.9;
  936. }
  937. /* 前景层:顶部柔和高光,塑造空间感 */
  938. .glowC {
  939. width: 600rpx;
  940. height: 360rpx;
  941. top: -60rpx;
  942. left: 50%;
  943. transform: translateX(-50%);
  944. background: radial-gradient(ellipse at 50% 0%, rgba(255, 252, 246, 0.38), transparent 70%);
  945. filter: blur(20rpx);
  946. opacity: 0.7;
  947. }
  948. /* 纹理叠加层 */
  949. .pageGrain {
  950. position: absolute;
  951. inset: 0;
  952. opacity: 0.55;
  953. background:
  954. radial-gradient(900rpx 380rpx at 48% -4%, rgba(255, 255, 255, 0.55), rgba(255, 255, 255, 0)),
  955. radial-gradient(640rpx 400rpx at 104% 48%, rgba(68, 112, 86, 0.09), rgba(68, 112, 86, 0)),
  956. radial-gradient(700rpx 320rpx at -4% 76%, rgba(210, 180, 130, 0.12), rgba(210, 180, 130, 0)),
  957. linear-gradient(180deg, rgba(248, 244, 236, 0.16) 0%, rgba(228, 242, 232, 0.08) 100%);
  958. }
  959. .safeTop {
  960. height: calc(env(safe-area-inset-top));
  961. position: relative;
  962. z-index: 1;
  963. }
  964. /* 轻品牌页头:透明底,不叠在 Hero 上产生「白雾」;层级低于首屏卡 */
  965. .brandPageHeader {
  966. position: relative;
  967. z-index: 2;
  968. display: flex;
  969. align-items: center;
  970. min-height: 72rpx;
  971. padding: 0 40rpx;
  972. box-sizing: border-box;
  973. border-bottom: 1rpx solid rgba(39, 75, 57, 0.05);
  974. background: transparent;
  975. }
  976. .brandPageHeaderInner {
  977. display: flex;
  978. align-items: center;
  979. gap: 10rpx;
  980. width: 100%;
  981. }
  982. .brandPageHeaderName {
  983. font-size: 22rpx;
  984. font-weight: 600;
  985. line-height: 1.15;
  986. letter-spacing: 0.1em;
  987. color: rgba(27, 41, 33, 0.65);
  988. }
  989. .brandPageHeaderDot {
  990. flex-shrink: 0;
  991. width: 4rpx;
  992. height: 4rpx;
  993. border-radius: 50%;
  994. background: rgba(39, 113, 76, 0.4);
  995. }
  996. .brandPageHeaderLabel {
  997. font-size: 20rpx;
  998. font-weight: 400;
  999. line-height: 1.15;
  1000. letter-spacing: 0.06em;
  1001. color: rgba(112, 143, 125, 0.7);
  1002. }
  1003. .hero,
  1004. .content {
  1005. position: relative;
  1006. z-index: 1;
  1007. }
  1008. .hero {
  1009. padding: 0 24rpx;
  1010. }
  1011. /* 完整首屏大卡:与品牌页头分离,避免负 margin 上提后被页头遮盖 */
  1012. .heroCard {
  1013. border-radius: 42rpx;
  1014. overflow: hidden;
  1015. margin-top: 20rpx;
  1016. position: relative;
  1017. z-index: 4;
  1018. box-shadow:
  1019. 0 48rpx 120rpx rgba(10, 36, 24, 0.2),
  1020. 0 16rpx 48rpx rgba(10, 36, 24, 0.1),
  1021. 0 2rpx 0 rgba(255, 255, 255, 0.6) inset;
  1022. backdrop-filter: blur(4px);
  1023. }
  1024. /* Hero 视觉区:多层次光感空间,顶部高光 + 景深 + 底部暗化 */
  1025. .heroBg {
  1026. position: relative;
  1027. width: 100%;
  1028. height: 0;
  1029. padding-bottom: 56.25%;
  1030. overflow: hidden;
  1031. background: linear-gradient(160deg, rgba(20, 74, 52, 0.72), rgba(205, 177, 129, 0.3) 70%, rgba(243, 232, 214, 0.36));
  1032. }
  1033. /* 前景层光晕:右下淡金色微光(提升质感) */
  1034. .heroBg::before {
  1035. content: '';
  1036. position: absolute;
  1037. right: -100rpx;
  1038. bottom: -72rpx;
  1039. width: 520rpx;
  1040. height: 280rpx;
  1041. border-radius: 50%;
  1042. background: radial-gradient(ellipse at 42% 38%, rgba(252, 244, 228, 0.18), transparent 68%);
  1043. pointer-events: none;
  1044. z-index: 2;
  1045. }
  1046. .heroImage {
  1047. position: absolute;
  1048. inset: 0;
  1049. width: 100%;
  1050. height: 100%;
  1051. display: block;
  1052. z-index: 1;
  1053. }
  1054. .heroBrandAnchor {
  1055. position: absolute;
  1056. right: -48rpx;
  1057. bottom: -14rpx;
  1058. z-index: 2;
  1059. pointer-events: none;
  1060. }
  1061. .heroBrandAnchorText {
  1062. font-size: 164rpx;
  1063. font-weight: 760;
  1064. letter-spacing: 0.1em;
  1065. line-height: 1;
  1066. color: rgba(248, 241, 227, 0.045);
  1067. transform: rotate(-8deg);
  1068. }
  1069. /* Hero 遮罩层:提亮后仍保留轻微底部过渡,避免压住真实商品图 */
  1070. .heroMask {
  1071. position: absolute;
  1072. inset: 0;
  1073. z-index: 3;
  1074. background:
  1075. radial-gradient(ellipse 180% 60% at 50% 0%, rgba(255, 252, 248, 0.26), transparent 68%),
  1076. radial-gradient(480rpx 240rpx at 90% 8%, rgba(252, 248, 240, 0.2), transparent 62%),
  1077. radial-gradient(700rpx 360rpx at 8% 65%, rgba(6, 24, 16, 0.12), transparent 58%),
  1078. radial-gradient(560rpx 300rpx at 88% 80%, rgba(205, 177, 129, 0.06), transparent 62%),
  1079. linear-gradient(180deg,
  1080. rgba(8, 28, 18, 0) 0%,
  1081. rgba(8, 28, 18, 0) 62%,
  1082. rgba(10, 36, 24, 0.05) 80%,
  1083. rgba(8, 26, 18, 0.1) 92%,
  1084. rgba(248, 244, 238, 0.12) 100%
  1085. );
  1086. }
  1087. /* 商品信息面板:玻璃浮层效果,负margin上浮 + backdrop-filter */
  1088. .heroInfoPanel {
  1089. background: rgba(252, 248, 242, 0.88);
  1090. backdrop-filter: blur(14px);
  1091. -webkit-backdrop-filter: blur(14px);
  1092. position: relative;
  1093. }
  1094. /* 与 Hero 底部自然过渡 */
  1095. .heroInfoPanel::before {
  1096. content: '';
  1097. position: absolute;
  1098. top: -56rpx;
  1099. left: 0;
  1100. right: 0;
  1101. height: 56rpx;
  1102. background: linear-gradient(180deg,
  1103. rgba(252, 248, 242, 0) 0%,
  1104. rgba(252, 248, 242, 0.88) 100%
  1105. );
  1106. pointer-events: none;
  1107. z-index: 1;
  1108. }
  1109. .infoPanelInner {
  1110. position: relative;
  1111. z-index: 2;
  1112. padding: 32rpx 30rpx 30rpx;
  1113. }
  1114. /* 与下方农场/批次等卡片共用 .sectionHeader / .sectionHeaderLeft 样式 */
  1115. .infoPanelBody {
  1116. padding-top: 0;
  1117. }
  1118. .infoPanelName {
  1119. display: block;
  1120. font-size: 30rpx;
  1121. font-weight: 680;
  1122. line-height: 1.4;
  1123. letter-spacing: 0.01em;
  1124. color: rgba(27, 41, 33, 0.92);
  1125. }
  1126. .infoPanelSpec {
  1127. display: inline-flex;
  1128. align-items: center;
  1129. gap: 10rpx;
  1130. margin-top: 14rpx;
  1131. padding: 8rpx 16rpx;
  1132. border-radius: 20rpx;
  1133. background: rgba(39, 113, 76, 0.06);
  1134. border: 1rpx solid rgba(39, 113, 76, 0.08);
  1135. }
  1136. .specDot {
  1137. width: 6rpx;
  1138. height: 6rpx;
  1139. border-radius: 50%;
  1140. background: rgba(39, 113, 76, 0.5);
  1141. }
  1142. .specText {
  1143. font-size: 20rpx;
  1144. font-weight: 500;
  1145. color: rgba(54, 66, 57, 0.85);
  1146. }
  1147. .infoPanelIntro {
  1148. margin-top: 16rpx;
  1149. padding-top: 16rpx;
  1150. border-top: 1rpx solid rgba(39, 75, 57, 0.06);
  1151. font-size: 23rpx;
  1152. line-height: 1.8;
  1153. color: rgba(54, 66, 57, 0.75);
  1154. }
  1155. .content {
  1156. padding: 20rpx 0 40rpx;
  1157. }
  1158. /* 基础卡片:半透明玻璃质感 */
  1159. .card {
  1160. margin: 0 24rpx 22rpx;
  1161. padding: 30rpx 28rpx;
  1162. border-radius: 36rpx;
  1163. background: rgba(252, 250, 244, 0.76);
  1164. box-shadow:
  1165. 0 16rpx 40rpx rgba(38, 41, 32, 0.07),
  1166. 0 1rpx 0 rgba(255, 255, 255, 0.5) inset;
  1167. backdrop-filter: blur(8px);
  1168. -webkit-backdrop-filter: blur(8px);
  1169. }
  1170. .card.soft {
  1171. background: rgba(252, 248, 244, 0.6);
  1172. }
  1173. /* Hero 之后第一张卡:适度上浮 */
  1174. .cardHeroFirst {
  1175. margin-top: -28rpx;
  1176. position: relative;
  1177. z-index: 3;
  1178. }
  1179. /* 层级 0:与 Hero 同级,主卡(商品面板) */
  1180. .cardLevel0 {
  1181. box-shadow:
  1182. 0 48rpx 120rpx rgba(10, 36, 24, 0.16),
  1183. 0 16rpx 48rpx rgba(10, 36, 24, 0.08),
  1184. 0 1rpx 0 rgba(255, 255, 255, 0.6) inset;
  1185. }
  1186. /* 层级 1:次要信息卡,阴影中等 */
  1187. .cardLevel1 {
  1188. box-shadow:
  1189. 0 20rpx 50rpx rgba(30, 34, 24, 0.09),
  1190. 0 8rpx 24rpx rgba(30, 34, 24, 0.05),
  1191. 0 1rpx 0 rgba(255, 255, 255, 0.55) inset;
  1192. }
  1193. /* 层级 2:次次要信息卡,阴影更轻 */
  1194. .cardLevel2 {
  1195. box-shadow:
  1196. 0 14rpx 36rpx rgba(36, 38, 28, 0.065),
  1197. 0 1rpx 0 rgba(255, 255, 255, 0.5) inset;
  1198. }
  1199. /* 层级 3:底部辅助信息卡,最轻 */
  1200. .cardLevel3 {
  1201. box-shadow:
  1202. 0 8rpx 22rpx rgba(42, 44, 30, 0.04),
  1203. 0 1rpx 0 rgba(255, 255, 255, 0.44) inset;
  1204. }
  1205. .cardFarm {
  1206. background: rgba(250, 248, 242, 0.82);
  1207. }
  1208. .cardBatch {
  1209. background: rgba(251, 248, 242, 0.8);
  1210. }
  1211. .cardCredential {
  1212. background: rgba(252, 248, 240, 0.84);
  1213. }
  1214. .cardBatchState {
  1215. background: rgba(252, 248, 244, 0.74);
  1216. }
  1217. /* 检测结论卡:结论型卡片,比普通资料卡更有结论感 */
  1218. .cardConclusion {
  1219. background: rgba(252, 250, 244, 0.82);
  1220. }
  1221. .conclusionBody {
  1222. padding-top: 6rpx;
  1223. }
  1224. .conclusionLead {
  1225. display: block;
  1226. font-size: 32rpx;
  1227. font-weight: 680;
  1228. line-height: 1.5;
  1229. letter-spacing: 0.01em;
  1230. color: rgba(24, 38, 30, 0.96);
  1231. margin-bottom: 14rpx;
  1232. }
  1233. .conclusionDesc {
  1234. display: block;
  1235. font-size: 20rpx;
  1236. font-weight: 400;
  1237. line-height: 1.9;
  1238. color: rgba(84, 106, 93, 0.5);
  1239. }
  1240. /* 农事时间轴:关键种植过程 */
  1241. .cardTimeline {
  1242. background: rgba(252, 250, 244, 0.82);
  1243. }
  1244. .timelineList {
  1245. padding-top: 4rpx;
  1246. }
  1247. .timelineItem {
  1248. display: flex;
  1249. padding-bottom: 24rpx;
  1250. }
  1251. .timelineItem:last-child {
  1252. padding-bottom: 0;
  1253. }
  1254. .timelineLeft {
  1255. display: flex;
  1256. flex-direction: column;
  1257. align-items: center;
  1258. width: 40rpx;
  1259. flex-shrink: 0;
  1260. }
  1261. .timelineDot {
  1262. width: 12rpx;
  1263. height: 12rpx;
  1264. border-radius: 50%;
  1265. background: rgba(39, 113, 76, 0.5);
  1266. flex-shrink: 0;
  1267. }
  1268. .timelineLine {
  1269. width: 1rpx;
  1270. flex: 1;
  1271. background: rgba(39, 113, 76, 0.2);
  1272. margin-top: 8rpx;
  1273. }
  1274. .timelineRight {
  1275. flex: 1;
  1276. padding-left: 24rpx;
  1277. }
  1278. .timelineTime {
  1279. display: block;
  1280. font-size: 19rpx;
  1281. font-weight: 400;
  1282. color: rgba(84, 106, 93, 0.5);
  1283. margin-bottom: 6rpx;
  1284. letter-spacing: 0.02em;
  1285. }
  1286. .timelineTitle {
  1287. display: block;
  1288. font-size: 27rpx;
  1289. font-weight: 600;
  1290. color: rgba(27, 41, 33, 0.88);
  1291. margin-bottom: 6rpx;
  1292. line-height: 1.3;
  1293. }
  1294. .timelineDesc {
  1295. display: block;
  1296. font-size: 21rpx;
  1297. font-weight: 400;
  1298. line-height: 1.7;
  1299. color: rgba(84, 106, 93, 0.75);
  1300. }
  1301. /* 关键节点(安全检测):轻强调 */
  1302. .timelineItem.is-key .timelineDot {
  1303. background: rgba(39, 113, 76, 0.7);
  1304. }
  1305. .timelineItem.is-key .timelineTitle {
  1306. font-weight: 600;
  1307. color: rgba(27, 41, 33, 0.9);
  1308. }
  1309. /* 当前节点(包装出库):主高亮 */
  1310. .timelineItem.is-current .timelineDot {
  1311. background: #2f7d55;
  1312. box-shadow: 0 0 0 6rpx rgba(47, 125, 85, 0.12);
  1313. }
  1314. .timelineItem.is-current .timelineTitle {
  1315. font-weight: 700;
  1316. color: rgba(20, 60, 40, 0.95);
  1317. }
  1318. /* 底部可信来源说明 */
  1319. .timelineFootnote {
  1320. margin-top: 20rpx;
  1321. font-size: 20rpx;
  1322. color: rgba(84, 106, 93, 0.45);
  1323. line-height: 1.6;
  1324. }
  1325. /* 种植现场实时画面模块 */
  1326. .cardLive {
  1327. background: rgba(252, 250, 244, 0.82);
  1328. }
  1329. .liveMediaWrap {
  1330. position: relative;
  1331. width: 100%;
  1332. height: 380rpx;
  1333. border-radius: 20rpx;
  1334. overflow: hidden;
  1335. background: linear-gradient(
  1336. 160deg,
  1337. rgba(39, 113, 76, 0.04) 0%,
  1338. rgba(39, 113, 76, 0.015) 50%,
  1339. rgba(39, 113, 76, 0.05) 100%
  1340. );
  1341. border: 1rpx solid rgba(39, 75, 57, 0.08);
  1342. box-shadow: inset 0 2rpx 12rpx rgba(39, 113, 76, 0.04);
  1343. }
  1344. .liveCover {
  1345. width: 100%;
  1346. height: 100%;
  1347. }
  1348. .livePlaceholder {
  1349. width: 100%;
  1350. height: 100%;
  1351. display: flex;
  1352. flex-direction: column;
  1353. align-items: center;
  1354. justify-content: center;
  1355. background: transparent;
  1356. }
  1357. .livePlayIcon {
  1358. display: flex;
  1359. align-items: center;
  1360. gap: 8rpx;
  1361. margin-bottom: 20rpx;
  1362. }
  1363. .livePlayDot {
  1364. width: 8rpx;
  1365. height: 8rpx;
  1366. border-radius: 50%;
  1367. background: rgba(39, 113, 76, 0.35);
  1368. animation: livePulse 1.4s ease-in-out infinite;
  1369. }
  1370. .livePlayDot.dot2 {
  1371. animation-delay: 0.2s;
  1372. }
  1373. .livePlayDot.dot3 {
  1374. animation-delay: 0.4s;
  1375. }
  1376. @keyframes livePulse {
  1377. 0%, 100% {
  1378. opacity: 0.3;
  1379. transform: scale(0.8);
  1380. }
  1381. 50% {
  1382. opacity: 1;
  1383. transform: scale(1.1);
  1384. }
  1385. }
  1386. .livePlaceholderMain {
  1387. font-size: 26rpx;
  1388. font-weight: 500;
  1389. color: rgba(39, 113, 76, 0.5);
  1390. margin-bottom: 10rpx;
  1391. }
  1392. .livePlaceholderSub {
  1393. font-size: 20rpx;
  1394. color: rgba(84, 106, 93, 0.35);
  1395. }
  1396. .liveStatusPill {
  1397. position: absolute;
  1398. top: 20rpx;
  1399. right: 20rpx;
  1400. display: flex;
  1401. align-items: center;
  1402. gap: 8rpx;
  1403. padding: 8rpx 18rpx;
  1404. border-radius: 30rpx;
  1405. font-size: 20rpx;
  1406. font-weight: 600;
  1407. }
  1408. .liveStatusDot {
  1409. width: 10rpx;
  1410. height: 10rpx;
  1411. border-radius: 50%;
  1412. flex-shrink: 0;
  1413. }
  1414. .liveStatusPill.online {
  1415. background: rgba(47, 125, 85, 0.1);
  1416. color: #2f7d55;
  1417. }
  1418. .liveStatusPill.online .liveStatusDot {
  1419. background: #2f7d55;
  1420. animation: statusPulse 2s ease-in-out infinite;
  1421. }
  1422. @keyframes statusPulse {
  1423. 0%, 100% {
  1424. opacity: 1;
  1425. box-shadow: 0 0 0 0 rgba(47, 125, 85, 0.4);
  1426. }
  1427. 50% {
  1428. opacity: 0.7;
  1429. box-shadow: 0 0 0 4rpx rgba(47, 125, 85, 0);
  1430. }
  1431. }
  1432. .liveStatusPill.offline {
  1433. background: rgba(84, 106, 93, 0.08);
  1434. color: rgba(84, 106, 93, 0.55);
  1435. }
  1436. .liveStatusPill.offline .liveStatusDot {
  1437. background: rgba(84, 106, 93, 0.4);
  1438. }
  1439. .liveDesc {
  1440. margin-top: 16rpx;
  1441. font-size: 20rpx;
  1442. color: rgba(84, 106, 93, 0.5);
  1443. line-height: 1.6;
  1444. }
  1445. /* ================================================
  1446. 微动效:让页面"活起来"
  1447. ================================================ */
  1448. /* Hero 进入:上浮 + 淡入 */
  1449. @keyframes heroEnter {
  1450. 0% {
  1451. opacity: 0;
  1452. transform: translateY(28rpx);
  1453. }
  1454. 100% {
  1455. opacity: 1;
  1456. transform: translateY(0);
  1457. }
  1458. }
  1459. .heroCard {
  1460. animation: heroEnter 0.6s cubic-bezier(0.22, 0.78, 0.35, 1) both;
  1461. }
  1462. /* 卡片依次入场:延迟递增,制造节奏感 */
  1463. @keyframes cardEnter {
  1464. 0% {
  1465. opacity: 0;
  1466. transform: translateY(20rpx);
  1467. }
  1468. 100% {
  1469. opacity: 1;
  1470. transform: translateY(0);
  1471. }
  1472. }
  1473. /* 商品面板:稍晚于 Hero */
  1474. .heroInfoPanel {
  1475. animation: cardEnter 0.5s cubic-bezier(0.22, 0.78, 0.35, 1) 0.15s both;
  1476. }
  1477. /* 检测结论卡 */
  1478. .cardConclusion {
  1479. animation: cardEnter 0.5s cubic-bezier(0.22, 0.78, 0.35, 1) 0.2s both;
  1480. }
  1481. /* 农场卡 */
  1482. .cardFarm {
  1483. animation: cardEnter 0.5s cubic-bezier(0.22, 0.78, 0.35, 1) 0.25s both;
  1484. }
  1485. /* 批次卡 */
  1486. .cardBatch {
  1487. animation: cardEnter 0.5s cubic-bezier(0.22, 0.78, 0.35, 1) 0.35s both;
  1488. }
  1489. /* 凭证卡 */
  1490. .cardCredential {
  1491. animation: cardEnter 0.5s cubic-bezier(0.22, 0.78, 0.35, 1) 0.45s both;
  1492. }
  1493. /* ============================
  1494. 检测报告横向卡片列表
  1495. ============================ */
  1496. .reportCardList {
  1497. width: 100%;
  1498. overflow-x: auto;
  1499. overflow-y: hidden;
  1500. -webkit-overflow-scrolling: touch;
  1501. white-space: nowrap;
  1502. }
  1503. .reportCardList::-webkit-scrollbar {
  1504. display: none;
  1505. }
  1506. .reportCardTrack {
  1507. display: inline-flex;
  1508. flex-wrap: nowrap;
  1509. align-items: stretch;
  1510. gap: 16rpx;
  1511. padding: 6rpx 4rpx 10rpx;
  1512. /* 关键:让子项不受 flex 压缩,自然撑开形成横向溢出 */
  1513. width: max-content;
  1514. }
  1515. /* 单张报告卡片 */
  1516. .reportCard {
  1517. flex-shrink: 0;
  1518. width: 220rpx;
  1519. background: #FAFBF9;
  1520. border: 1rpx solid rgba(39, 75, 57, 0.09);
  1521. border-radius: 14rpx;
  1522. padding: 12rpx 12rpx 10rpx;
  1523. display: flex;
  1524. flex-direction: column;
  1525. align-items: stretch;
  1526. box-shadow: 0 2rpx 8rpx rgba(39, 75, 57, 0.04);
  1527. }
  1528. /* 缩略图(带页数角标覆盖层) */
  1529. .reportCardThumb {
  1530. position: relative;
  1531. width: 100%;
  1532. height: 196rpx;
  1533. background: rgba(39, 75, 57, 0.03);
  1534. border-radius: 10rpx;
  1535. overflow: hidden;
  1536. display: flex;
  1537. align-items: center;
  1538. justify-content: center;
  1539. margin-bottom: 10rpx;
  1540. }
  1541. .reportCardThumbImg {
  1542. width: 100%;
  1543. height: 100%;
  1544. }
  1545. .reportCardThumbEmpty {
  1546. width: 100%;
  1547. height: 100%;
  1548. display: flex;
  1549. align-items: center;
  1550. justify-content: center;
  1551. }
  1552. .reportCardThumbEmptyText {
  1553. font-size: 18rpx;
  1554. color: rgba(39, 75, 57, 0.3);
  1555. }
  1556. /* 页数角标:绝对定位在缩略图右下角 */
  1557. .reportCardPages {
  1558. position: absolute;
  1559. bottom: 8rpx;
  1560. right: 8rpx;
  1561. font-size: 15rpx;
  1562. color: rgba(39, 75, 57, 0.65);
  1563. background: rgba(250, 251, 249, 0.88);
  1564. border: 1rpx solid rgba(39, 75, 57, 0.1);
  1565. border-radius: 6rpx;
  1566. padding: 2rpx 8rpx;
  1567. line-height: 1.4;
  1568. }
  1569. /* 报告元信息 */
  1570. .reportCardMeta {
  1571. flex: 1;
  1572. margin-bottom: 6rpx;
  1573. }
  1574. .reportCardRow {
  1575. display: flex;
  1576. justify-content: space-between;
  1577. align-items: baseline;
  1578. gap: 4rpx;
  1579. margin-bottom: 4rpx;
  1580. }
  1581. .reportCardLabel {
  1582. font-size: 16rpx;
  1583. color: rgba(84, 106, 93, 0.6);
  1584. flex-shrink: 0;
  1585. }
  1586. .reportCardValue {
  1587. font-size: 16rpx;
  1588. color: rgba(68, 82, 72, 0.85);
  1589. font-weight: 460;
  1590. text-align: right;
  1591. word-break: break-all;
  1592. }
  1593. .reportCardValueNo {
  1594. font-size: 14rpx;
  1595. color: rgba(68, 82, 72, 0.72);
  1596. }
  1597. /* 点击提示 */
  1598. .reportCardHint {
  1599. font-size: 14rpx;
  1600. color: rgba(39, 75, 57, 0.3);
  1601. text-align: center;
  1602. margin-top: auto;
  1603. }
  1604. /* 底部辅助卡 */
  1605. .cardBatchState {
  1606. animation: cardEnter 0.5s cubic-bezier(0.22, 0.78, 0.35, 1) 0.55s both;
  1607. }
  1608. /* 卡片触摸反馈:阴影增强(模拟按压感) */
  1609. .card:active {
  1610. transition: box-shadow 0.15s ease, transform 0.15s ease;
  1611. }
  1612. /* 触摸时轻微缩小,增加真实感 */
  1613. .card:active:not(.heroInfoPanel) {
  1614. transform: scale(0.99);
  1615. }
  1616. .sectionHeader {
  1617. display: flex;
  1618. align-items: center;
  1619. justify-content: space-between;
  1620. gap: 18rpx;
  1621. margin-bottom: 20rpx;
  1622. }
  1623. .sectionHeaderLeft {
  1624. display: flex;
  1625. flex-direction: column;
  1626. gap: 10rpx;
  1627. position: relative;
  1628. padding-bottom: 8rpx;
  1629. }
  1630. .sectionHeaderLeft::after {
  1631. content: '';
  1632. position: absolute;
  1633. left: 0;
  1634. bottom: 0;
  1635. width: 78rpx;
  1636. height: 2rpx;
  1637. border-radius: 999rpx;
  1638. background: linear-gradient(90deg, rgba(46, 82, 63, 0.3), rgba(203, 176, 128, 0.26), rgba(46, 82, 63, 0));
  1639. pointer-events: none;
  1640. }
  1641. .sectionEn {
  1642. font-size: 15rpx;
  1643. font-weight: 500;
  1644. letter-spacing: 0.24em;
  1645. text-transform: uppercase;
  1646. color: rgba(84, 106, 93, 0.76);
  1647. }
  1648. .sectionTitle {
  1649. font-size: 27rpx;
  1650. font-weight: 620;
  1651. color: rgba(60, 72, 64, 0.72);
  1652. }
  1653. .statusBadge {
  1654. font-size: 18rpx;
  1655. font-weight: 620;
  1656. padding: 9rpx 15rpx;
  1657. border-radius: 999rpx;
  1658. white-space: nowrap;
  1659. border: 1rpx solid transparent;
  1660. box-shadow: 0 1rpx 0 rgba(255, 255, 255, 0.34) inset;
  1661. }
  1662. .statusBadge.ok {
  1663. color: #1d5d40;
  1664. background: rgba(39, 113, 76, 0.12);
  1665. border-color: rgba(39, 113, 76, 0.2);
  1666. }
  1667. .statusBadge.wait {
  1668. color: #715726;
  1669. background: rgba(203, 171, 112, 0.2);
  1670. border-color: rgba(203, 171, 112, 0.26);
  1671. }
  1672. .statusBadge.warning {
  1673. color: #8a5e22;
  1674. background: rgba(214, 174, 95, 0.22);
  1675. border-color: rgba(214, 174, 95, 0.28);
  1676. }
  1677. .statusBadge.muted {
  1678. color: #677069;
  1679. background: rgba(126, 137, 128, 0.14);
  1680. border-color: rgba(126, 137, 128, 0.2);
  1681. }
  1682. /* =============================================
  1683. 商品信息面板专用信任认证徽章:强化图标主视觉、文字辅助
  1684. ============================================= */
  1685. /* 整体:认证章风格,低饱和细边框,无交互感 */
  1686. .trustBadge {
  1687. display: inline-flex;
  1688. align-items: center;
  1689. gap: 7rpx;
  1690. padding: 9rpx 18rpx 9rpx 15rpx;
  1691. border-radius: 999rpx;
  1692. border: 1rpx solid rgba(39, 113, 76, 0.22);
  1693. background: rgba(39, 113, 76, 0.06);
  1694. box-shadow:
  1695. 0 2rpx 6rpx rgba(39, 113, 76, 0.06),
  1696. 0 1rpx 0 rgba(255, 255, 255, 0.55) inset;
  1697. white-space: nowrap;
  1698. cursor: default;
  1699. pointer-events: none;
  1700. user-select: none;
  1701. }
  1702. /* 双圆环认证图标:20×20 viewBox,图标为第一视觉焦点 */
  1703. .trustBadgeIcon {
  1704. width: 30rpx;
  1705. height: 30rpx;
  1706. flex-shrink: 0;
  1707. vertical-align: middle;
  1708. display: inline-block;
  1709. }
  1710. /* 文案:品牌深绿,字重略降,视觉权重低于图标 */
  1711. .trustBadgeText {
  1712. font-size: 18rpx;
  1713. font-weight: 500;
  1714. letter-spacing: 0.05em;
  1715. color: rgba(27, 71, 42, 0.68);
  1716. }
  1717. .kv {
  1718. display: flex;
  1719. flex-direction: column;
  1720. gap: 16rpx;
  1721. }
  1722. .kvRow {
  1723. display: flex;
  1724. align-items: baseline;
  1725. justify-content: space-between;
  1726. gap: 16rpx;
  1727. }
  1728. .kvKey {
  1729. font-size: 22rpx;
  1730. font-weight: 480;
  1731. color: rgba(54, 68, 58, 0.62);
  1732. }
  1733. .kvVal {
  1734. font-size: 24rpx;
  1735. font-weight: 660;
  1736. text-align: right;
  1737. color: rgba(27, 41, 33, 0.92);
  1738. }
  1739. /* 农场主体:左图右文,右栏与图片垂直居中,名称/地址成组 */
  1740. .farmBody {
  1741. display: flex;
  1742. align-items: center;
  1743. gap: 24rpx;
  1744. margin-bottom: 18rpx;
  1745. }
  1746. .farmImageWrap {
  1747. flex-shrink: 0;
  1748. width: 180rpx;
  1749. height: 136rpx;
  1750. border-radius: 22rpx;
  1751. overflow: hidden;
  1752. position: relative;
  1753. box-shadow:
  1754. 0 12rpx 28rpx rgba(24, 41, 30, 0.14),
  1755. 0 2rpx 0 rgba(255, 255, 255, 0.3) inset;
  1756. }
  1757. .farmImage {
  1758. width: 100%;
  1759. height: 100%;
  1760. display: block;
  1761. background: linear-gradient(160deg, rgba(234, 245, 240, 0.9), rgba(220, 238, 228, 0.85));
  1762. }
  1763. .farmMeta {
  1764. flex: 1;
  1765. display: flex;
  1766. flex-direction: column;
  1767. justify-content: center;
  1768. min-width: 0;
  1769. gap: 10rpx;
  1770. }
  1771. .farmName {
  1772. font-size: 28rpx;
  1773. font-weight: 680;
  1774. line-height: 1.45;
  1775. letter-spacing: 0.01em;
  1776. color: rgba(27, 41, 33, 0.94);
  1777. }
  1778. /* 农场地址:普通文本样式,弱于标题;多行时图标与首行对齐 */
  1779. .farmLocation {
  1780. display: flex;
  1781. align-items: flex-start;
  1782. gap: 8rpx;
  1783. }
  1784. /* 定位小图标:略弱、与首行文字视觉对齐 */
  1785. .locationIcon {
  1786. width: 18rpx;
  1787. height: 18rpx;
  1788. flex-shrink: 0;
  1789. margin-top: 6rpx;
  1790. opacity: 0.42;
  1791. }
  1792. .locationText {
  1793. flex: 1;
  1794. min-width: 0;
  1795. font-size: 21rpx;
  1796. font-weight: 400;
  1797. color: rgba(84, 106, 93, 0.82);
  1798. line-height: 1.55;
  1799. letter-spacing: 0.02em;
  1800. }
  1801. /* 农场介绍:融入主体,使用分隔线区分,与商品简介风格统一 */
  1802. .farmIntro {
  1803. padding-top: 18rpx;
  1804. border-top: 1rpx solid rgba(39, 75, 57, 0.06);
  1805. font-size: 23rpx;
  1806. line-height: 1.86;
  1807. color: rgba(54, 68, 58, 0.76);
  1808. }
  1809. .emptyText {
  1810. margin-top: 10rpx;
  1811. font-size: 24rpx;
  1812. line-height: 1.62;
  1813. color: rgba(16, 24, 19, 0.72);
  1814. }
  1815. .btnRow {
  1816. margin-top: 22rpx;
  1817. }
  1818. /* 合格证紧凑缩略图 */
  1819. /* ============================
  1820. 合格证:信息摘要 + 缩略图一体化布局
  1821. ============================ */
  1822. .certSummary {
  1823. display: flex;
  1824. align-items: flex-start;
  1825. gap: 24rpx;
  1826. }
  1827. .certSummaryInfo {
  1828. flex: 1;
  1829. display: flex;
  1830. flex-direction: column;
  1831. gap: 14rpx;
  1832. min-width: 0;
  1833. }
  1834. .certSummaryRow {
  1835. display: flex;
  1836. flex-direction: column;
  1837. gap: 4rpx;
  1838. }
  1839. .certSummaryLabel {
  1840. font-size: 20rpx;
  1841. font-weight: 500;
  1842. color: rgba(39, 75, 57, 0.55);
  1843. letter-spacing: 0.02em;
  1844. }
  1845. .certSummaryValue {
  1846. font-size: 25rpx;
  1847. font-weight: 580;
  1848. color: rgba(27, 41, 33, 0.9);
  1849. line-height: 1.4;
  1850. }
  1851. .certThumbContainer {
  1852. flex: 0 0 auto;
  1853. display: flex;
  1854. flex-direction: column;
  1855. align-items: center;
  1856. gap: 4rpx;
  1857. }
  1858. .certThumbContainer .certThumbWrap {
  1859. /* 与左侧两行摘要区总高约齐平:缩略图 + gap + 提示行 */
  1860. width: 88rpx;
  1861. height: 120rpx;
  1862. border-radius: 8rpx;
  1863. overflow: hidden;
  1864. background: #fff;
  1865. border: 1rpx solid rgba(200, 180, 140, 0.22);
  1866. box-shadow: 0 2rpx 8rpx rgba(30, 36, 26, 0.06);
  1867. }
  1868. .certThumbContainer .certThumbImage {
  1869. width: 100%;
  1870. height: 100%;
  1871. display: block;
  1872. }
  1873. .certPreviewHint {
  1874. font-size: 16rpx;
  1875. line-height: 1.15;
  1876. color: rgba(39, 75, 57, 0.4);
  1877. letter-spacing: 0.01em;
  1878. }
  1879. .certPendingHint {
  1880. flex: 1;
  1881. display: flex;
  1882. align-items: center;
  1883. padding: 12rpx 0;
  1884. }
  1885. .certPendingText {
  1886. font-size: 21rpx;
  1887. color: rgba(39, 75, 57, 0.5);
  1888. line-height: 1.5;
  1889. }
  1890. /* 旧样式保留但隐藏(兼容其他模块) */
  1891. .certPreviewCompact {
  1892. display: flex;
  1893. flex-direction: column;
  1894. align-items: center;
  1895. margin-top: 24rpx;
  1896. }
  1897. .docPlaceholder {
  1898. margin-top: 16rpx;
  1899. padding: 20rpx;
  1900. border-radius: 26rpx;
  1901. background: linear-gradient(160deg, rgba(244, 237, 224, 0.82), rgba(237, 244, 235, 0.72));
  1902. border: 1rpx solid rgba(201, 171, 120, 0.22);
  1903. }
  1904. .docPlaceholderTitle {
  1905. display: block;
  1906. margin-bottom: 12rpx;
  1907. font-size: 22rpx;
  1908. font-weight: 650;
  1909. color: rgba(35, 93, 64, 0.92);
  1910. }
  1911. .docPlaceholderText {
  1912. display: block;
  1913. font-size: 22rpx;
  1914. line-height: 1.75;
  1915. color: rgba(59, 71, 62, 0.82);
  1916. }
  1917. .docBlock {
  1918. margin-top: 14rpx;
  1919. padding: 18rpx;
  1920. border-radius: 22rpx;
  1921. background: rgba(255, 255, 255, 0.46);
  1922. }
  1923. .docBlock:first-of-type {
  1924. margin-top: 4rpx;
  1925. }
  1926. .docTitle {
  1927. display: block;
  1928. margin-bottom: 10rpx;
  1929. font-size: 22rpx;
  1930. font-weight: 620;
  1931. color: rgba(45, 87, 64, 0.84);
  1932. }
  1933. .docText {
  1934. font-size: 22rpx;
  1935. line-height: 1.8;
  1936. color: rgba(64, 75, 67, 0.8);
  1937. }
  1938. /* 批次信息主体:连续信息排版,标签-值对应,与商品信息卡风格统一 */
  1939. .batchBody {
  1940. display: flex;
  1941. flex-direction: column;
  1942. }
  1943. /* 批次信息主体:左右两列布局,左列固定,右列自适应,内容左对齐 */
  1944. .batchBody {
  1945. display: flex;
  1946. flex-direction: column;
  1947. }
  1948. .batchRow {
  1949. display: flex;
  1950. align-items: baseline;
  1951. padding: 16rpx 0;
  1952. }
  1953. .batchRow + .batchRow {
  1954. border-top: 1rpx solid rgba(39, 75, 57, 0.05);
  1955. }
  1956. .batchRowLabel {
  1957. flex-shrink: 0;
  1958. width: 162rpx;
  1959. font-size: 21rpx;
  1960. font-weight: 460;
  1961. color: rgba(84, 106, 93, 0.68);
  1962. letter-spacing: 0.02em;
  1963. line-height: 1.5;
  1964. }
  1965. .batchRowValue {
  1966. flex: 1;
  1967. min-width: 0;
  1968. font-size: 25rpx;
  1969. font-weight: 580;
  1970. line-height: 1.4;
  1971. letter-spacing: 0.01em;
  1972. color: rgba(27, 41, 33, 0.9);
  1973. }
  1974. .cardCredential .certSummary {
  1975. margin-top: 4rpx;
  1976. }
  1977. /* 底部柔和收口渐变 */
  1978. .footerFade {
  1979. position: relative;
  1980. height: 1rpx;
  1981. margin: 0 80rpx;
  1982. background: linear-gradient(90deg,
  1983. transparent 0%,
  1984. rgba(39, 75, 57, 0.15) 30%,
  1985. rgba(39, 75, 57, 0.2) 50%,
  1986. rgba(39, 75, 57, 0.15) 70%,
  1987. transparent 100%
  1988. );
  1989. }
  1990. .footerFade::before {
  1991. content: '';
  1992. position: absolute;
  1993. top: 0;
  1994. left: 50%;
  1995. transform: translateX(-50%);
  1996. width: 60%;
  1997. height: 80rpx;
  1998. background: radial-gradient(ellipse at 50% 100%, rgba(39, 75, 57, 0.06), transparent 70%);
  1999. }
  2000. /* 轻量页脚 */
  2001. .traceFooter {
  2002. padding: 36rpx 32rpx 52rpx;
  2003. display: flex;
  2004. flex-direction: column;
  2005. align-items: center;
  2006. gap: 14rpx;
  2007. }
  2008. .traceFooterHint {
  2009. font-size: 20rpx;
  2010. font-weight: 400;
  2011. color: rgba(88, 100, 92, 0.45);
  2012. letter-spacing: 0.03em;
  2013. }
  2014. .traceFooterPhone {
  2015. display: flex;
  2016. align-items: center;
  2017. gap: 6rpx;
  2018. padding: 8rpx 16rpx;
  2019. border-radius: 6rpx;
  2020. transition: opacity 0.2s ease;
  2021. }
  2022. .traceFooterPhone:active {
  2023. opacity: 0.55;
  2024. }
  2025. .traceFooterPhoneIcon {
  2026. width: 22rpx;
  2027. height: 22rpx;
  2028. opacity: 0.55;
  2029. }
  2030. .traceFooterPhoneNum {
  2031. font-size: 22rpx;
  2032. font-weight: 400;
  2033. color: rgba(39, 75, 57, 0.6);
  2034. letter-spacing: 0.04em;
  2035. }
  2036. .traceFooterBrand {
  2037. font-size: 17rpx;
  2038. font-weight: 400;
  2039. letter-spacing: 0.1em;
  2040. color: rgba(88, 100, 92, 0.3);
  2041. margin-top: 8rpx;
  2042. }
  2043. </style>