detail.vue 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695
  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.farm?.name" class="card cardLevel2 cardFarm">
  72. <view class="sectionHeader">
  73. <view class="sectionHeaderLeft">
  74. <text class="sectionEn">Farm</text>
  75. <text class="sectionTitle">农场信息</text>
  76. </view>
  77. </view>
  78. <!-- 农场主体:左图右文结构,融入主体内容 -->
  79. <view class="farmBody">
  80. <view class="farmImageWrap">
  81. <image class="farmImage" :src="traceDetail.farm.image" mode="aspectFill" />
  82. </view>
  83. <view class="farmMeta">
  84. <text class="farmName">{{ traceDetail.farm.name }}</text>
  85. <view v-if="traceDetail.farm.location" class="farmLocation">
  86. <svg class="locationIcon" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
  87. <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)"/>
  88. </svg>
  89. <text class="locationText">{{ traceDetail.farm.location }}</text>
  90. </view>
  91. </view>
  92. </view>
  93. <view v-if="traceDetail.farm?.intro" class="farmIntro">
  94. <text>{{ traceDetail.farm.intro }}</text>
  95. </view>
  96. </view>
  97. <!-- 批次信息(可选:batch 可能不存在) -->
  98. <view v-if="traceDetail.batch?.exists" class="card cardLevel1 cardBatch">
  99. <view class="sectionHeader">
  100. <view class="sectionHeaderLeft">
  101. <text class="sectionEn">Batch</text>
  102. <text class="sectionTitle">批次信息</text>
  103. </view>
  104. </view>
  105. <!-- 批次信息主体:左右两列布局,左列标签固定宽度,右列内容左对齐 -->
  106. <view class="batchBody">
  107. <view v-if="traceDetail.batch?.no" class="batchRow">
  108. <text class="batchRowLabel">批次号</text>
  109. <text class="batchRowValue">{{ traceDetail.batch.no }}</text>
  110. </view>
  111. <view v-if="traceDetail.batch?.harvestTime" class="batchRow">
  112. <text class="batchRowLabel">生产/采收时间</text>
  113. <text class="batchRowValue">{{ traceDetail.batch.harvestTime }}</text>
  114. </view>
  115. <view v-if="traceDetail.batch?.packTime" class="batchRow">
  116. <text class="batchRowLabel">包装时间</text>
  117. <text class="batchRowValue">{{ traceDetail.batch.packTime }}</text>
  118. </view>
  119. </view>
  120. </view>
  121. <!-- 批次不存在/已下线的说明 -->
  122. <view v-else-if="traceDetail.batch?.statusBadge" class="card soft cardLevel1 cardBatchState">
  123. <view class="sectionHeader">
  124. <view class="sectionHeaderLeft">
  125. <text class="sectionEn">Batch</text>
  126. <text class="sectionTitle">当前批次状态</text>
  127. </view>
  128. <view class="statusBadge" :class="traceDetail.batch.statusBadge.type">
  129. {{ traceDetail.batch.statusBadge.text }}
  130. </view>
  131. </view>
  132. <view class="emptyText">{{ traceDetail.batch?.emptyMessage || '该批次暂不可查询' }}</view>
  133. <view class="btnRow">
  134. <button class="primaryBtn" @click="goBackToPurchase">返回购买渠道</button>
  135. </view>
  136. </view>
  137. <!-- 检测报告:横向卡片列表 -->
  138. <view v-if="traceDetail.batch?.exists" class="card cardLevel2 cardCredential">
  139. <view class="sectionHeader">
  140. <view class="sectionHeaderLeft">
  141. <text class="sectionEn">Report</text>
  142. <text class="sectionTitle">检测报告</text>
  143. </view>
  144. </view>
  145. <!-- 横向滑动卡片列表 -->
  146. <scroll-view
  147. v-if="reportItems.length"
  148. class="reportCardList"
  149. scroll-x
  150. :show-scrollbar="false"
  151. enhanced
  152. >
  153. <view class="reportCardTrack">
  154. <view
  155. v-for="(item, itemIdx) in reportItems"
  156. :key="`report-card-${itemIdx}`"
  157. class="reportCard"
  158. @click="previewDoc('report', getReportGlobalIndex(itemIdx, 0))"
  159. >
  160. <!-- 报告缩略图(带页数角标) -->
  161. <view class="reportCardThumb">
  162. <image
  163. v-if="item.images && item.images.length"
  164. class="reportCardThumbImg"
  165. :src="item.images[0]"
  166. mode="aspectFit"
  167. />
  168. <view v-else class="reportCardThumbEmpty">
  169. <text class="reportCardThumbEmptyText">暂无图片</text>
  170. </view>
  171. <view v-if="item.images && item.images.length > 1" class="reportCardPages">
  172. 共{{ item.images.length }}页
  173. </view>
  174. </view>
  175. <!-- 报告信息 -->
  176. <view class="reportCardMeta">
  177. <view v-if="item.detectDate" class="reportCardRow">
  178. <text class="reportCardLabel">检测日期</text>
  179. <text class="reportCardValue">{{ item.detectDate }}</text>
  180. </view>
  181. <view v-if="item.no" class="reportCardRow">
  182. <text class="reportCardLabel">报告编号</text>
  183. <text class="reportCardValue reportCardValueNo">{{ item.no }}</text>
  184. </view>
  185. </view>
  186. <!-- 点击提示 -->
  187. <text class="reportCardHint">点击查看大图</text>
  188. </view>
  189. </view>
  190. </scroll-view>
  191. <!-- 待补充状态 -->
  192. <view v-else class="docPlaceholder">
  193. <text class="docPlaceholderTitle">检测报告待补充</text>
  194. <text class="docPlaceholderText">
  195. {{ traceDetail.report?.emptyMessage || '检测报告待补充,请耐心等待。' }}
  196. </text>
  197. </view>
  198. </view>
  199. <!-- 合格证:信息摘要 + 缩略图一体化布局 -->
  200. <view v-if="traceDetail.batch?.exists" class="card cardLevel1 cardCredential">
  201. <view class="sectionHeader">
  202. <view class="sectionHeaderLeft">
  203. <text class="sectionEn">Certificate</text>
  204. <text class="sectionTitle">合格证</text>
  205. </view>
  206. </view>
  207. <!-- 信息与缩略图一体化区域 -->
  208. <view class="certSummary">
  209. <!-- 左侧:信息摘要 -->
  210. <view class="certSummaryInfo">
  211. <view v-if="traceDetail.certificate?.issueDate" class="certSummaryRow">
  212. <text class="certSummaryLabel">开具日期</text>
  213. <text class="certSummaryValue">{{ traceDetail.certificate.issueDate }}</text>
  214. </view>
  215. <view v-if="traceDetail.certificate?.no" class="certSummaryRow">
  216. <text class="certSummaryLabel">合格证编号</text>
  217. <text class="certSummaryValue">{{ traceDetail.certificate.no }}</text>
  218. </view>
  219. </view>
  220. <!-- 右侧:缩略图 -->
  221. <view
  222. v-if="traceDetail.certificate?.status === 'uploaded' && certificateImages.length"
  223. class="certThumbContainer"
  224. @click="previewDoc('certificate', 0)"
  225. >
  226. <view class="certThumbWrap">
  227. <image class="certThumbImage" :src="certificateImages[0]" mode="aspectFit" />
  228. </view>
  229. <text class="certPreviewHint">点击查看</text>
  230. </view>
  231. <!-- 待补充状态:仅显示提示 -->
  232. <view v-else class="certPendingHint">
  233. <text class="certPendingText">
  234. {{ traceDetail.certificate?.emptyMessage || '合格证待补充,请耐心等待。' }}
  235. </text>
  236. </view>
  237. </view>
  238. </view>
  239. <!-- 底部柔和收口渐变 -->
  240. <view class="footerFade" />
  241. <!-- 轻量页脚 -->
  242. <view class="traceFooter">
  243. <view class="traceFooterHint">如需帮助,联系客服</view>
  244. <view class="traceFooterPhone" @click="contactCustomer">
  245. <image class="traceFooterPhoneIcon" src="/static/icons/phone.svg" mode="aspectFit" />
  246. <text class="traceFooterPhoneNum">13379508760</text>
  247. </view>
  248. <view class="traceFooterBrand">佳友厚苑 · 安心之选</view>
  249. </view>
  250. </scroll-view>
  251. </view>
  252. </template>
  253. <script setup>
  254. import { computed, ref } from 'vue'
  255. import { onLoad } from '@dcloudio/uni-app'
  256. import {getTraceDetail } from '@/api/base/index.js'
  257. // 页面状态选择(H5 演示用):通过路由参数传入 state 即可切换 mock 场景
  258. // 例如:/pages/trace/detail?state=reportPending
  259. // 加载状态
  260. const loading = ref(false)
  261. // 数据存储
  262. const traceInfo = ref(null)
  263. const routeOptions = ref({})
  264. const mockStateKey = ref('normal')
  265. const MOCK_TRACE_DETAILS = {
  266. normal: {
  267. product: {
  268. name: '',
  269. spec: '',
  270. image: '',
  271. intro: ''
  272. },
  273. farm: {
  274. name: '',
  275. location: '',
  276. image: '',
  277. intro: ''
  278. },
  279. batch: {
  280. exists: false,
  281. statusBadge: { text: '', type: '' },
  282. no: '',
  283. harvestTime: '',
  284. packTime: ''
  285. },
  286. report: {
  287. status: '',
  288. statusBadge: { text: '', type: '' },
  289. // 每份报告独立 detectDate / no / images
  290. items: [
  291. {
  292. detectDate: '',
  293. no: '',
  294. images: ['']
  295. },
  296. {
  297. detectDate: '',
  298. no: '',
  299. images: ['']
  300. },
  301. {
  302. detectDate: '',
  303. no: '',
  304. images: ['']
  305. }
  306. ]
  307. },
  308. certificate: {
  309. status: '',
  310. statusBadge: { text: '', type: '' },
  311. issueDate: '',
  312. no: '',
  313. fileUrl: '',
  314. images: ['']
  315. }
  316. },
  317. reportPending: {
  318. product: {
  319. name: '',
  320. spec: '',
  321. image: '',
  322. intro: ''
  323. },
  324. farm: {
  325. name: '',
  326. location: '',
  327. image: null,
  328. intro: ''
  329. },
  330. batch: {
  331. exists: false,
  332. statusBadge: { text: '', type: '' },
  333. no: '',
  334. harvestTime: '',
  335. packTime: ''
  336. },
  337. report: {
  338. status: '',
  339. statusBadge: { text: '', type: '' },
  340. emptyMessage: '',
  341. items: []
  342. },
  343. certificate: {
  344. status: '',
  345. statusBadge: { text: '', type: '' },
  346. issueDate: '',
  347. no: '',
  348. fileUrl: '',
  349. images: []
  350. }
  351. },
  352. certificatePending: {
  353. product: {
  354. name: '',
  355. spec: '',
  356. image: '',
  357. intro: ''
  358. },
  359. farm: {
  360. name: '',
  361. location: '',
  362. image: null,
  363. intro: ''
  364. },
  365. batch: {
  366. exists: false,
  367. statusBadge: { text: '', type: '' },
  368. no: '',
  369. harvestTime: '',
  370. packTime: ''
  371. },
  372. report: {
  373. status: '',
  374. statusBadge: { text: '', type: '' },
  375. items: [
  376. {
  377. detectDate: '',
  378. no: '',
  379. images: []
  380. }
  381. ]
  382. },
  383. certificate: {
  384. status: '',
  385. statusBadge: { text: '', type: '' },
  386. emptyMessage: ''
  387. }
  388. },
  389. batchNotFound: {
  390. product: {
  391. name:'',
  392. spec: '',
  393. image: '',
  394. intro: ''
  395. },
  396. farm: {
  397. name: '',
  398. location: '',
  399. image: null,
  400. intro: ''
  401. },
  402. batch: {
  403. exists: false,
  404. statusBadge: { text: '', type: '' },
  405. emptyMessage: ''
  406. },
  407. report: null,
  408. certificate: null
  409. },
  410. batchOfflined: {
  411. product: {
  412. name: '',
  413. spec: '',
  414. image: '',
  415. intro: ''
  416. },
  417. farm: {
  418. name: '',
  419. location: '',
  420. image: null,
  421. intro: ''
  422. },
  423. batch: {
  424. exists: false,
  425. statusBadge: { text: '', type: '' },
  426. emptyMessage: ''
  427. },
  428. report: null,
  429. certificate: null
  430. }
  431. }
  432. function resolveStateKey(opts) {
  433. const raw = (opts?.state || opts?.batchState || opts?.scene || '').toString().trim()
  434. if (!raw) return 'normal'
  435. const allowed = ['normal', 'reportPending', 'certificatePending', 'batchNotFound', 'batchOfflined']
  436. return allowed.includes(raw) ? raw : 'normal'
  437. }
  438. onLoad((opts) => {
  439. // 拿到浏览器路径参数
  440. const fullPath = window.location.pathname
  441. const batchId = fullPath.split('/').filter(Boolean).pop()
  442. // 优先使用路由参数中的 id,其次使用 URL 路径中的 id
  443. const finalId = batchId || 1
  444. loadData(finalId)
  445. routeOptions.value = opts || {}
  446. mockStateKey.value = resolveStateKey(opts || {})
  447. })
  448. const loadData = async (batchId) => {
  449. loading.value = true
  450. try {
  451. // 调用接口
  452. const res = await getTraceDetail(batchId)
  453. traceInfo.value = res.data.data
  454. console.log('接口返回数据:', res)
  455. } catch (error) {
  456. console.error('加载失败', error)
  457. // 错误提示已在拦截器中自动处理
  458. } finally {
  459. // 无论成功失败都关闭 loading
  460. loading.value = false
  461. }
  462. }
  463. const traceDetail = computed(() => {
  464. // 如果没有真实数据,返回 mock 数据
  465. if (!traceInfo.value) {
  466. return MOCK_TRACE_DETAILS[mockStateKey.value] || MOCK_TRACE_DETAILS.normal
  467. }
  468. const data = traceInfo.value
  469. // 解析 certFiles 和 reportFiles(JSON 字符串转数组)
  470. let certFiles = []
  471. let reportFiles = []
  472. try {
  473. if (data.certificate?.certFiles) {
  474. certFiles = JSON.parse(data.certificate.certFiles)
  475. }
  476. } catch (e) {
  477. console.error('解析 certFiles 失败', e)
  478. }
  479. // 构建返回数据结构
  480. return {
  481. product: {
  482. name: data.productName || '',
  483. spec: data.productSpec || '',
  484. image: data.productImage || '',
  485. intro: data.productDesc || ''
  486. },
  487. farm: {
  488. name: data.farmName || '',
  489. location: data.farmRegion || '',
  490. image: data.farmImage || '',
  491. intro: data.farmIntro || ''
  492. },
  493. batch: {
  494. exists: data.status === '1', // status 为 '1' 表示批次正常
  495. statusBadge: data.status === '1'
  496. ? { text: '检验合格', type: 'ok' }
  497. : { text: '批次不存在', type: 'muted' },
  498. no: data.batchNo || '',
  499. harvestTime: data.produceDate || '',
  500. packTime: data.packageDate || '',
  501. emptyMessage: data.status !== '1' ? '未找到对应溯源批次信息。请确认二维码是否为佳友厚苑正品批次。' : ''
  502. },
  503. report: {
  504. status: data.reports && data.reports.length > 0 ? 'uploaded' : 'pending',
  505. statusBadge: data.reports && data.reports.length > 0
  506. ? { text: '已上传', type: 'ok' }
  507. : { text: '待补充', type: 'wait' },
  508. emptyMessage: '检测报告待补充,上传完成后可查看大图。',
  509. items: (data.reports || []).map(report => {
  510. let images = []
  511. try {
  512. if (report.reportFiles) {
  513. const files = JSON.parse(report.reportFiles)
  514. images = files.map(f => f.url)
  515. }
  516. } catch (e) {
  517. console.error('解析 reportFiles 失败', e)
  518. }
  519. return {
  520. detectDate: report.reportDate || '',
  521. no: report.reportNo || '',
  522. images: images
  523. }
  524. })
  525. },
  526. certificate: {
  527. status: data.certificate && certFiles.length > 0 ? 'uploaded' : 'pending',
  528. statusBadge: data.certificate && certFiles.length > 0
  529. ? { text: '已上传', type: 'ok' }
  530. : { text: '待补充', type: 'wait' },
  531. issueDate: data.certificate?.certIssueDate || '',
  532. no: data.certificate?.certNo || '',
  533. fileUrl: certFiles.length > 0 ? certFiles[0].url : '',
  534. images: certFiles.map(f => f.url),
  535. emptyMessage: '合格证待补充,上传完成后可查看大图。'
  536. }
  537. }
  538. })
  539. const reportImages = computed(() => {
  540. const items = traceDetail.value?.report?.items
  541. if (Array.isArray(items)) {
  542. return items.flatMap(item => Array.isArray(item.images) ? item.images : [])
  543. }
  544. return []
  545. })
  546. const reportItems = computed(() => {
  547. return traceDetail.value?.report?.items || []
  548. })
  549. // 计算某份报告内某张图在全局 reportImages 扁平数组中的索引
  550. function getReportGlobalIndex(itemIndex, imgIndex) {
  551. const items = reportItems.value
  552. let offset = 0
  553. for (let i = 0; i < itemIndex; i++) {
  554. const imgArr = items[i]?.images
  555. offset += Array.isArray(imgArr) ? imgArr.length : 0
  556. }
  557. return offset + imgIndex
  558. }
  559. const certificateImages = computed(() => {
  560. const c = traceDetail.value?.certificate
  561. if (!c) return []
  562. if (Array.isArray(c.images) && c.images.length) return c.images
  563. return c.fileUrl ? [c.fileUrl] : []
  564. })
  565. function contactCustomer() {
  566. uni.makePhoneCall({
  567. phoneNumber: '13379508760'
  568. })
  569. }
  570. function goBackToPurchase() {
  571. uni.showModal({
  572. title: '返回购买渠道',
  573. content: 'mock 模式:后续可跳转到直播间/小店页面。',
  574. showCancel: true,
  575. confirmText: '确认返回'
  576. })
  577. }
  578. function previewDoc(kind, index) {
  579. const urls = kind === 'report' ? reportImages.value : certificateImages.value
  580. if (!urls.length) {
  581. uni.showToast({ title: '该文件暂未上传', icon: 'none' })
  582. return
  583. }
  584. uni.previewImage({
  585. urls,
  586. current: urls[index] || urls[0]
  587. })
  588. }
  589. </script>
  590. <style scoped>
  591. .page {
  592. position: relative;
  593. min-height: 100vh;
  594. --bg-cream-1: #f8f2e8;
  595. --bg-cream-2: #f3ebdd;
  596. --bg-cream-3: #ece1ce;
  597. --forest-900: #143a2b;
  598. --forest-800: #1f513b;
  599. --forest-700: #2d6849;
  600. --sage-500: #708f7d;
  601. --gold-320: #ccb086;
  602. --gold-260: #dcc4a0;
  603. --text-900: #233126;
  604. --text-700: #3c4d41;
  605. --text-500: #6b776d;
  606. background: linear-gradient(172deg,
  607. #FAF6EF 0%,
  608. #F4EEE3 38%,
  609. #EDF0EC 68%,
  610. #E8EFE9 100%
  611. );
  612. overflow: hidden;
  613. }
  614. /* 背景氛围层:三层光感(前景/中景/背景)+ grain */
  615. .pageDecor {
  616. position: fixed;
  617. inset: 0;
  618. pointer-events: none;
  619. z-index: 0;
  620. }
  621. .pageGlow {
  622. position: absolute;
  623. border-radius: 50%;
  624. filter: blur(14rpx);
  625. }
  626. /* 背景层:左上森林绿微光 */
  627. .glowA {
  628. width: 780rpx;
  629. height: 780rpx;
  630. top: -280rpx;
  631. left: -300rpx;
  632. background: radial-gradient(circle, rgba(26, 84, 58, 0.18) 0%, rgba(20, 72, 50, 0) 70%);
  633. opacity: 0.85;
  634. }
  635. /* 中景层:右下金色暖光 */
  636. .glowB {
  637. width: 720rpx;
  638. height: 720rpx;
  639. right: -280rpx;
  640. top: 480rpx;
  641. background: radial-gradient(circle, rgba(200, 170, 120, 0.2) 0%, rgba(200, 170, 120, 0) 72%);
  642. opacity: 0.9;
  643. }
  644. /* 前景层:顶部柔和高光,塑造空间感 */
  645. .glowC {
  646. width: 600rpx;
  647. height: 360rpx;
  648. top: -60rpx;
  649. left: 50%;
  650. transform: translateX(-50%);
  651. background: radial-gradient(ellipse at 50% 0%, rgba(255, 252, 246, 0.38), transparent 70%);
  652. filter: blur(20rpx);
  653. opacity: 0.7;
  654. }
  655. /* 纹理叠加层 */
  656. .pageGrain {
  657. position: absolute;
  658. inset: 0;
  659. opacity: 0.55;
  660. background:
  661. radial-gradient(900rpx 380rpx at 48% -4%, rgba(255, 255, 255, 0.55), rgba(255, 255, 255, 0)),
  662. radial-gradient(640rpx 400rpx at 104% 48%, rgba(68, 112, 86, 0.09), rgba(68, 112, 86, 0)),
  663. radial-gradient(700rpx 320rpx at -4% 76%, rgba(210, 180, 130, 0.12), rgba(210, 180, 130, 0)),
  664. linear-gradient(180deg, rgba(248, 244, 236, 0.16) 0%, rgba(228, 242, 232, 0.08) 100%);
  665. }
  666. .safeTop {
  667. height: calc(env(safe-area-inset-top));
  668. position: relative;
  669. z-index: 1;
  670. }
  671. /* 轻品牌页头:透明底,不叠在 Hero 上产生「白雾」;层级低于首屏卡 */
  672. .brandPageHeader {
  673. position: relative;
  674. z-index: 2;
  675. display: flex;
  676. align-items: center;
  677. min-height: 72rpx;
  678. padding: 0 40rpx;
  679. box-sizing: border-box;
  680. border-bottom: 1rpx solid rgba(39, 75, 57, 0.05);
  681. background: transparent;
  682. }
  683. .brandPageHeaderInner {
  684. display: flex;
  685. align-items: center;
  686. gap: 10rpx;
  687. width: 100%;
  688. }
  689. .brandPageHeaderName {
  690. font-size: 22rpx;
  691. font-weight: 600;
  692. line-height: 1.15;
  693. letter-spacing: 0.1em;
  694. color: rgba(27, 41, 33, 0.65);
  695. }
  696. .brandPageHeaderDot {
  697. flex-shrink: 0;
  698. width: 4rpx;
  699. height: 4rpx;
  700. border-radius: 50%;
  701. background: rgba(39, 113, 76, 0.4);
  702. }
  703. .brandPageHeaderLabel {
  704. font-size: 20rpx;
  705. font-weight: 400;
  706. line-height: 1.15;
  707. letter-spacing: 0.06em;
  708. color: rgba(112, 143, 125, 0.7);
  709. }
  710. .hero,
  711. .content {
  712. position: relative;
  713. z-index: 1;
  714. }
  715. .hero {
  716. padding: 0 24rpx;
  717. }
  718. /* 完整首屏大卡:与品牌页头分离,避免负 margin 上提后被页头遮盖 */
  719. .heroCard {
  720. border-radius: 42rpx;
  721. overflow: hidden;
  722. margin-top: 20rpx;
  723. position: relative;
  724. z-index: 4;
  725. box-shadow:
  726. 0 48rpx 120rpx rgba(10, 36, 24, 0.2),
  727. 0 16rpx 48rpx rgba(10, 36, 24, 0.1),
  728. 0 2rpx 0 rgba(255, 255, 255, 0.6) inset;
  729. backdrop-filter: blur(4px);
  730. }
  731. /* Hero 视觉区:多层次光感空间,顶部高光 + 景深 + 底部暗化 */
  732. .heroBg {
  733. position: relative;
  734. width: 100%;
  735. height: 0;
  736. padding-bottom: 56.25%;
  737. overflow: hidden;
  738. background: linear-gradient(160deg, rgba(20, 74, 52, 0.72), rgba(205, 177, 129, 0.3) 70%, rgba(243, 232, 214, 0.36));
  739. }
  740. /* 前景层光晕:右下淡金色微光(提升质感) */
  741. .heroBg::before {
  742. content: '';
  743. position: absolute;
  744. right: -100rpx;
  745. bottom: -72rpx;
  746. width: 520rpx;
  747. height: 280rpx;
  748. border-radius: 50%;
  749. background: radial-gradient(ellipse at 42% 38%, rgba(252, 244, 228, 0.18), transparent 68%);
  750. pointer-events: none;
  751. z-index: 2;
  752. }
  753. .heroImage {
  754. position: absolute;
  755. inset: 0;
  756. width: 100%;
  757. height: 100%;
  758. display: block;
  759. z-index: 1;
  760. }
  761. .heroBrandAnchor {
  762. position: absolute;
  763. right: -48rpx;
  764. bottom: -14rpx;
  765. z-index: 2;
  766. pointer-events: none;
  767. }
  768. .heroBrandAnchorText {
  769. font-size: 164rpx;
  770. font-weight: 760;
  771. letter-spacing: 0.1em;
  772. line-height: 1;
  773. color: rgba(248, 241, 227, 0.045);
  774. transform: rotate(-8deg);
  775. }
  776. /* Hero 遮罩层:提亮后仍保留轻微底部过渡,避免压住真实商品图 */
  777. .heroMask {
  778. position: absolute;
  779. inset: 0;
  780. z-index: 3;
  781. background:
  782. radial-gradient(ellipse 180% 60% at 50% 0%, rgba(255, 252, 248, 0.26), transparent 68%),
  783. radial-gradient(480rpx 240rpx at 90% 8%, rgba(252, 248, 240, 0.2), transparent 62%),
  784. radial-gradient(700rpx 360rpx at 8% 65%, rgba(6, 24, 16, 0.12), transparent 58%),
  785. radial-gradient(560rpx 300rpx at 88% 80%, rgba(205, 177, 129, 0.06), transparent 62%),
  786. linear-gradient(180deg,
  787. rgba(8, 28, 18, 0) 0%,
  788. rgba(8, 28, 18, 0) 62%,
  789. rgba(10, 36, 24, 0.05) 80%,
  790. rgba(8, 26, 18, 0.1) 92%,
  791. rgba(248, 244, 238, 0.12) 100%
  792. );
  793. }
  794. /* 商品信息面板:玻璃浮层效果,负margin上浮 + backdrop-filter */
  795. .heroInfoPanel {
  796. background: rgba(252, 248, 242, 0.88);
  797. backdrop-filter: blur(14px);
  798. -webkit-backdrop-filter: blur(14px);
  799. position: relative;
  800. }
  801. /* 与 Hero 底部自然过渡 */
  802. .heroInfoPanel::before {
  803. content: '';
  804. position: absolute;
  805. top: -56rpx;
  806. left: 0;
  807. right: 0;
  808. height: 56rpx;
  809. background: linear-gradient(180deg,
  810. rgba(252, 248, 242, 0) 0%,
  811. rgba(252, 248, 242, 0.88) 100%
  812. );
  813. pointer-events: none;
  814. z-index: 1;
  815. }
  816. .infoPanelInner {
  817. position: relative;
  818. z-index: 2;
  819. padding: 32rpx 30rpx 30rpx;
  820. }
  821. /* 与下方农场/批次等卡片共用 .sectionHeader / .sectionHeaderLeft 样式 */
  822. .infoPanelBody {
  823. padding-top: 0;
  824. }
  825. .infoPanelName {
  826. display: block;
  827. font-size: 30rpx;
  828. font-weight: 680;
  829. line-height: 1.4;
  830. letter-spacing: 0.01em;
  831. color: rgba(27, 41, 33, 0.92);
  832. }
  833. .infoPanelSpec {
  834. display: inline-flex;
  835. align-items: center;
  836. gap: 10rpx;
  837. margin-top: 14rpx;
  838. padding: 8rpx 16rpx;
  839. border-radius: 20rpx;
  840. background: rgba(39, 113, 76, 0.06);
  841. border: 1rpx solid rgba(39, 113, 76, 0.08);
  842. }
  843. .specDot {
  844. width: 6rpx;
  845. height: 6rpx;
  846. border-radius: 50%;
  847. background: rgba(39, 113, 76, 0.5);
  848. }
  849. .specText {
  850. font-size: 20rpx;
  851. font-weight: 500;
  852. color: rgba(54, 66, 57, 0.85);
  853. }
  854. .infoPanelIntro {
  855. margin-top: 16rpx;
  856. padding-top: 16rpx;
  857. border-top: 1rpx solid rgba(39, 75, 57, 0.06);
  858. font-size: 23rpx;
  859. line-height: 1.8;
  860. color: rgba(54, 66, 57, 0.75);
  861. }
  862. .content {
  863. padding: 20rpx 0 40rpx;
  864. }
  865. /* 基础卡片:半透明玻璃质感 */
  866. .card {
  867. margin: 0 24rpx 22rpx;
  868. padding: 30rpx 28rpx;
  869. border-radius: 36rpx;
  870. background: rgba(252, 250, 244, 0.76);
  871. box-shadow:
  872. 0 16rpx 40rpx rgba(38, 41, 32, 0.07),
  873. 0 1rpx 0 rgba(255, 255, 255, 0.5) inset;
  874. backdrop-filter: blur(8px);
  875. -webkit-backdrop-filter: blur(8px);
  876. }
  877. .card.soft {
  878. background: rgba(252, 248, 244, 0.6);
  879. }
  880. /* Hero 之后第一张卡:适度上浮 */
  881. .cardHeroFirst {
  882. margin-top: -28rpx;
  883. position: relative;
  884. z-index: 3;
  885. }
  886. /* 层级 0:与 Hero 同级,主卡(商品面板) */
  887. .cardLevel0 {
  888. box-shadow:
  889. 0 48rpx 120rpx rgba(10, 36, 24, 0.16),
  890. 0 16rpx 48rpx rgba(10, 36, 24, 0.08),
  891. 0 1rpx 0 rgba(255, 255, 255, 0.6) inset;
  892. }
  893. /* 层级 1:次要信息卡,阴影中等 */
  894. .cardLevel1 {
  895. box-shadow:
  896. 0 20rpx 50rpx rgba(30, 34, 24, 0.09),
  897. 0 8rpx 24rpx rgba(30, 34, 24, 0.05),
  898. 0 1rpx 0 rgba(255, 255, 255, 0.55) inset;
  899. }
  900. /* 层级 2:次次要信息卡,阴影更轻 */
  901. .cardLevel2 {
  902. box-shadow:
  903. 0 14rpx 36rpx rgba(36, 38, 28, 0.065),
  904. 0 1rpx 0 rgba(255, 255, 255, 0.5) inset;
  905. }
  906. /* 层级 3:底部辅助信息卡,最轻 */
  907. .cardLevel3 {
  908. box-shadow:
  909. 0 8rpx 22rpx rgba(42, 44, 30, 0.04),
  910. 0 1rpx 0 rgba(255, 255, 255, 0.44) inset;
  911. }
  912. .cardFarm {
  913. background: rgba(250, 248, 242, 0.82);
  914. }
  915. .cardBatch {
  916. background: rgba(251, 248, 242, 0.8);
  917. }
  918. .cardCredential {
  919. background: rgba(252, 248, 240, 0.84);
  920. }
  921. .cardBatchState {
  922. background: rgba(252, 248, 244, 0.74);
  923. }
  924. /* ================================================
  925. 微动效:让页面"活起来"
  926. ================================================ */
  927. /* Hero 进入:上浮 + 淡入 */
  928. @keyframes heroEnter {
  929. 0% {
  930. opacity: 0;
  931. transform: translateY(28rpx);
  932. }
  933. 100% {
  934. opacity: 1;
  935. transform: translateY(0);
  936. }
  937. }
  938. .heroCard {
  939. animation: heroEnter 0.6s cubic-bezier(0.22, 0.78, 0.35, 1) both;
  940. }
  941. /* 卡片依次入场:延迟递增,制造节奏感 */
  942. @keyframes cardEnter {
  943. 0% {
  944. opacity: 0;
  945. transform: translateY(20rpx);
  946. }
  947. 100% {
  948. opacity: 1;
  949. transform: translateY(0);
  950. }
  951. }
  952. /* 商品面板:稍晚于 Hero */
  953. .heroInfoPanel {
  954. animation: cardEnter 0.5s cubic-bezier(0.22, 0.78, 0.35, 1) 0.15s both;
  955. }
  956. /* 农场卡 */
  957. .cardFarm {
  958. animation: cardEnter 0.5s cubic-bezier(0.22, 0.78, 0.35, 1) 0.25s both;
  959. }
  960. /* 批次卡 */
  961. .cardBatch {
  962. animation: cardEnter 0.5s cubic-bezier(0.22, 0.78, 0.35, 1) 0.35s both;
  963. }
  964. /* 凭证卡 */
  965. .cardCredential {
  966. animation: cardEnter 0.5s cubic-bezier(0.22, 0.78, 0.35, 1) 0.45s both;
  967. }
  968. /* ============================
  969. 检测报告横向卡片列表
  970. ============================ */
  971. .reportCardList {
  972. width: 100%;
  973. overflow-x: auto;
  974. overflow-y: hidden;
  975. -webkit-overflow-scrolling: touch;
  976. white-space: nowrap;
  977. }
  978. .reportCardList::-webkit-scrollbar {
  979. display: none;
  980. }
  981. .reportCardTrack {
  982. display: inline-flex;
  983. flex-wrap: nowrap;
  984. align-items: stretch;
  985. gap: 16rpx;
  986. padding: 6rpx 4rpx 10rpx;
  987. /* 关键:让子项不受 flex 压缩,自然撑开形成横向溢出 */
  988. width: max-content;
  989. }
  990. /* 单张报告卡片 */
  991. .reportCard {
  992. flex-shrink: 0;
  993. width: 220rpx;
  994. background: #FAFBF9;
  995. border: 1rpx solid rgba(39, 75, 57, 0.09);
  996. border-radius: 14rpx;
  997. padding: 12rpx 12rpx 10rpx;
  998. display: flex;
  999. flex-direction: column;
  1000. align-items: stretch;
  1001. box-shadow: 0 2rpx 8rpx rgba(39, 75, 57, 0.04);
  1002. }
  1003. /* 缩略图(带页数角标覆盖层) */
  1004. .reportCardThumb {
  1005. position: relative;
  1006. width: 100%;
  1007. height: 196rpx;
  1008. background: rgba(39, 75, 57, 0.03);
  1009. border-radius: 10rpx;
  1010. overflow: hidden;
  1011. display: flex;
  1012. align-items: center;
  1013. justify-content: center;
  1014. margin-bottom: 10rpx;
  1015. }
  1016. .reportCardThumbImg {
  1017. width: 100%;
  1018. height: 100%;
  1019. }
  1020. .reportCardThumbEmpty {
  1021. width: 100%;
  1022. height: 100%;
  1023. display: flex;
  1024. align-items: center;
  1025. justify-content: center;
  1026. }
  1027. .reportCardThumbEmptyText {
  1028. font-size: 18rpx;
  1029. color: rgba(39, 75, 57, 0.3);
  1030. }
  1031. /* 页数角标:绝对定位在缩略图右下角 */
  1032. .reportCardPages {
  1033. position: absolute;
  1034. bottom: 8rpx;
  1035. right: 8rpx;
  1036. font-size: 15rpx;
  1037. color: rgba(39, 75, 57, 0.65);
  1038. background: rgba(250, 251, 249, 0.88);
  1039. border: 1rpx solid rgba(39, 75, 57, 0.1);
  1040. border-radius: 6rpx;
  1041. padding: 2rpx 8rpx;
  1042. line-height: 1.4;
  1043. }
  1044. /* 报告元信息 */
  1045. .reportCardMeta {
  1046. flex: 1;
  1047. margin-bottom: 6rpx;
  1048. }
  1049. .reportCardRow {
  1050. display: flex;
  1051. justify-content: space-between;
  1052. align-items: baseline;
  1053. gap: 4rpx;
  1054. margin-bottom: 4rpx;
  1055. }
  1056. .reportCardLabel {
  1057. font-size: 16rpx;
  1058. color: rgba(84, 106, 93, 0.6);
  1059. flex-shrink: 0;
  1060. }
  1061. .reportCardValue {
  1062. font-size: 16rpx;
  1063. color: rgba(68, 82, 72, 0.85);
  1064. font-weight: 460;
  1065. text-align: right;
  1066. word-break: break-all;
  1067. }
  1068. .reportCardValueNo {
  1069. font-size: 14rpx;
  1070. color: rgba(68, 82, 72, 0.72);
  1071. }
  1072. /* 点击提示 */
  1073. .reportCardHint {
  1074. font-size: 14rpx;
  1075. color: rgba(39, 75, 57, 0.3);
  1076. text-align: center;
  1077. margin-top: auto;
  1078. }
  1079. /* 底部辅助卡 */
  1080. .cardBatchState {
  1081. animation: cardEnter 0.5s cubic-bezier(0.22, 0.78, 0.35, 1) 0.55s both;
  1082. }
  1083. /* 卡片触摸反馈:阴影增强(模拟按压感) */
  1084. .card:active {
  1085. transition: box-shadow 0.15s ease, transform 0.15s ease;
  1086. }
  1087. /* 触摸时轻微缩小,增加真实感 */
  1088. .card:active:not(.heroInfoPanel) {
  1089. transform: scale(0.99);
  1090. }
  1091. .sectionHeader {
  1092. display: flex;
  1093. align-items: center;
  1094. justify-content: space-between;
  1095. gap: 18rpx;
  1096. margin-bottom: 20rpx;
  1097. }
  1098. .sectionHeaderLeft {
  1099. display: flex;
  1100. flex-direction: column;
  1101. gap: 10rpx;
  1102. position: relative;
  1103. padding-bottom: 8rpx;
  1104. }
  1105. .sectionHeaderLeft::after {
  1106. content: '';
  1107. position: absolute;
  1108. left: 0;
  1109. bottom: 0;
  1110. width: 78rpx;
  1111. height: 2rpx;
  1112. border-radius: 999rpx;
  1113. background: linear-gradient(90deg, rgba(46, 82, 63, 0.3), rgba(203, 176, 128, 0.26), rgba(46, 82, 63, 0));
  1114. pointer-events: none;
  1115. }
  1116. .sectionEn {
  1117. font-size: 15rpx;
  1118. font-weight: 500;
  1119. letter-spacing: 0.24em;
  1120. text-transform: uppercase;
  1121. color: rgba(84, 106, 93, 0.76);
  1122. }
  1123. .sectionTitle {
  1124. font-size: 27rpx;
  1125. font-weight: 680;
  1126. color: rgba(60, 72, 64, 0.82);
  1127. }
  1128. .statusBadge {
  1129. font-size: 18rpx;
  1130. font-weight: 620;
  1131. padding: 9rpx 15rpx;
  1132. border-radius: 999rpx;
  1133. white-space: nowrap;
  1134. border: 1rpx solid transparent;
  1135. box-shadow: 0 1rpx 0 rgba(255, 255, 255, 0.34) inset;
  1136. }
  1137. .statusBadge.ok {
  1138. color: #1d5d40;
  1139. background: rgba(39, 113, 76, 0.12);
  1140. border-color: rgba(39, 113, 76, 0.2);
  1141. }
  1142. .statusBadge.wait {
  1143. color: #715726;
  1144. background: rgba(203, 171, 112, 0.2);
  1145. border-color: rgba(203, 171, 112, 0.26);
  1146. }
  1147. .statusBadge.warning {
  1148. color: #8a5e22;
  1149. background: rgba(214, 174, 95, 0.22);
  1150. border-color: rgba(214, 174, 95, 0.28);
  1151. }
  1152. .statusBadge.muted {
  1153. color: #677069;
  1154. background: rgba(126, 137, 128, 0.14);
  1155. border-color: rgba(126, 137, 128, 0.2);
  1156. }
  1157. /* =============================================
  1158. 商品信息面板专用信任认证徽章:强化图标主视觉、文字辅助
  1159. ============================================= */
  1160. /* 整体:认证章风格,低饱和细边框,无交互感 */
  1161. .trustBadge {
  1162. display: inline-flex;
  1163. align-items: center;
  1164. gap: 7rpx;
  1165. padding: 9rpx 18rpx 9rpx 15rpx;
  1166. border-radius: 999rpx;
  1167. border: 1rpx solid rgba(39, 113, 76, 0.22);
  1168. background: rgba(39, 113, 76, 0.06);
  1169. box-shadow:
  1170. 0 2rpx 6rpx rgba(39, 113, 76, 0.06),
  1171. 0 1rpx 0 rgba(255, 255, 255, 0.55) inset;
  1172. white-space: nowrap;
  1173. cursor: default;
  1174. pointer-events: none;
  1175. user-select: none;
  1176. }
  1177. /* 双圆环认证图标:20×20 viewBox,图标为第一视觉焦点 */
  1178. .trustBadgeIcon {
  1179. width: 30rpx;
  1180. height: 30rpx;
  1181. flex-shrink: 0;
  1182. vertical-align: middle;
  1183. display: inline-block;
  1184. }
  1185. /* 文案:品牌深绿,字重略降,视觉权重低于图标 */
  1186. .trustBadgeText {
  1187. font-size: 18rpx;
  1188. font-weight: 500;
  1189. letter-spacing: 0.05em;
  1190. color: rgba(27, 71, 42, 0.68);
  1191. }
  1192. .kv {
  1193. display: flex;
  1194. flex-direction: column;
  1195. gap: 16rpx;
  1196. }
  1197. .kvRow {
  1198. display: flex;
  1199. align-items: baseline;
  1200. justify-content: space-between;
  1201. gap: 16rpx;
  1202. }
  1203. .kvKey {
  1204. font-size: 22rpx;
  1205. font-weight: 480;
  1206. color: rgba(54, 68, 58, 0.62);
  1207. }
  1208. .kvVal {
  1209. font-size: 24rpx;
  1210. font-weight: 660;
  1211. text-align: right;
  1212. color: rgba(27, 41, 33, 0.92);
  1213. }
  1214. /* 农场主体:左图右文,右栏与图片垂直居中,名称/地址成组 */
  1215. .farmBody {
  1216. display: flex;
  1217. align-items: center;
  1218. gap: 24rpx;
  1219. margin-bottom: 18rpx;
  1220. }
  1221. .farmImageWrap {
  1222. flex-shrink: 0;
  1223. width: 180rpx;
  1224. height: 136rpx;
  1225. border-radius: 22rpx;
  1226. overflow: hidden;
  1227. position: relative;
  1228. box-shadow:
  1229. 0 12rpx 28rpx rgba(24, 41, 30, 0.14),
  1230. 0 2rpx 0 rgba(255, 255, 255, 0.3) inset;
  1231. }
  1232. .farmImage {
  1233. width: 100%;
  1234. height: 100%;
  1235. display: block;
  1236. background: linear-gradient(160deg, rgba(234, 245, 240, 0.9), rgba(220, 238, 228, 0.85));
  1237. }
  1238. .farmMeta {
  1239. flex: 1;
  1240. display: flex;
  1241. flex-direction: column;
  1242. justify-content: center;
  1243. min-width: 0;
  1244. gap: 10rpx;
  1245. }
  1246. .farmName {
  1247. font-size: 28rpx;
  1248. font-weight: 680;
  1249. line-height: 1.45;
  1250. letter-spacing: 0.01em;
  1251. color: rgba(27, 41, 33, 0.94);
  1252. }
  1253. /* 农场地址:普通文本样式,弱于标题;多行时图标与首行对齐 */
  1254. .farmLocation {
  1255. display: flex;
  1256. align-items: flex-start;
  1257. gap: 8rpx;
  1258. }
  1259. /* 定位小图标:略弱、与首行文字视觉对齐 */
  1260. .locationIcon {
  1261. width: 18rpx;
  1262. height: 18rpx;
  1263. flex-shrink: 0;
  1264. margin-top: 6rpx;
  1265. opacity: 0.42;
  1266. }
  1267. .locationText {
  1268. flex: 1;
  1269. min-width: 0;
  1270. font-size: 21rpx;
  1271. font-weight: 400;
  1272. color: rgba(84, 106, 93, 0.82);
  1273. line-height: 1.55;
  1274. letter-spacing: 0.02em;
  1275. }
  1276. /* 农场介绍:融入主体,使用分隔线区分,与商品简介风格统一 */
  1277. .farmIntro {
  1278. padding-top: 18rpx;
  1279. border-top: 1rpx solid rgba(39, 75, 57, 0.06);
  1280. font-size: 23rpx;
  1281. line-height: 1.86;
  1282. color: rgba(54, 68, 58, 0.76);
  1283. }
  1284. .emptyText {
  1285. margin-top: 10rpx;
  1286. font-size: 24rpx;
  1287. line-height: 1.62;
  1288. color: rgba(16, 24, 19, 0.72);
  1289. }
  1290. .btnRow {
  1291. margin-top: 22rpx;
  1292. }
  1293. /* 合格证紧凑缩略图 */
  1294. /* ============================
  1295. 合格证:信息摘要 + 缩略图一体化布局
  1296. ============================ */
  1297. .certSummary {
  1298. display: flex;
  1299. align-items: flex-start;
  1300. gap: 24rpx;
  1301. }
  1302. .certSummaryInfo {
  1303. flex: 1;
  1304. display: flex;
  1305. flex-direction: column;
  1306. gap: 14rpx;
  1307. min-width: 0;
  1308. }
  1309. .certSummaryRow {
  1310. display: flex;
  1311. flex-direction: column;
  1312. gap: 4rpx;
  1313. }
  1314. .certSummaryLabel {
  1315. font-size: 20rpx;
  1316. font-weight: 500;
  1317. color: rgba(39, 75, 57, 0.55);
  1318. letter-spacing: 0.02em;
  1319. }
  1320. .certSummaryValue {
  1321. font-size: 25rpx;
  1322. font-weight: 580;
  1323. color: rgba(27, 41, 33, 0.9);
  1324. line-height: 1.4;
  1325. }
  1326. .certThumbContainer {
  1327. flex: 0 0 auto;
  1328. display: flex;
  1329. flex-direction: column;
  1330. align-items: center;
  1331. gap: 4rpx;
  1332. }
  1333. .certThumbContainer .certThumbWrap {
  1334. /* 与左侧两行摘要区总高约齐平:缩略图 + gap + 提示行 */
  1335. width: 88rpx;
  1336. height: 120rpx;
  1337. border-radius: 8rpx;
  1338. overflow: hidden;
  1339. background: #fff;
  1340. border: 1rpx solid rgba(200, 180, 140, 0.22);
  1341. box-shadow: 0 2rpx 8rpx rgba(30, 36, 26, 0.06);
  1342. }
  1343. .certThumbContainer .certThumbImage {
  1344. width: 100%;
  1345. height: 100%;
  1346. display: block;
  1347. }
  1348. .certPreviewHint {
  1349. font-size: 16rpx;
  1350. line-height: 1.15;
  1351. color: rgba(39, 75, 57, 0.4);
  1352. letter-spacing: 0.01em;
  1353. }
  1354. .certPendingHint {
  1355. flex: 1;
  1356. display: flex;
  1357. align-items: center;
  1358. padding: 12rpx 0;
  1359. }
  1360. .certPendingText {
  1361. font-size: 21rpx;
  1362. color: rgba(39, 75, 57, 0.5);
  1363. line-height: 1.5;
  1364. }
  1365. /* 旧样式保留但隐藏(兼容其他模块) */
  1366. .certPreviewCompact {
  1367. display: flex;
  1368. flex-direction: column;
  1369. align-items: center;
  1370. margin-top: 24rpx;
  1371. }
  1372. .docPlaceholder {
  1373. margin-top: 16rpx;
  1374. padding: 20rpx;
  1375. border-radius: 26rpx;
  1376. background: linear-gradient(160deg, rgba(244, 237, 224, 0.82), rgba(237, 244, 235, 0.72));
  1377. border: 1rpx solid rgba(201, 171, 120, 0.22);
  1378. }
  1379. .docPlaceholderTitle {
  1380. display: block;
  1381. margin-bottom: 12rpx;
  1382. font-size: 22rpx;
  1383. font-weight: 650;
  1384. color: rgba(35, 93, 64, 0.92);
  1385. }
  1386. .docPlaceholderText {
  1387. display: block;
  1388. font-size: 22rpx;
  1389. line-height: 1.75;
  1390. color: rgba(59, 71, 62, 0.82);
  1391. }
  1392. .docBlock {
  1393. margin-top: 14rpx;
  1394. padding: 18rpx;
  1395. border-radius: 22rpx;
  1396. background: rgba(255, 255, 255, 0.46);
  1397. }
  1398. .docBlock:first-of-type {
  1399. margin-top: 4rpx;
  1400. }
  1401. .docTitle {
  1402. display: block;
  1403. margin-bottom: 10rpx;
  1404. font-size: 22rpx;
  1405. font-weight: 620;
  1406. color: rgba(45, 87, 64, 0.84);
  1407. }
  1408. .docText {
  1409. font-size: 22rpx;
  1410. line-height: 1.8;
  1411. color: rgba(64, 75, 67, 0.8);
  1412. }
  1413. /* 批次信息主体:连续信息排版,标签-值对应,与商品信息卡风格统一 */
  1414. .batchBody {
  1415. display: flex;
  1416. flex-direction: column;
  1417. }
  1418. /* 批次信息主体:左右两列布局,左列固定,右列自适应,内容左对齐 */
  1419. .batchBody {
  1420. display: flex;
  1421. flex-direction: column;
  1422. }
  1423. .batchRow {
  1424. display: flex;
  1425. align-items: baseline;
  1426. padding: 16rpx 0;
  1427. }
  1428. .batchRow + .batchRow {
  1429. border-top: 1rpx solid rgba(39, 75, 57, 0.05);
  1430. }
  1431. .batchRowLabel {
  1432. flex-shrink: 0;
  1433. width: 162rpx;
  1434. font-size: 21rpx;
  1435. font-weight: 460;
  1436. color: rgba(84, 106, 93, 0.68);
  1437. letter-spacing: 0.02em;
  1438. line-height: 1.5;
  1439. }
  1440. .batchRowValue {
  1441. flex: 1;
  1442. min-width: 0;
  1443. font-size: 25rpx;
  1444. font-weight: 580;
  1445. line-height: 1.4;
  1446. letter-spacing: 0.01em;
  1447. color: rgba(27, 41, 33, 0.9);
  1448. }
  1449. .cardCredential .certSummary {
  1450. margin-top: 4rpx;
  1451. }
  1452. /* 底部柔和收口渐变 */
  1453. .footerFade {
  1454. position: relative;
  1455. height: 1rpx;
  1456. margin: 0 80rpx;
  1457. background: linear-gradient(90deg,
  1458. transparent 0%,
  1459. rgba(39, 75, 57, 0.15) 30%,
  1460. rgba(39, 75, 57, 0.2) 50%,
  1461. rgba(39, 75, 57, 0.15) 70%,
  1462. transparent 100%
  1463. );
  1464. }
  1465. .footerFade::before {
  1466. content: '';
  1467. position: absolute;
  1468. top: 0;
  1469. left: 50%;
  1470. transform: translateX(-50%);
  1471. width: 60%;
  1472. height: 80rpx;
  1473. background: radial-gradient(ellipse at 50% 100%, rgba(39, 75, 57, 0.06), transparent 70%);
  1474. }
  1475. /* 轻量页脚 */
  1476. .traceFooter {
  1477. padding: 36rpx 32rpx 52rpx;
  1478. display: flex;
  1479. flex-direction: column;
  1480. align-items: center;
  1481. gap: 14rpx;
  1482. }
  1483. .traceFooterHint {
  1484. font-size: 20rpx;
  1485. font-weight: 400;
  1486. color: rgba(88, 100, 92, 0.45);
  1487. letter-spacing: 0.03em;
  1488. }
  1489. .traceFooterPhone {
  1490. display: flex;
  1491. align-items: center;
  1492. gap: 6rpx;
  1493. padding: 8rpx 16rpx;
  1494. border-radius: 6rpx;
  1495. transition: opacity 0.2s ease;
  1496. }
  1497. .traceFooterPhone:active {
  1498. opacity: 0.55;
  1499. }
  1500. .traceFooterPhoneIcon {
  1501. width: 22rpx;
  1502. height: 22rpx;
  1503. opacity: 0.55;
  1504. }
  1505. .traceFooterPhoneNum {
  1506. font-size: 22rpx;
  1507. font-weight: 400;
  1508. color: rgba(39, 75, 57, 0.6);
  1509. letter-spacing: 0.04em;
  1510. }
  1511. .traceFooterBrand {
  1512. font-size: 17rpx;
  1513. font-weight: 400;
  1514. letter-spacing: 0.1em;
  1515. color: rgba(88, 100, 92, 0.3);
  1516. margin-top: 8rpx;
  1517. }
  1518. </style>