shareDynamic.html 68 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="utf-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
  6. <meta name="format-detection" content="telephone=no">
  7. <title>动态详情</title>
  8. <style>
  9. :root {
  10. --orange: #F58220;
  11. --text: #333333;
  12. --text-secondary: #999999;
  13. --text-muted: #666666;
  14. --border: #EEEEEE;
  15. --page-bg: #F5F6F7;
  16. --card-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
  17. --safe-bottom: env(safe-area-inset-bottom, 0px);
  18. }
  19. * {
  20. margin: 0;
  21. padding: 0;
  22. box-sizing: border-box;
  23. }
  24. html {
  25. font-size: 16px;
  26. -webkit-tap-highlight-color: transparent;
  27. overflow-x: hidden;
  28. }
  29. body.page-dynamic {
  30. font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", "Helvetica Neue", sans-serif;
  31. background: #fff;
  32. color: var(--text);
  33. padding-bottom: calc(100px + var(--safe-bottom));
  34. min-height: 100vh;
  35. overflow-x: hidden;
  36. }
  37. /* getOne businessStatus=99 或「暂无承载数据」:隐藏主内容 #dynPageMain,展示关店横幅 + 更多推荐(与 shareIndex 一致) */
  38. #dynPageMain {
  39. display: block;
  40. }
  41. .closed-rec-wrap {
  42. display: none;
  43. }
  44. .closed-rec-divider {
  45. height: 8px;
  46. background: #F7F7F7;
  47. margin: 0;
  48. }
  49. .closed-rec-title {
  50. padding: 8px 15px 12px;
  51. font-size: 16px;
  52. font-weight: 700;
  53. }
  54. .closed-rec-grid {
  55. display: grid;
  56. grid-template-columns: repeat(2, 1fr);
  57. gap: 10px;
  58. padding: 0 15px 20px;
  59. }
  60. #shareClosedRecEmpty {
  61. grid-column: 1 / -1;
  62. }
  63. .closed-rec-empty {
  64. padding: 12px;
  65. color: var(--text-secondary);
  66. font-size: 14px;
  67. }
  68. .closed-rec-card {
  69. min-width: 0;
  70. background: #fff;
  71. border-radius: 10px;
  72. overflow: hidden;
  73. box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
  74. }
  75. .closed-rec-card__img {
  76. aspect-ratio: 4 / 3;
  77. background: #eee;
  78. }
  79. .closed-rec-card__img img {
  80. width: 100%;
  81. height: 100%;
  82. object-fit: cover;
  83. display: block;
  84. border-radius: 0;
  85. }
  86. .closed-rec-card__body {
  87. padding: 10px;
  88. }
  89. .closed-rec-card__top {
  90. display: flex;
  91. justify-content: space-between;
  92. align-items: baseline;
  93. gap: 8px;
  94. margin-bottom: 6px;
  95. }
  96. .closed-rec-card__name {
  97. font-size: 15px;
  98. font-weight: 700;
  99. flex: 1;
  100. min-width: 0;
  101. overflow: hidden;
  102. text-overflow: ellipsis;
  103. white-space: nowrap;
  104. }
  105. .closed-rec-card__dist {
  106. font-size: 12px;
  107. color: var(--text-secondary);
  108. flex-shrink: 0;
  109. }
  110. .closed-rec-card__rating {
  111. display: flex;
  112. align-items: center;
  113. flex-wrap: wrap;
  114. gap: 4px 6px;
  115. }
  116. .closed-rec-card__rating .closed-rec-stars {
  117. display: inline-flex;
  118. align-items: center;
  119. gap: 2px;
  120. }
  121. .closed-rec-card__rating .closed-rec-star {
  122. display: block;
  123. flex-shrink: 0;
  124. }
  125. .closed-rec-card__rating .closed-rec-rating-num {
  126. font-size: 12px;
  127. font-weight: 600;
  128. color: var(--orange);
  129. }
  130. .closed-rec-meta {
  131. font-size: 12px;
  132. color: var(--text-secondary);
  133. }
  134. .closed-rec-card__footer {
  135. margin-top: 8px;
  136. font-size: 12px;
  137. color: var(--text-secondary);
  138. overflow: hidden;
  139. text-overflow: ellipsis;
  140. white-space: nowrap;
  141. }
  142. .share-none {
  143. display: none;
  144. text-align: center;
  145. padding: 28px 20px 16px;
  146. color: var(--text-muted);
  147. font-size: 15px;
  148. line-height: 1.5;
  149. }
  150. .share-none img {
  151. width: 240px;
  152. height: 240px;
  153. margin-bottom: 12px;
  154. display: inline-block;
  155. }
  156. .share-none p {
  157. margin: 0;
  158. padding: 0 0 60px 0;
  159. color: #999;
  160. }
  161. /* 顶部轮播 */
  162. .dyn-hero {
  163. position: relative;
  164. margin: 12px 15px 0;
  165. border-radius: 12px 12px 0 0;
  166. overflow: hidden;
  167. background: #e8e8e8;
  168. }
  169. .dyn-hero__track {
  170. display: flex;
  171. transition: transform 0.35s ease;
  172. }
  173. .dyn-hero__slide {
  174. flex: 0 0 100%;
  175. aspect-ratio: 16 / 10;
  176. background: #e0e0e0;
  177. }
  178. .dyn-hero__slide img,
  179. .dyn-hero__slide video {
  180. width: 100%;
  181. height: 100%;
  182. object-fit: cover;
  183. display: block;
  184. }
  185. /* 仅展示首帧:禁止点击触发播放;叠层 poster 时视频背景透明,避免 iOS 黑底盖住图 */
  186. .dyn-hero__slide .dyn-hero__media-wrap {
  187. position: relative;
  188. width: 100%;
  189. height: 100%;
  190. overflow: hidden;
  191. background: #e0e0e0;
  192. }
  193. /* 封面叠在 video 之上:微信/iOS 里 video 常呈灰底,否则会盖住已加载的 poster/img */
  194. .dyn-hero__slide .dyn-hero__poster-swap {
  195. position: absolute;
  196. left: 0;
  197. top: 0;
  198. width: 100%;
  199. height: 100%;
  200. object-fit: cover;
  201. z-index: 2;
  202. transition: opacity 0.2s ease;
  203. }
  204. .dyn-hero__slide video.dyn-hero__video--poster-only {
  205. position: relative;
  206. z-index: 1;
  207. pointer-events: none;
  208. background: transparent;
  209. }
  210. .dyn-hero--no-dots .dyn-hero__dots {
  211. display: none;
  212. }
  213. .dyn-hero__dots {
  214. position: absolute;
  215. bottom: 12px;
  216. left: 0;
  217. right: 0;
  218. display: flex;
  219. justify-content: center;
  220. align-items: center;
  221. gap: 6px;display: none;
  222. }
  223. .dyn-hero__dot {
  224. width: 6px;
  225. height: 6px;
  226. border-radius: 3px;
  227. background: rgba(255, 255, 255, 0.45);
  228. transition: width 0.25s ease, background 0.25s ease;
  229. }
  230. .dyn-hero__dot.is-active {
  231. width: 16px;
  232. background: #fff;
  233. }
  234. /* 用户信息 */
  235. .dyn-user {
  236. padding: 16px 15px 12px;
  237. background:#fff;
  238. }
  239. .dyn-user__row {
  240. display: flex;
  241. align-items: flex-start;
  242. gap: 12px;
  243. }
  244. .dyn-user__avatar {
  245. width: 48px;
  246. height: 48px;
  247. border-radius: 50%;
  248. object-fit: cover;
  249. background: #ddd;
  250. flex-shrink: 0;
  251. }
  252. .dyn-user__name {
  253. font-size: 17px;
  254. font-weight: 700;
  255. color: var(--text);
  256. line-height: 1.3;
  257. padding-top: 2px;
  258. }
  259. .dyn-user__desc {
  260. margin-top: 10px;
  261. font-size: 15px;
  262. line-height: 1.6;
  263. color: var(--text-muted);
  264. }
  265. /* 卡片通用 */
  266. .dyn-card {
  267. margin: 0 15px 12px;
  268. padding: 14px;
  269. background: #fff;
  270. border-radius: 12px;
  271. box-shadow: var(--card-shadow);
  272. }
  273. /* 店铺卡片 */
  274. .store-row {
  275. display: flex;
  276. gap: 12px;
  277. align-items: flex-start;
  278. }
  279. .store-row__thumb {
  280. width: 80px;
  281. height: 80px;
  282. border-radius: 8px;
  283. object-fit: cover;
  284. background: #eee;
  285. flex-shrink: 0;
  286. }
  287. .store-row__main {
  288. flex: 1;
  289. min-width: 0;
  290. }
  291. .store-row__title {
  292. font-size: 16px;
  293. font-weight: 700;
  294. color: var(--text);
  295. margin-bottom: 6px;
  296. }
  297. .store-row__rating {
  298. display: flex;
  299. align-items: center;
  300. flex-wrap: wrap;
  301. gap: 4px 8px;
  302. margin-bottom: 6px;
  303. }
  304. .store-row__rating .stars {
  305. display: inline-flex;
  306. align-items: center;
  307. gap: 2px;
  308. color: var(--orange);
  309. }
  310. .store-row__rating .star {
  311. width: 12px;
  312. height: 12px;
  313. color: var(--orange);
  314. }
  315. .store-row__rating .rating-num {
  316. font-size: 13px;
  317. font-weight: 600;
  318. color: var(--orange);
  319. }
  320. .store-row__meta {
  321. font-size: 12px;
  322. color: var(--text-secondary);
  323. }
  324. .store-row__cat {
  325. font-size: 12px;
  326. color: var(--text-secondary);
  327. margin-bottom: 4px;
  328. }
  329. .store-row__tagline {
  330. font-size: 13px;
  331. color: var(--text);
  332. line-height: 1.45;
  333. }
  334. /* 评价 */
  335. .comment-card__title {
  336. font-size: 15px;
  337. font-weight: 700;
  338. color: var(--text);
  339. margin-bottom: 20px;
  340. }
  341. .comment-empty {
  342. display: flex;
  343. flex-direction: column;
  344. align-items: center;
  345. justify-content: center;
  346. padding: 28px 16px 20px;
  347. color: #aaa;
  348. font-size: 14px;
  349. }
  350. .comment-empty__icon {
  351. width: 56px;
  352. height: 56px;
  353. margin-bottom: 12px;
  354. object-fit: contain;
  355. display: block;
  356. }
  357. .comment-list {
  358. display: none;
  359. }
  360. .comment-list.is-visible {
  361. display: block;
  362. }
  363. .comment-item {
  364. display: flex;
  365. align-items: flex-start;
  366. gap: 10px;
  367. padding: 12px 0;
  368. border-top: 1px solid var(--border);
  369. }
  370. .comment-item:first-child {
  371. border-top: none;
  372. padding-top: 0;
  373. }
  374. .comment-item__avatar {
  375. width: 40px;
  376. height: 40px;
  377. border-radius: 50%;
  378. object-fit: cover;
  379. flex-shrink: 0;
  380. background: #eee;
  381. }
  382. .comment-item__col {
  383. flex: 1;
  384. min-width: 0;
  385. }
  386. .comment-item__meta {
  387. font-size: 13px;
  388. color: var(--text-secondary);
  389. margin-bottom: 6px;
  390. }
  391. .comment-item__name {
  392. font-weight: 600;
  393. color: var(--text);
  394. }
  395. .comment-item__text {
  396. font-size: 14px;
  397. line-height: 1.55;
  398. color: var(--text);
  399. word-break: break-word;
  400. margin-bottom: 4px;
  401. }
  402. .comment-item__time {
  403. display: block;
  404. font-size: 12px;
  405. font-weight: 400;
  406. color: var(--text-secondary);
  407. margin-top: 2px;
  408. }
  409. /* 底部按钮 */
  410. .fab-wrap {
  411. position: fixed;
  412. left: 0;
  413. right: 0;
  414. bottom: 0;
  415. z-index: 200;
  416. padding: 12px 24px calc(12px + var(--safe-bottom));
  417. background: linear-gradient(to top, rgba(255, 255, 255, 0.98) 70%, transparent);
  418. pointer-events: none;
  419. }
  420. .fab-wrap .fab {
  421. pointer-events: auto;
  422. }
  423. .fab {
  424. display: flex;
  425. align-items: center;
  426. justify-content: center;
  427. gap: 10px;
  428. width: 100%;
  429. max-width: 320px;
  430. margin: 0 auto;
  431. height: 48px;
  432. border: none;
  433. border-radius: 24px;
  434. background: var(--orange);
  435. color: #fff;
  436. font-size: 16px;
  437. font-weight: 600;
  438. box-shadow: 0 4px 16px rgba(245, 130, 32, 0.45);
  439. cursor: pointer;
  440. background: #F47D1F;
  441. }
  442. .fab__logo {
  443. width: 28px;
  444. height: 28px;
  445. flex-shrink: 0;
  446. }
  447. .home-indicator {
  448. height: 5px;
  449. background: #000;
  450. border-radius: 3px;
  451. width: 134px;
  452. margin: 8px auto 4px;
  453. opacity: 0.2;
  454. }
  455. </style>
  456. </head>
  457. <body class="page-dynamic">
  458. <div class="share-none" id="shareClosedBanner" aria-hidden="true">
  459. <img src="images/storeNone.png" alt="">
  460. <p>抱歉,商户已关闭,看看别的吧</p>
  461. </div>
  462. <div class="closed-rec-wrap" id="shareClosedRecommendWrap" aria-hidden="true">
  463. <div class="closed-rec-divider"></div>
  464. <h3 class="closed-rec-title">更多推荐</h3>
  465. <div class="closed-rec-grid" id="shareClosedRecList">
  466. <p id="shareClosedRecEmpty" class="closed-rec-empty" style="display:none;">暂无推荐</p>
  467. </div>
  468. </div>
  469. <div id="dynPageMain">
  470. <!-- 顶部配图:含 .mp4 时只展示首帧静图(不播放视频)、不轮播;纯图片可多图轮播。 -->
  471. <div class="dyn-hero" id="dynHero">
  472. <div class="dyn-hero__track" id="dynHeroTrack"></div>
  473. <div class="dyn-hero__dots" id="dynHeroDots" aria-hidden="true"></div>
  474. </div>
  475. <section class="dyn-user">
  476. <div class="dyn-user__row">
  477. <img class="dyn-user__avatar" id="userAvatar" src="images/hero.png" alt="">
  478. <div>
  479. <div class="dyn-user__name" id="userName">用户名称</div>
  480. <p class="dyn-user__desc" id="userDesc">文案描述文案描述文案描述文案描述文案描述文案描述文案描述文案描述文案描述文案描述文案描述</p>
  481. </div>
  482. </div>
  483. </section>
  484. <div class="dyn-card store-card">
  485. <div class="store-row">
  486. <img class="store-row__thumb" id="storeThumb" src="images/dynamic.png" alt="">
  487. <div class="store-row__main">
  488. <div class="store-row__title" id="storeTitle">Sober</div>
  489. <div class="store-row__rating">
  490. <span class="stars" aria-hidden="true">
  491. <svg class="star" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>
  492. <svg class="star" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>
  493. <svg class="star" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>
  494. <svg class="star" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>
  495. <svg class="star" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>
  496. </span>
  497. <span class="rating-num" id="storeScore">4.9</span>
  498. <span class="store-row__meta"><span id="storeReviews">200</span>条</span>
  499. </div>
  500. </div>
  501. </div>
  502. </div>
  503. <div class="dyn-card comment-card">
  504. <div class="comment-card__title">评价 <span id="commentCount" style="color:#aaa">(0)</span></div>
  505. <div id="commentList" class="comment-list" aria-live="polite"></div>
  506. <div class="comment-empty" id="commentEmpty">
  507. <img class="comment-empty__icon" src="images/zwpl.png" alt="" width="56" height="56" decoding="async">
  508. <span>暂无评论</span>
  509. </div>
  510. </div>
  511. </div>
  512. <div class="fab-wrap">
  513. <button type="button" class="fab" id="openApp">
  514. <img src="images/uBtn.png" alt="APP内打开" decoding="async">
  515. </button>
  516. <div class="home-indicator" aria-hidden="true"></div>
  517. </div>
  518. <script>
  519. (function () {
  520. 'use strict';
  521. var API_BASE = 'https://test.ailien.shop/alienStore';
  522. /**
  523. * 暂无承载数据时更多推荐:POST …/ai/multimodal-services/api/v1/search/global/store-recommend
  524. * 与 shareIndex.html / shareCheckInUndefined.html 一致。
  525. */
  526. var API_LIFE_AI_BASE = 'http://124.93.18.180:9100';
  527. var STORE_GLOBAL_RECOMMEND_PATH =
  528. '/ai/multimodal-services/api/v1/search/global/store-recommend';
  529. var DEFAULT_REC_USER_LAT = 38.925747;
  530. var DEFAULT_REC_USER_LNG = 121.662531;
  531. var DEFAULT_REC_USER_CITY = '大连市';
  532. var COMMENT_PAGE_NUM = 1;
  533. var COMMENT_PAGE_SIZE = 20;
  534. var COMMENT_AVATAR_FALLBACK = 'images/demouser.png';
  535. function apiFetch(path) {
  536. return fetch(API_BASE + path, {
  537. method: 'GET',
  538. mode: 'cors',
  539. credentials: 'omit',
  540. headers: { Accept: 'application/json' }
  541. }).then(function (res) {
  542. if (!res.ok) throw new Error('HTTP ' + res.status);
  543. return res.json();
  544. });
  545. }
  546. /**
  547. * 与 shareIndex / group_user manifest 一致
  548. * 动态详情默认落地:pages/newdetails/index(onLoad 使用 item 等,与当前页合并 query 一致)
  549. * 打开其它 App 页:H5 URL 上带 appPath 或 appPage,如 ?appPath=pages%2FcheckIn%2Findex(不要前导 /)
  550. * 关店 businessStatus=99:getOne 置 closedMerchantFlag 或 URL 带 businessStatus=99 时,「APP内打开」深链为
  551. * shopro://pages/index/login?…(与 shareIndex.html、group_user pages/index/login 一致)。
  552. */
  553. var APP_ANDROID_PACKAGE = 'com.alien.Udianzaizhe';
  554. var APP_IOS_URL_SCHEME = 'shopro://';
  555. var APP_UNI_STORE_PATH = 'pages/newdetails/index';
  556. function showDownloadTip() {
  557. var msg = '请到应用商店下载';
  558. if (typeof uni !== 'undefined' && typeof uni.showToast === 'function') {
  559. uni.showToast({ title: msg, icon: 'none', duration: 2500 });
  560. } else {
  561. window.alert(msg);
  562. }
  563. }
  564. /**
  565. * 唤起 App 时携带当前 H5 全部查询参数:先 location.search,再 hash 中 ? 后一段;
  566. * 逐项 append,避免丢参、并保留重名键顺序(与 getMergedQueryString 合并规则一致)。
  567. */
  568. function mergeSearchAndHashParams() {
  569. var params = new URLSearchParams();
  570. function ingestAppend(querySlice) {
  571. if (!querySlice) return;
  572. var p = new URLSearchParams(querySlice);
  573. p.forEach(function (val, key) {
  574. params.append(key, val);
  575. });
  576. }
  577. function ingestSet(querySlice) {
  578. if (!querySlice) return;
  579. var p = new URLSearchParams(querySlice);
  580. p.forEach(function (val, key) {
  581. params.set(key, val);
  582. });
  583. }
  584. if (location.search && location.search.length > 1) {
  585. ingestAppend(location.search.slice(1));
  586. }
  587. var hash = location.hash || '';
  588. var qi = hash.indexOf('?');
  589. if (qi >= 0) {
  590. /** hash 内 query 覆盖 search(如 localhost:5173/#/pages/...?item= 场景) */
  591. ingestSet(hash.slice(qi + 1));
  592. }
  593. return params;
  594. }
  595. /**
  596. * hash 形如 #/pages/newdetails/index?item=...&needShowMore=... 时取出 Uni 页面路径(与 App 内 route 一致)
  597. */
  598. function extractUniPagePathFromHash() {
  599. var h = location.hash || '';
  600. if (h.length < 3 || h.charAt(0) !== '#' || h.charAt(1) !== '/') return '';
  601. var rest = h.slice(2).trim();
  602. if (!rest) return '';
  603. var qpos = rest.indexOf('?');
  604. var pathPart = (qpos >= 0 ? rest.slice(0, qpos) : rest).trim();
  605. if (!pathPart || pathPart.indexOf('pages/') !== 0) return '';
  606. return pathPart.replace(/^\/+/, '');
  607. }
  608. function buildAppOpenQueryStringMerged() {
  609. var params = mergeSearchAndHashParams();
  610. /** newdetails onLoad:仅当地址栏未带 item 时补全;同时按需补顶层 imagePath */
  611. if (!params.has('item')) {
  612. var itemObj = parseOptionsItem();
  613. var imagePath = getMergedParam('imagePath');
  614. if (!imagePath && itemObj && itemObj.imagePath != null && String(itemObj.imagePath).trim() !== '') {
  615. imagePath = String(itemObj.imagePath);
  616. }
  617. if (!imagePath) {
  618. var carouselUrls = collectImageUrlsFromUrl();
  619. if (carouselUrls && carouselUrls.length) {
  620. imagePath = carouselUrls.join(',');
  621. }
  622. } else {
  623. imagePath = normalizeMediaUrl(imagePath) || imagePath;
  624. }
  625. var dynId =
  626. getMergedParam('sourceId') ||
  627. getMergedParam('dynamicId') ||
  628. getMergedParam('id') ||
  629. q('id');
  630. if (!dynId && itemObj && itemObj.id != null && String(itemObj.id).trim() !== '') {
  631. dynId = String(itemObj.id);
  632. }
  633. var base = {};
  634. if (itemObj) {
  635. try {
  636. base = JSON.parse(JSON.stringify(itemObj));
  637. } catch (eCopy) {
  638. base = {};
  639. }
  640. }
  641. if (imagePath) {
  642. base.imagePath = imagePath;
  643. }
  644. if (dynId != null && String(dynId).trim() !== '' && base.id == null) {
  645. base.id = dynId;
  646. }
  647. if (Object.keys(base).length) {
  648. try {
  649. params.set('item', JSON.stringify(base));
  650. } catch (eItem) {}
  651. }
  652. }
  653. if (!params.has('imagePath')) {
  654. var ipTop = getMergedParam('imagePath');
  655. if (!ipTop) {
  656. var it2 = parseOptionsItem();
  657. if (it2 && it2.imagePath != null && String(it2.imagePath).trim() !== '') {
  658. ipTop = String(it2.imagePath);
  659. }
  660. }
  661. if (!ipTop) {
  662. var cu = collectImageUrlsFromUrl();
  663. if (cu && cu.length) {
  664. ipTop = cu.join(',');
  665. }
  666. }
  667. if (ipTop) {
  668. params.set('imagePath', normalizeMediaUrl(ipTop) || ipTop);
  669. }
  670. }
  671. var sid = params.get('storeId') || params.get('id') || '';
  672. if (sid && !params.has('storeId')) {
  673. params.set('storeId', sid);
  674. }
  675. /** 顶层 imagePath 若未 encode,URLSearchParams 会截断在 OSS 的 &fm= 等处;用原始串解析结果覆盖 */
  676. var ipMerged = getMergedParam('imagePath');
  677. if (ipMerged) {
  678. params.set('imagePath', normalizeMediaUrl(ipMerged) || ipMerged);
  679. }
  680. /**
  681. * 与 App utils/shareVideoPrecache.js 一致:无 shareVideoCk 时从页面解析首条 https .mp4,写入唤起链接,
  682. * 便于 App 端 downloadFile 预缓存并与 hash 对齐后优先本地播放。
  683. */
  684. try {
  685. var ckPrev = params.get('shareVideoCk');
  686. if (!ckPrev || String(ckPrev).trim() === '') {
  687. var pvid = pickFirstHttpsMp4ForShareVideoPrecache();
  688. if (pvid) {
  689. var hck = hashShareVideoRemoteUrl(pvid);
  690. if (hck) {
  691. params.set('shareVideoCk', hck);
  692. }
  693. }
  694. }
  695. } catch (eShareCk) {}
  696. /**
  697. * App 内无分享 H5 写入的 newdetailsList;若沿用浏览器 URL 里的 fromHomeFeed=1,
  698. * 详情会走「首页种子」分支并禁止 recommend,表现为仅一条、下滑无更多作品。
  699. */
  700. params.delete('fromHomeFeed');
  701. var qsOut = params.toString();
  702. return qsOut ? ('?' + qsOut) : '';
  703. }
  704. /**
  705. * 真机系统浏览器对超长 shopro:// 常截断;去掉超大 item,用 id/type 让 App 内再拉详情。
  706. */
  707. /** getOne 返回关店后置位;与 shareIndex.html closedMerchantFlag 一致 */
  708. var closedMerchantFlag = false;
  709. function isGetOneBusinessStatus99(res) {
  710. if (!res || typeof res !== 'object') return false;
  711. var d = res.data;
  712. if (d && typeof d === 'object') {
  713. var bs = d.businessStatus;
  714. if (bs === 99 || bs === '99' || Number(bs) === 99) return true;
  715. }
  716. return false;
  717. }
  718. /**
  719. * 关店 businessStatus=99:getOne 已置 closedMerchantFlag,或 URL(含 hash)带 businessStatus=99;
  720. * 唤起 App 时深链进 pages/index/login(与 shareIndex.html getAppUniPathForBusinessSection 一致)。
  721. */
  722. function isClosedMerchantForAppOpen() {
  723. if (closedMerchantFlag) return true;
  724. try {
  725. var bs = String(mergeSearchAndHashParams().get('businessStatus') || '').trim();
  726. if (bs === '99' || Number(bs) === 99) return true;
  727. } catch (e0) {}
  728. return false;
  729. }
  730. function compactShoproDeepLinkIfTooLong(fullUrl, maxLen) {
  731. maxLen = maxLen || 7200;
  732. if (!fullUrl || fullUrl.length <= maxLen) return fullUrl;
  733. var scheme = 'shopro://';
  734. if (fullUrl.indexOf(scheme) !== 0) return fullUrl;
  735. var after = fullUrl.slice(scheme.length).replace(/^\/+/, '');
  736. var qI = after.indexOf('?');
  737. var pathPart = qI >= 0 ? after.slice(0, qI) : after;
  738. var queryPart = qI >= 0 ? after.slice(qI + 1) : '';
  739. if (!queryPart) return fullUrl;
  740. var sp = new URLSearchParams(queryPart);
  741. var item = sp.get('item');
  742. if (!item || item.length < 800) return fullUrl;
  743. var idm = /"id"\s*:\s*(\d+)/.exec(item);
  744. if (idm) {
  745. if (!sp.get('dynamicId')) sp.set('dynamicId', idm[1]);
  746. if (!sp.get('sourceId')) sp.set('sourceId', idm[1]);
  747. }
  748. var tpm = /"type"\s*:\s*"(\d+)"/.exec(item) || /"type"\s*:\s*(\d+)/.exec(item);
  749. if (tpm && !sp.get('sourceType')) sp.set('sourceType', tpm[1]);
  750. sp.delete('item');
  751. var qs = sp.toString();
  752. var out = scheme + pathPart + (qs ? ('?' + qs) : '');
  753. return out.length <= maxLen ? out : fullUrl;
  754. }
  755. function buildAppDeepLink() {
  756. if (isClosedMerchantForAppOpen()) {
  757. var sClosed = buildAppOpenQueryStringMerged();
  758. var rootClosed = APP_IOS_URL_SCHEME.replace(/\/$/, '');
  759. var pathClosed = 'pages/index/login'.replace(/^\//, '');
  760. var rawClosed = !sClosed ? rootClosed + '/' + pathClosed : rootClosed + '/' + pathClosed + sClosed;
  761. return compactShoproDeepLinkIfTooLong(rawClosed, 7200);
  762. }
  763. var explicit = (getMergedParam('appPath') || getMergedParam('appPage') || '').trim().replace(/^\//, '');
  764. var fromHash = extractUniPagePathFromHash();
  765. var defaultPath = String(APP_UNI_STORE_PATH || '/pages/newdetails/index').replace(/^\//, '');
  766. /**
  767. * 不要用「任意 pages/ 的 hash」当 App 路径:分享页常在 #/pages/shareDynamic?…,
  768. * 会误唤起成该页而不是 newdetails。仅当 hash 明确含 newdetails 时才采用 hash 路径。
  769. */
  770. var path = explicit;
  771. if (!path && fromHash && /newdetails/i.test(fromHash)) {
  772. path = fromHash.replace(/^\//, '');
  773. }
  774. if (!path) {
  775. path = defaultPath;
  776. }
  777. path = String(path || defaultPath).replace(/^\//, '');
  778. var s = buildAppOpenQueryStringMerged();
  779. var root = APP_IOS_URL_SCHEME.replace(/\/$/, '');
  780. var raw = !s ? root + '/' + path : root + '/' + path + s;
  781. return compactShoproDeepLinkIfTooLong(raw, 7200);
  782. }
  783. function isWeChatInAppBrowser() {
  784. return /MicroMessenger/i.test(navigator.userAgent || '');
  785. }
  786. /** 微信内置浏览器、iOS:OSS 截图/封面易因 Referer 被拒;video 首帧不可靠 */
  787. function preferStaticStillOverVideo() {
  788. return isIOSVideoEnv() || isWeChatInAppBrowser();
  789. }
  790. /** 减轻微信/X5 对阿里云 OSS 图片、video poster 的 Referer 拦截 */
  791. function applyMediaNoReferrer(el) {
  792. if (!el) return;
  793. try {
  794. el.setAttribute('referrerpolicy', 'no-referrer');
  795. if ('referrerPolicy' in el) {
  796. el.referrerPolicy = 'no-referrer';
  797. }
  798. } catch (e) {}
  799. }
  800. function launchAppDeepLink(deepLink) {
  801. try {
  802. var a = document.createElement('a');
  803. a.href = deepLink;
  804. a.setAttribute('target', '_self');
  805. document.body.appendChild(a);
  806. a.click();
  807. document.body.removeChild(a);
  808. } catch (e1) {}
  809. try {
  810. window.location.href = deepLink;
  811. } catch (e2) {}
  812. }
  813. function tryOpenHBuilderApp() {
  814. var runOpen = function () {
  815. var deepLink = buildAppDeepLink();
  816. if (typeof plus !== 'undefined' && plus.runtime) {
  817. var installed = null;
  818. try {
  819. if (typeof plus.runtime.isApplicationExist === 'function') {
  820. installed = plus.runtime.isApplicationExist({
  821. pname: APP_ANDROID_PACKAGE,
  822. action: APP_IOS_URL_SCHEME
  823. });
  824. }
  825. } catch (e) {
  826. console.warn(e);
  827. }
  828. /**
  829. * 不在「未安装」时直接弹下载:部分 ROM 对 isApplicationExist 误判为 false,
  830. * 仍应尝试 openURL,避免一点按钮就「请到应用商店下载」。
  831. */
  832. try {
  833. plus.runtime.openURL(deepLink);
  834. } catch (e2) {
  835. console.warn(e2);
  836. if (installed === false) {
  837. showDownloadTip();
  838. }
  839. }
  840. return;
  841. }
  842. var done = false;
  843. function finish() {
  844. if (done) return;
  845. done = true;
  846. document.removeEventListener('visibilitychange', onVis);
  847. window.removeEventListener('pagehide', onHide);
  848. }
  849. function onVis() {
  850. if (document.visibilityState === 'hidden') finish();
  851. }
  852. function onHide() {
  853. finish();
  854. }
  855. document.addEventListener('visibilitychange', onVis);
  856. window.addEventListener('pagehide', onHide);
  857. if (isWeChatInAppBrowser()) {
  858. window.alert('若点击后无法打开 App:请先点右上角「···」,选择「在浏览器中打开」,再点「APP内打开」。');
  859. }
  860. try {
  861. launchAppDeepLink(deepLink);
  862. } catch (e3) {
  863. finish();
  864. showDownloadTip();
  865. return;
  866. }
  867. /**
  868. * 不在超时后弹「去应用商店」:App 已打开时页面常仍 visible,易误报;
  869. * 若未安装,用户无反应可自行去商店,避免打断操作。
  870. */
  871. window.setTimeout(function () {
  872. finish();
  873. }, 3200);
  874. };
  875. var pf = prefetchShareVideoBeforeAppOpen();
  876. if (pf && typeof pf.then === 'function') {
  877. pf.then(runOpen).catch(function () {
  878. runOpen();
  879. });
  880. } else {
  881. runOpen();
  882. }
  883. }
  884. function qs() {
  885. return new URLSearchParams(location.search || '');
  886. }
  887. function q(name) {
  888. var v = qs().get(name);
  889. return v == null ? '' : String(v);
  890. }
  891. /** 与 mergeSearchAndHashParams 一致:hash 内 query 覆盖 search,供 getMergedParam 与 App 唤起共用 */
  892. function getMergedQueryString() {
  893. var m = mergeSearchAndHashParams().toString();
  894. return m || '';
  895. }
  896. function forEachQueryParam(queryStr, fn) {
  897. if (!queryStr) return;
  898. var p = new URLSearchParams(queryStr);
  899. p.forEach(function (val, key) {
  900. fn(val, key);
  901. });
  902. }
  903. /**
  904. * App 分享链接里 URL 类参数常未 encode,值里的 &(如 OSS 的 &fm=)会被拆成多个 query;
  905. * 从合并后的原始 query 串中按「下一个已知分享参数名」截取整段值。
  906. */
  907. var SHARE_QUERY_URL_PARAM_KEYS = [
  908. 'options',
  909. 'item',
  910. 'sourceType',
  911. 'businessType',
  912. 'sourceId',
  913. 'dynamicId',
  914. 'id',
  915. 'userId',
  916. 'userImage',
  917. 'coverUrl',
  918. 'imagePath',
  919. 'imagePaths',
  920. 'images',
  921. 'image_path',
  922. 'storeName',
  923. 'scoreAvg',
  924. 'commentCount',
  925. 'shareUserName',
  926. 'storeId',
  927. 'content',
  928. 'desc',
  929. 'text',
  930. 'tagline',
  931. 'category',
  932. 'storeCat',
  933. 'userName',
  934. 'phoneId',
  935. 'longitude',
  936. 'latitude',
  937. 'goodsId',
  938. 'pageNum',
  939. 'pageSize',
  940. 'needShowMore',
  941. 'fromHomeFeed',
  942. 'appPath',
  943. 'appPage',
  944. 'shareVideoCk'
  945. ];
  946. function findShareQueryValueStart(q, paramName) {
  947. var nlow = String(paramName || '').toLowerCase() + '=';
  948. var low = String(q || '').toLowerCase();
  949. var i = low.indexOf('&' + nlow);
  950. if (i >= 0) return i + 1 + nlow.length;
  951. i = low.indexOf('?' + nlow);
  952. if (i >= 0) return i + 1 + nlow.length;
  953. if (low.indexOf(nlow) === 0) return nlow.length;
  954. return -1;
  955. }
  956. function extractUrlLikeShareQueryParam(fullQuery, paramName) {
  957. var nm = String(paramName || '').trim();
  958. if (!nm || !fullQuery) return '';
  959. var start = findShareQueryValueStart(fullQuery, nm);
  960. if (start < 0) return '';
  961. var rest = fullQuery.slice(start);
  962. var nml = nm.toLowerCase();
  963. var keys = SHARE_QUERY_URL_PARAM_KEYS.filter(function (k) {
  964. return String(k).toLowerCase() !== nml;
  965. });
  966. keys.sort(function (a, b) {
  967. return b.length - a.length;
  968. });
  969. if (!keys.length) {
  970. try {
  971. return decodeURIComponent(rest.replace(/\+/g, ' '));
  972. } catch (e) {
  973. return rest;
  974. }
  975. }
  976. var esc = keys.map(function (k) {
  977. return String(k).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  978. });
  979. var re = new RegExp('&(' + esc.join('|') + ')=', 'i');
  980. var m = re.exec(rest);
  981. var end = m ? m.index : rest.length;
  982. var rawVal = rest.slice(0, end).replace(/\+/g, ' ');
  983. try {
  984. return decodeURIComponent(rawVal);
  985. } catch (e2) {
  986. return rawVal;
  987. }
  988. }
  989. function getAllParamValuesCI(queryStr, nameLower) {
  990. if (!queryStr) return [];
  991. if (nameLower === 'imagepath' || nameLower === 'imagepaths') {
  992. var one =
  993. extractUrlLikeShareQueryParam(queryStr, 'imagePath') ||
  994. (nameLower === 'imagepaths'
  995. ? extractUrlLikeShareQueryParam(queryStr, 'imagePaths')
  996. : '');
  997. if (one) return [one];
  998. }
  999. var list = [];
  1000. forEachQueryParam(queryStr, function (val, key) {
  1001. if (key && String(key).toLowerCase() === nameLower) {
  1002. list.push(val);
  1003. }
  1004. });
  1005. return list;
  1006. }
  1007. function getMergedParam(name) {
  1008. var m = getMergedQueryString();
  1009. if (!m) return '';
  1010. var n = String(name);
  1011. var nl = n.toLowerCase();
  1012. if (nl === 'imagepath' || nl === 'coverurl' || nl === 'userimage') {
  1013. var smart = extractUrlLikeShareQueryParam(m, n);
  1014. if (smart) return smart;
  1015. }
  1016. var v = new URLSearchParams(m).get(name);
  1017. if (v == null) {
  1018. new URLSearchParams(m).forEach(function (val, key) {
  1019. if (v == null && key && String(key).toLowerCase() === nl) {
  1020. v = val;
  1021. }
  1022. });
  1023. }
  1024. return v == null ? '' : String(v);
  1025. }
  1026. /**
  1027. * getDeleteFlagById:data 为数字 1(旧格式),或 data.businessStatus 为 99 时进「已删除」落地页。
  1028. * 与 shareCheckIn.html 一致。
  1029. */
  1030. function shouldRedirectToShareCheckInUndefined(res) {
  1031. if (!res || typeof res !== 'object') return false;
  1032. var d = res.data;
  1033. if (d === 1 || d === '1' || Number(d) === 1) return true;
  1034. if (d && typeof d === 'object') {
  1035. var bs = d.businessStatus;
  1036. if (bs === 99 || bs === '99' || Number(bs) === 99) return true;
  1037. }
  1038. return false;
  1039. }
  1040. /** 当前页合并 query + 接口返回的 storeId、businessStatus(若有)供 shareCheckInUndefined 使用 */
  1041. function buildShareCheckInUndefinedHref(res) {
  1042. var params = new URLSearchParams(getMergedQueryString());
  1043. if (res && res.data && typeof res.data === 'object') {
  1044. if (res.data.storeId != null) {
  1045. var sid = String(res.data.storeId).trim();
  1046. if (sid) params.set('storeId', sid);
  1047. }
  1048. if (res.data.businessStatus != null && String(res.data.businessStatus).trim() !== '') {
  1049. params.set('businessStatus', String(res.data.businessStatus).trim());
  1050. }
  1051. }
  1052. var m = params.toString();
  1053. return 'shareCheckInUndefined.html' + (m ? ('?' + m) : '');
  1054. }
  1055. function tryParseJsonObject(raw) {
  1056. raw = String(raw == null ? '' : raw).trim();
  1057. if (!raw) return null;
  1058. try {
  1059. var o = JSON.parse(raw);
  1060. return o && typeof o === 'object' && !Array.isArray(o) ? o : null;
  1061. } catch (e) {
  1062. return null;
  1063. }
  1064. }
  1065. /**
  1066. * 与 App buildDynamicShareH5FullUrl 一致:店铺 id 常在整段 item= / dynamicItem= JSON 里,
  1067. * 顶层未必带 storeId(sourceId 是动态 id,不能当店铺传给 getDeleteFlagById)。
  1068. */
  1069. function parseShareDynamicItemBlob() {
  1070. var a = tryParseJsonObject(getMergedParam('item'));
  1071. if (a) return a;
  1072. return tryParseJsonObject(getMergedParam('dynamicItem'));
  1073. }
  1074. /**
  1075. * getDeleteFlagById 要求 query 参数名为 id,值为店铺 storeId(与后端约定一致)。
  1076. */
  1077. function resolveStoreIdForDeleteFlagApi() {
  1078. var sid =
  1079. getMergedParam('storeId').trim() ||
  1080. q('storeId').trim();
  1081. if (!sid) {
  1082. var optItem = parseOptionsItem();
  1083. if (optItem && optItem.storeId != null && String(optItem.storeId).trim() !== '') {
  1084. sid = String(optItem.storeId).trim();
  1085. }
  1086. }
  1087. if (!sid) {
  1088. var blob = parseShareDynamicItemBlob();
  1089. if (blob) {
  1090. if (blob.storeId != null && String(blob.storeId).trim() !== '') {
  1091. sid = String(blob.storeId).trim();
  1092. } else if (blob.store_id != null && String(blob.store_id).trim() !== '') {
  1093. sid = String(blob.store_id).trim();
  1094. }
  1095. }
  1096. }
  1097. return sid;
  1098. }
  1099. /** 进入页先拉删除标记;如 getDeleteFlagById?id=<storeId> */
  1100. function fetchGetDeleteFlagByIdIfId() {
  1101. var id = resolveStoreIdForDeleteFlagApi();
  1102. if (!id) return Promise.resolve(null);
  1103. var qs = new URLSearchParams();
  1104. qs.set('id', id);
  1105. var path = '/store/info/getOne?' + qs.toString();
  1106. return apiFetch(path)
  1107. .then(function (res) {
  1108. return res;
  1109. })
  1110. .catch(function (e) {
  1111. console.warn('[getDeleteFlagById]', e);
  1112. return null;
  1113. });
  1114. }
  1115. function isNoCarryingDataDeleteFlagMsg(res) {
  1116. if (!res || typeof res !== 'object') return false;
  1117. var tip = res.msg != null ? String(res.msg).trim() : '';
  1118. return tip === '暂无承载数据';
  1119. }
  1120. var CLOSED_REC_STAR_PATH =
  1121. 'M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z';
  1122. function closedRecEscHtml(s) {
  1123. return String(s == null ? '' : s)
  1124. .replace(/&/g, '&amp;')
  1125. .replace(/</g, '&lt;')
  1126. .replace(/>/g, '&gt;')
  1127. .replace(/"/g, '&quot;');
  1128. }
  1129. function closedRecStarsHtml(score) {
  1130. var s = Number(score);
  1131. if (isNaN(s)) s = 0;
  1132. s = Math.max(0, Math.min(5, s));
  1133. var rounded = Math.round(s);
  1134. var parts = [];
  1135. var i;
  1136. for (i = 1; i <= 5; i++) {
  1137. var fill = i <= rounded ? '#F58220' : '#E5E5E5';
  1138. parts.push(
  1139. '<svg class="closed-rec-star" width="11" height="11" viewBox="0 0 24 24" aria-hidden="true">' +
  1140. '<path fill="' + fill + '" d="' + CLOSED_REC_STAR_PATH + '"/></svg>'
  1141. );
  1142. }
  1143. return '<span class="closed-rec-stars">' + parts.join('') + '</span>';
  1144. }
  1145. function closedRecFormatDistance(item) {
  1146. if (item.distance != null && item.distance !== '') {
  1147. var km = Number(item.distance);
  1148. if (!isNaN(km) && km >= 0) {
  1149. return Math.round(km * 1000) + '米';
  1150. }
  1151. return String(item.distance).trim();
  1152. }
  1153. if (item.position != null && String(item.position).trim() !== '') {
  1154. return String(item.position).trim();
  1155. }
  1156. if (item.dist != null && item.dist !== '') {
  1157. var dn = Number(item.dist);
  1158. if (!isNaN(dn)) {
  1159. return dn >= 1 ? dn.toFixed(dn % 1 === 0 ? 0 : 1) + 'km' : Math.round(dn * 1000) + '米';
  1160. }
  1161. }
  1162. return '';
  1163. }
  1164. function closedRecPickScore(item) {
  1165. var x =
  1166. item.scoreAvg != null ? Number(item.scoreAvg)
  1167. : item.score != null ? Number(item.score)
  1168. : item.rating != null ? Number(item.rating)
  1169. : item.starScore != null ? Number(item.starScore)
  1170. : NaN;
  1171. return isNaN(x) ? null : x;
  1172. }
  1173. function closedRecPickReviewCount(item) {
  1174. var n =
  1175. item.commitCount != null ? Number(item.commitCount)
  1176. : item.commentCount != null ? Number(item.commentCount)
  1177. : item.reviewCount != null ? Number(item.reviewCount)
  1178. : item.evaluateCount != null ? Number(item.evaluateCount)
  1179. : NaN;
  1180. return isNaN(n) ? 0 : Math.max(0, Math.floor(n));
  1181. }
  1182. function normalizeClosedStoreRecommendList(res) {
  1183. if (!res || typeof res !== 'object') return [];
  1184. var raw = res.data != null ? res.data : res.result;
  1185. if (Array.isArray(raw)) return raw;
  1186. if (raw && typeof raw === 'object') {
  1187. if (Array.isArray(raw.list)) return raw.list;
  1188. if (Array.isArray(raw.records)) return raw.records;
  1189. if (Array.isArray(raw.rows)) return raw.rows;
  1190. if (Array.isArray(raw.content)) return raw.content;
  1191. if (Array.isArray(raw.stores)) return raw.stores;
  1192. if (Array.isArray(raw.storeList)) return raw.storeList;
  1193. if (Array.isArray(raw.storeVos)) return raw.storeVos;
  1194. if (Array.isArray(raw.items)) return raw.items;
  1195. }
  1196. if (Array.isArray(res.list)) return res.list;
  1197. if (Array.isArray(res.records)) return res.records;
  1198. return [];
  1199. }
  1200. function fetchShareClosedStoreRecommend(storeIdStr) {
  1201. var latRaw = (q('userLat') || q('latitude') || q('lat') || q('weidu')).trim();
  1202. var lngRaw = (q('userLng') || q('longitude') || q('lon') || q('jingdu')).trim();
  1203. var userLat =
  1204. latRaw !== '' && !isNaN(Number(latRaw)) ? Number(latRaw) : DEFAULT_REC_USER_LAT;
  1205. var userLng =
  1206. lngRaw !== '' && !isNaN(Number(lngRaw)) ? Number(lngRaw) : DEFAULT_REC_USER_LNG;
  1207. var page = parseInt(q('page') || '1', 10);
  1208. var pageSize = parseInt(q('pageSize') || '10', 10);
  1209. if (isNaN(page) || page < 1) page = 1;
  1210. if (isNaN(pageSize) || pageSize < 1) pageSize = 10;
  1211. var userCityRaw = (q('userCity') || q('city') || '').trim();
  1212. var userCity = userCityRaw !== '' ? userCityRaw : DEFAULT_REC_USER_CITY;
  1213. var body = {
  1214. page: page,
  1215. pageSize: pageSize,
  1216. storeId: String(storeIdStr || ''),
  1217. userCity: userCity,
  1218. userLat: userLat,
  1219. userLng: userLng
  1220. };
  1221. return fetch(API_LIFE_AI_BASE + STORE_GLOBAL_RECOMMEND_PATH, {
  1222. method: 'POST',
  1223. mode: 'cors',
  1224. credentials: 'omit',
  1225. headers: {
  1226. Accept: 'application/json',
  1227. 'Content-Type': 'application/json;charset=UTF-8'
  1228. },
  1229. body: JSON.stringify(body)
  1230. }).then(function (res) {
  1231. if (!res.ok) throw new Error('HTTP ' + res.status);
  1232. return res.json();
  1233. });
  1234. }
  1235. function renderShareClosedRecommended(list) {
  1236. var wrap = document.getElementById('shareClosedRecList');
  1237. var empty = document.getElementById('shareClosedRecEmpty');
  1238. if (!wrap || !empty) return;
  1239. wrap.querySelectorAll('.closed-rec-card').forEach(function (n) {
  1240. n.remove();
  1241. });
  1242. if (!list || !list.length) {
  1243. empty.style.display = 'block';
  1244. return;
  1245. }
  1246. empty.style.display = 'none';
  1247. list.forEach(function (item) {
  1248. if (!item || typeof item !== 'object') return;
  1249. var imgUrlField = item.imgUrl != null ? String(item.imgUrl).trim() : '';
  1250. var home = item.homeImage != null ? String(item.homeImage).trim() : '';
  1251. if (home && /\.mp4(\?|#|$)/i.test(home)) {
  1252. var vf = item.videoFirstFrame != null ? String(item.videoFirstFrame).trim() : '';
  1253. if (vf) home = vf;
  1254. }
  1255. var imgUrl =
  1256. imgUrlField ||
  1257. home ||
  1258. (item.coverUrl != null ? String(item.coverUrl).trim() : '') ||
  1259. (item.mainImage != null ? String(item.mainImage).trim() : '') ||
  1260. (item.goodsImage != null ? String(item.goodsImage).trim() : '') ||
  1261. (item.entranceImage != null ? String(item.entranceImage).trim() : '') ||
  1262. (Array.isArray(item.goodsImageList) && item.goodsImageList[0]) ||
  1263. (Array.isArray(item.imageList) && item.imageList[0]) ||
  1264. (Array.isArray(item.storeAlbumUrlList) && item.storeAlbumUrlList[0]) ||
  1265. '';
  1266. var name = item.title != null && String(item.title).trim() !== ''
  1267. ? String(item.title).replace(/\r?\n/g, ' ').replace(/\s+/g, ' ').trim()
  1268. : (item.storeName || item.goodsName || item.secondGoodsTitle || item.name || '推荐');
  1269. var dist = closedRecFormatDistance(item);
  1270. var scoreVal = closedRecPickScore(item);
  1271. var displayScore = scoreVal != null ? scoreVal.toFixed(1) : '—';
  1272. var starScore = scoreVal != null ? scoreVal : 0;
  1273. var rc = closedRecPickReviewCount(item);
  1274. var reviewLabel = rc > 0 ? rc + '条评价' : '';
  1275. var seller =
  1276. item.userName != null && String(item.userName).trim() !== ''
  1277. ? String(item.userName).trim()
  1278. : (item.nickName != null && String(item.nickName).trim() !== ''
  1279. ? String(item.nickName).trim()
  1280. : '');
  1281. var card = document.createElement('article');
  1282. card.className = 'closed-rec-card';
  1283. card.innerHTML =
  1284. '<div class="closed-rec-card__img"><img class="closed-rec-card__cover" src="" alt="" decoding="async"></div>' +
  1285. '<div class="closed-rec-card__body">' +
  1286. '<div class="closed-rec-card__top">' +
  1287. '<span class="closed-rec-card__name">' + closedRecEscHtml(name) + '</span>' +
  1288. (dist ? '<span class="closed-rec-card__dist">' + closedRecEscHtml(dist) + '</span>' : '') +
  1289. '</div>' +
  1290. '<div class="closed-rec-card__rating">' +
  1291. closedRecStarsHtml(starScore) +
  1292. '<span class="closed-rec-rating-num">' + closedRecEscHtml(displayScore) + '</span>' +
  1293. (reviewLabel ? '<span class="closed-rec-meta">' + closedRecEscHtml(reviewLabel) + '</span>' : '') +
  1294. '</div>' +
  1295. (seller ? '<div class="closed-rec-card__footer">' + closedRecEscHtml(seller) + '</div>' : '') +
  1296. '</div>';
  1297. var coverIm = card.querySelector('img.closed-rec-card__cover');
  1298. if (coverIm) {
  1299. coverIm.src = imgUrl;
  1300. coverIm.onerror = function () {
  1301. this.onerror = null;
  1302. this.src = '';
  1303. };
  1304. }
  1305. wrap.appendChild(card);
  1306. });
  1307. }
  1308. function loadDynNoCarryRecommendations() {
  1309. var sid = resolveStoreIdForDeleteFlagApi();
  1310. if (!sid) {
  1311. renderShareClosedRecommended([]);
  1312. return;
  1313. }
  1314. fetchShareClosedStoreRecommend(sid)
  1315. .then(function (res) {
  1316. var list = normalizeClosedStoreRecommendList(res);
  1317. if (!list.length && res && res.msg) {
  1318. console.warn('[store-recommend]', res.msg);
  1319. }
  1320. renderShareClosedRecommended(list);
  1321. })
  1322. .catch(function (e) {
  1323. console.error(e);
  1324. renderShareClosedRecommended([]);
  1325. });
  1326. }
  1327. /** 关店横幅 + 更多推荐区显隐(与 shareIndex applyShareIndexClosedMerchantUi 的 banner / content 对应关系一致) */
  1328. function setShareDynamicClosedSurfaceDom(visible) {
  1329. var main = document.getElementById('dynPageMain');
  1330. if (main) {
  1331. main.style.display = visible ? 'none' : '';
  1332. main.setAttribute('aria-hidden', visible ? 'true' : 'false');
  1333. }
  1334. var banner = document.getElementById('shareClosedBanner');
  1335. var recWrap = document.getElementById('shareClosedRecommendWrap');
  1336. if (banner) {
  1337. banner.style.display = visible ? 'block' : 'none';
  1338. banner.setAttribute('aria-hidden', visible ? 'false' : 'true');
  1339. }
  1340. if (recWrap) {
  1341. recWrap.style.display = visible ? 'block' : 'none';
  1342. recWrap.setAttribute('aria-hidden', visible ? 'false' : 'true');
  1343. }
  1344. }
  1345. /**
  1346. * store/info/getOne 返回 businessStatus=99:与 shareIndex.html 一致,置 closedMerchantFlag、隐藏 #dynPageMain、
  1347. * 展示 #shareClosedBanner / #shareClosedRecommendWrap 并拉更多推荐。
  1348. */
  1349. function applyShareDynamicClosedMerchantUi(closed) {
  1350. var wasClosed = closedMerchantFlag;
  1351. closedMerchantFlag = closed;
  1352. if (closed) {
  1353. setShareDynamicClosedSurfaceDom(true);
  1354. if (!wasClosed) {
  1355. loadDynNoCarryRecommendations();
  1356. }
  1357. } else {
  1358. setShareDynamicClosedSurfaceDom(false);
  1359. renderShareClosedRecommended([]);
  1360. }
  1361. }
  1362. function showDynNoCarryingDataState() {
  1363. setShareDynamicClosedSurfaceDom(true);
  1364. loadDynNoCarryRecommendations();
  1365. }
  1366. function pushUniqueUrl(list, u) {
  1367. u = String(u == null ? '' : u).trim();
  1368. if (!u) return;
  1369. if (list.indexOf(u) >= 0) return;
  1370. list.push(u);
  1371. }
  1372. function parseOptionsItem() {
  1373. var raw = getMergedParam('options');
  1374. if (!raw) return null;
  1375. var opts;
  1376. try {
  1377. opts = JSON.parse(raw);
  1378. } catch (e) {
  1379. return null;
  1380. }
  1381. if (!opts || typeof opts !== 'object') return null;
  1382. var item = opts.item;
  1383. if (typeof item === 'string' && item) {
  1384. try {
  1385. item = JSON.parse(item);
  1386. } catch (e2) {
  1387. return null;
  1388. }
  1389. }
  1390. if (!item || typeof item !== 'object') return null;
  1391. return item;
  1392. }
  1393. function normalizeMediaUrl(raw) {
  1394. raw = String(raw == null ? '' : raw).trim();
  1395. if (!raw || raw === '0') return '';
  1396. if (/%[0-9A-Fa-f]{2}/.test(raw)) {
  1397. try {
  1398. raw = decodeURIComponent(raw.replace(/\+/g, ' '));
  1399. } catch (e) {}
  1400. }
  1401. return raw;
  1402. }
  1403. /** 媒体 URL 是否以 .mp4 结尾(忽略 query/hash,大小写不敏感) */
  1404. function isMp4VideoUrl(url) {
  1405. if (!url || typeof url !== 'string') return false;
  1406. var path = url.split(/[?#]/)[0].toLowerCase();
  1407. return path.length >= 4 && path.slice(-4) === '.mp4';
  1408. }
  1409. /**
  1410. * 播放用地址:只保留到 .mp4 为止,去掉 ?x-oss-process=video/snapshot... 等(否则会拿到截图流而非视频)
  1411. */
  1412. function getMp4PlaybackUrl(url) {
  1413. var u = normalizeMediaUrl(String(url || ''));
  1414. if (!u) return '';
  1415. var q = u.indexOf('?');
  1416. var h = u.indexOf('#');
  1417. var cut = u.length;
  1418. if (q >= 0) cut = Math.min(cut, q);
  1419. if (h >= 0) cut = Math.min(cut, h);
  1420. var base = u.slice(0, cut);
  1421. if (/\.mp4$/i.test(base)) return base;
  1422. return u;
  1423. }
  1424. function mp4UrlToJpgUrl(u) {
  1425. var q = u.indexOf('?');
  1426. var h = u.indexOf('#');
  1427. var cut = u.length;
  1428. if (q >= 0) cut = Math.min(cut, q);
  1429. if (h >= 0) cut = Math.min(cut, h);
  1430. var pathPart = u.slice(0, cut);
  1431. var rest = u.slice(cut);
  1432. if (!/\.mp4$/i.test(pathPart)) return '';
  1433. return pathPart.slice(0, -4) + '.jpg' + rest;
  1434. }
  1435. function mp4UrlToOssSnapshotUrl(mp4Base) {
  1436. if (!mp4Base) return '';
  1437. return (
  1438. mp4Base +
  1439. (mp4Base.indexOf('?') >= 0 ? '&' : '?') +
  1440. 'x-oss-process=video/snapshot,t_0,f_jpg,m_fast'
  1441. );
  1442. }
  1443. /** 微信内部分 CDN 对 t_0 抽帧异常时,换非零时刻再试 */
  1444. function mp4UrlToOssSnapshotUrlAtMs(mp4Base, ms) {
  1445. if (!mp4Base) return '';
  1446. var t = Math.max(0, parseInt(ms, 10) || 0);
  1447. return (
  1448. mp4Base +
  1449. (mp4Base.indexOf('?') >= 0 ? '&' : '?') +
  1450. 'x-oss-process=video/snapshot,t_' +
  1451. t +
  1452. ',f_jpg,m_fast'
  1453. );
  1454. }
  1455. /** 完整 URL 是否更像「图片/截图」而非纯视频文件 */
  1456. function urlLooksLikeStillImage(u) {
  1457. if (!u) return false;
  1458. var path = u.split(/[?#]/)[0].toLowerCase();
  1459. if (/\.(jpg|jpeg|png|gif|webp|bmp)$/i.test(path)) return true;
  1460. if (/x-oss-process=/i.test(u) && /snapshot|image|f_jpg/i.test(u)) return true;
  1461. return false;
  1462. }
  1463. function isIOSVideoEnv() {
  1464. return (
  1465. /iP(hone|od|ad)/i.test(navigator.userAgent || '') ||
  1466. (navigator.platform === 'MacIntel' && (navigator.maxTouchPoints || 0) > 1)
  1467. );
  1468. }
  1469. function pushUniqueStillUrl(arr, u) {
  1470. u = normalizeMediaUrl(String(u || '').trim());
  1471. if (!u || arr.indexOf(u) >= 0) return;
  1472. arr.push(u);
  1473. }
  1474. /**
  1475. * iOS / 微信内置浏览器:video 首帧与 OSS 截图常灰屏或被 Referer 拦截。
  1476. * 优先静态图链(封面、jpg、多档 OSS snapshot);叠层在 video 之上且不隐藏;失败再回退 video。
  1477. */
  1478. function appendMp4FirstFrameToSlide(slide, src) {
  1479. var full = normalizeMediaUrl(String(src || ''));
  1480. var playUrl = getMp4PlaybackUrl(full);
  1481. if (!playUrl) return;
  1482. var jpg = mp4UrlToJpgUrl(playUrl);
  1483. var snap = mp4UrlToOssSnapshotUrl(playUrl);
  1484. var snapAr =
  1485. playUrl +
  1486. (playUrl.indexOf('?') >= 0 ? '&' : '?') +
  1487. 'x-oss-process=video/snapshot,t_0,f_jpg,ar_auto,m_fast';
  1488. var poster = '';
  1489. var item = parseOptionsItem();
  1490. var optFrame =
  1491. item && item.videoFirstFrame != null
  1492. ? normalizeMediaUrl(String(item.videoFirstFrame).trim())
  1493. : '';
  1494. if (optFrame) {
  1495. poster = optFrame;
  1496. } else if (full.length > playUrl.length && urlLooksLikeStillImage(full)) {
  1497. poster = full;
  1498. } else if (jpg) {
  1499. poster = jpg;
  1500. } else if (snap) {
  1501. poster = snap;
  1502. }
  1503. var stillList = [];
  1504. if (optFrame) pushUniqueStillUrl(stillList, optFrame);
  1505. if (item && item.coverImage != null && String(item.coverImage).trim() !== '') {
  1506. pushUniqueStillUrl(stillList, item.coverImage);
  1507. }
  1508. if (item && item.cover != null && String(item.cover).trim() !== '') {
  1509. pushUniqueStillUrl(stillList, item.cover);
  1510. }
  1511. if (full.length > playUrl.length && urlLooksLikeStillImage(full)) {
  1512. pushUniqueStillUrl(stillList, full);
  1513. }
  1514. if (jpg) pushUniqueStillUrl(stillList, jpg);
  1515. if (snap) pushUniqueStillUrl(stillList, snap);
  1516. if (snapAr && snapAr !== snap) pushUniqueStillUrl(stillList, snapAr);
  1517. if (isWeChatInAppBrowser()) {
  1518. var snap400 = mp4UrlToOssSnapshotUrlAtMs(playUrl, 400);
  1519. var snap1200 = mp4UrlToOssSnapshotUrlAtMs(playUrl, 1200);
  1520. if (snap400 && snap400 !== snap) pushUniqueStillUrl(stillList, snap400);
  1521. if (snap1200 && snap1200 !== snap && snap1200 !== snap400) {
  1522. pushUniqueStillUrl(stillList, snap1200);
  1523. }
  1524. }
  1525. var fragile = preferStaticStillOverVideo();
  1526. if (fragile && stillList.length) {
  1527. var wrapImg = document.createElement('div');
  1528. wrapImg.className = 'dyn-hero__media-wrap';
  1529. var heroIm = document.createElement('img');
  1530. heroIm.className = 'dyn-hero__poster-swap';
  1531. heroIm.alt = '';
  1532. heroIm.style.width = '100%';
  1533. heroIm.style.height = '100%';
  1534. heroIm.style.objectFit = 'cover';
  1535. applyMediaNoReferrer(heroIm);
  1536. var idx = 0;
  1537. heroIm.onerror = function () {
  1538. idx += 1;
  1539. if (idx < stillList.length) {
  1540. heroIm.src = stillList[idx];
  1541. } else {
  1542. if (heroIm.parentNode) heroIm.parentNode.removeChild(heroIm);
  1543. appendMp4VideoFirstFrameBranch(wrapImg, playUrl, poster, snap, true);
  1544. }
  1545. };
  1546. heroIm.src = stillList[0];
  1547. wrapImg.appendChild(heroIm);
  1548. slide.appendChild(wrapImg);
  1549. return;
  1550. }
  1551. var wrap = document.createElement('div');
  1552. wrap.className = 'dyn-hero__media-wrap';
  1553. appendMp4VideoFirstFrameBranch(wrap, playUrl, poster, snap, fragile);
  1554. slide.appendChild(wrap);
  1555. }
  1556. function appendMp4VideoFirstFrameBranch(wrap, playUrl, poster, snap, fragilePosterEnv) {
  1557. var posterIm = null;
  1558. if (poster) {
  1559. posterIm = document.createElement('img');
  1560. posterIm.className = 'dyn-hero__poster-swap';
  1561. posterIm.alt = '';
  1562. applyMediaNoReferrer(posterIm);
  1563. posterIm.src = poster;
  1564. posterIm.decoding = 'async';
  1565. posterIm.onerror = function () {
  1566. this.style.display = 'none';
  1567. };
  1568. wrap.appendChild(posterIm);
  1569. }
  1570. var vid = document.createElement('video');
  1571. vid.className = 'dyn-hero__video--poster-only';
  1572. applyMediaNoReferrer(vid);
  1573. var playSrc = playUrl;
  1574. if (fragilePosterEnv && playUrl.indexOf('#') < 0) {
  1575. try {
  1576. playSrc = playUrl + '#t=0.12';
  1577. } catch (e0) {
  1578. playSrc = playUrl;
  1579. }
  1580. }
  1581. vid.src = playSrc;
  1582. if (poster) vid.setAttribute('poster', poster);
  1583. vid.setAttribute('playsinline', '');
  1584. vid.setAttribute('webkit-playsinline', '');
  1585. vid.setAttribute('disableremoteplayback', '');
  1586. vid.setAttribute('preload', 'auto');
  1587. vid.muted = true;
  1588. vid.setAttribute('muted', '');
  1589. vid.playsInline = true;
  1590. vid.loop = false;
  1591. vid.defaultMuted = true;
  1592. function hidePosterSwap() {
  1593. if (fragilePosterEnv) return;
  1594. if (!posterIm) return;
  1595. posterIm.style.opacity = '0';
  1596. window.setTimeout(function () {
  1597. if (posterIm && posterIm.parentNode) {
  1598. posterIm.style.visibility = 'hidden';
  1599. }
  1600. }, 220);
  1601. }
  1602. function iosKickDecodePaint() {
  1603. if (!fragilePosterEnv) return;
  1604. var pr = vid.play();
  1605. if (pr && typeof pr.then === 'function') {
  1606. pr.then(function () {
  1607. vid.pause();
  1608. }).catch(function () {});
  1609. }
  1610. }
  1611. function seekToFirstFrame() {
  1612. try {
  1613. if (vid.readyState < 2) return;
  1614. var dur = vid.duration;
  1615. var floorT = fragilePosterEnv ? 0.12 : 0.04;
  1616. if (typeof dur === 'number' && isFinite(dur) && dur > 0) {
  1617. var t = Math.max(floorT, Math.min(dur * 0.06, dur - 0.05));
  1618. vid.currentTime = t;
  1619. } else {
  1620. vid.currentTime = floorT;
  1621. }
  1622. } catch (e) {}
  1623. }
  1624. vid.addEventListener('loadedmetadata', function () {
  1625. seekToFirstFrame();
  1626. });
  1627. vid.addEventListener('loadeddata', function () {
  1628. seekToFirstFrame();
  1629. });
  1630. vid.addEventListener('seeked', function () {
  1631. vid.pause();
  1632. iosKickDecodePaint();
  1633. if (!fragilePosterEnv && vid.videoWidth > 0) {
  1634. hidePosterSwap();
  1635. }
  1636. });
  1637. vid.addEventListener('canplay', function once() {
  1638. vid.removeEventListener('canplay', once);
  1639. seekToFirstFrame();
  1640. });
  1641. vid.addEventListener('timeupdate', function onTU() {
  1642. if (!fragilePosterEnv && vid.currentTime >= 0.08 && vid.videoWidth > 0) {
  1643. vid.removeEventListener('timeupdate', onTU);
  1644. hidePosterSwap();
  1645. }
  1646. });
  1647. vid.addEventListener('error', function () {
  1648. if (vid.getAttribute('data-fallback') === '1') return;
  1649. vid.setAttribute('data-fallback', '1');
  1650. if (snap && String(vid.src || '').indexOf('x-oss-process') < 0) {
  1651. vid.src = snap;
  1652. try {
  1653. vid.load();
  1654. } catch (e2) {}
  1655. }
  1656. });
  1657. wrap.appendChild(vid);
  1658. }
  1659. /** 顶层 userImage;若无则 options.item.userImage(须为字符串地址) */
  1660. function resolveUserAvatarUrl() {
  1661. var top = normalizeMediaUrl(getMergedParam('userImage'));
  1662. if (top) return top;
  1663. var item = parseOptionsItem();
  1664. if (!item) return '';
  1665. var u = item.userImage;
  1666. if (u == null || u === '') return '';
  1667. if (typeof u === 'number') return '';
  1668. return normalizeMediaUrl(String(u));
  1669. }
  1670. /**
  1671. * App 分享链接里 imagePath 常在 options JSON 的 item 字符串内:
  1672. * options={ "item": "{\"imagePath\":\"https://...\",\"imageList\":[...]}" }
  1673. */
  1674. function collectImagesFromOptionsParam() {
  1675. var item = parseOptionsItem();
  1676. if (!item) return [];
  1677. var out = [];
  1678. if (Array.isArray(item.imageList)) {
  1679. item.imageList.forEach(function (u) {
  1680. pushUniqueUrl(out, u);
  1681. });
  1682. }
  1683. if (item.imagePath) {
  1684. String(item.imagePath).split(',').forEach(function (seg) {
  1685. pushUniqueUrl(out, seg);
  1686. });
  1687. }
  1688. pushUniqueUrl(out, item.cover);
  1689. pushUniqueUrl(out, item.src);
  1690. return out;
  1691. }
  1692. /**
  1693. * 从地址栏收集轮播图:① 顶层 imagePath;② options.item 内 imagePath / imageList;
  1694. * ③ 顶层 coverUrl。
  1695. */
  1696. function collectImageUrlsFromUrl() {
  1697. var merged = getMergedQueryString();
  1698. var vals = getAllParamValuesCI(merged, 'imagepath');
  1699. if (!vals.length) {
  1700. vals = getAllParamValuesCI(merged, 'imagepaths');
  1701. }
  1702. if (!vals.length) {
  1703. vals = getAllParamValuesCI(merged, 'images');
  1704. }
  1705. if (!vals.length) {
  1706. vals = getAllParamValuesCI(merged, 'image_path');
  1707. }
  1708. var urls = [];
  1709. vals.forEach(function (val) {
  1710. val = String(val == null ? '' : val).trim();
  1711. if (!val) return;
  1712. if (val.charAt(0) === '[') {
  1713. try {
  1714. var arr = JSON.parse(val);
  1715. if (Array.isArray(arr)) {
  1716. arr.forEach(function (u) {
  1717. pushUniqueUrl(urls, u);
  1718. });
  1719. return;
  1720. }
  1721. } catch (e1) {
  1722. try {
  1723. var arr2 = JSON.parse(decodeURIComponent(val));
  1724. if (Array.isArray(arr2)) {
  1725. arr2.forEach(function (u) {
  1726. pushUniqueUrl(urls, u);
  1727. });
  1728. return;
  1729. }
  1730. } catch (e2) {}
  1731. }
  1732. }
  1733. val.split(',').forEach(function (seg) {
  1734. seg = String(seg).trim();
  1735. if (!seg) return;
  1736. if (/%[0-9A-Fa-f]{2}/.test(seg)) {
  1737. try {
  1738. seg = decodeURIComponent(seg.replace(/\+/g, ' '));
  1739. } catch (e) {}
  1740. }
  1741. pushUniqueUrl(urls, seg);
  1742. });
  1743. });
  1744. if (!urls.length) {
  1745. collectImagesFromOptionsParam().forEach(function (u) {
  1746. pushUniqueUrl(urls, u);
  1747. });
  1748. }
  1749. if (!urls.length) {
  1750. var cover = getMergedParam('coverUrl');
  1751. if (cover) {
  1752. try {
  1753. cover = decodeURIComponent(cover);
  1754. } catch (e) {}
  1755. pushUniqueUrl(urls, cover);
  1756. }
  1757. }
  1758. return urls;
  1759. }
  1760. /** 与 App utils/shareVideoPrecache.js:canonical + hash 一致 */
  1761. function canonicalVideoUrlForPrecacheKey(url) {
  1762. var s = String(url == null ? '' : url).trim();
  1763. if (!s) return '';
  1764. try {
  1765. var u = new URL(s);
  1766. return (u.origin + u.pathname).toLowerCase();
  1767. } catch (eCan) {
  1768. return String(s.split('?')[0] || '')
  1769. .trim()
  1770. .toLowerCase();
  1771. }
  1772. }
  1773. function hashShareVideoRemoteUrl(url) {
  1774. var c = canonicalVideoUrlForPrecacheKey(url);
  1775. if (!c) return '';
  1776. var h = 5381;
  1777. for (var i = 0; i < c.length; i++) {
  1778. h = ((h << 5) + h + c.charCodeAt(i)) >>> 0;
  1779. }
  1780. var h2 = 52711;
  1781. for (var j = c.length - 1; j >= 0; j--) {
  1782. h2 = (h2 * 33 + c.charCodeAt(j)) >>> 0;
  1783. }
  1784. return (h >>> 0).toString(16) + '_' + (h2 >>> 0).toString(16) + '_' + c.length;
  1785. }
  1786. /**
  1787. * 唤起 App 前解析首条可播 https .mp4(与详情 DomVideoPlayer 主片同源),供 shareVideoCk 与预取。
  1788. */
  1789. function pickFirstHttpsMp4ForShareVideoPrecache() {
  1790. var seen = [];
  1791. function trySeg(u) {
  1792. u = normalizeMediaUrl(String(u || '').trim());
  1793. if (!u || seen.indexOf(u) >= 0) return '';
  1794. seen.push(u);
  1795. var play = getMp4PlaybackUrl(u);
  1796. if (!play || !/^https:\/\//i.test(play)) return '';
  1797. if (!isMp4VideoUrl(play)) return '';
  1798. return play;
  1799. }
  1800. var itemObj = parseOptionsItem();
  1801. if (itemObj) {
  1802. if (Array.isArray(itemObj.imageList)) {
  1803. for (var ii = 0; ii < itemObj.imageList.length; ii++) {
  1804. var t0 = trySeg(itemObj.imageList[ii]);
  1805. if (t0) return t0;
  1806. }
  1807. }
  1808. if (itemObj.imagePath) {
  1809. var ips = String(itemObj.imagePath).split(/,(?=https?:\/\/)/);
  1810. if (ips.length <= 1) ips = String(itemObj.imagePath).split(',');
  1811. for (var jj = 0; jj < ips.length; jj++) {
  1812. var t1 = trySeg(ips[jj]);
  1813. if (t1) return t1;
  1814. }
  1815. }
  1816. }
  1817. var itBlob = parseShareDynamicItemBlob();
  1818. if (itBlob && itBlob !== itemObj) {
  1819. if (Array.isArray(itBlob.imageList)) {
  1820. for (var aa = 0; aa < itBlob.imageList.length; aa++) {
  1821. var t2 = trySeg(itBlob.imageList[aa]);
  1822. if (t2) return t2;
  1823. }
  1824. }
  1825. if (itBlob.imagePath) {
  1826. var ips2 = String(itBlob.imagePath).split(/,(?=https?:\/\/)/);
  1827. if (ips2.length <= 1) ips2 = String(itBlob.imagePath).split(',');
  1828. for (var bb = 0; bb < ips2.length; bb++) {
  1829. var t3 = trySeg(ips2[bb]);
  1830. if (t3) return t3;
  1831. }
  1832. }
  1833. }
  1834. var carousel = collectImageUrlsFromUrl();
  1835. for (var kk = 0; kk < carousel.length; kk++) {
  1836. var t4 = trySeg(carousel[kk]);
  1837. if (t4) return t4;
  1838. }
  1839. var topIp = getMergedParam('imagePath');
  1840. if (topIp) {
  1841. var segs2 = normalizeMediaUrl(topIp).split(/,(?=https?:\/\/)/);
  1842. if (segs2.length <= 1) segs2 = String(topIp).split(',');
  1843. for (var mm = 0; mm < segs2.length; mm++) {
  1844. var t5 = trySeg(segs2[mm]);
  1845. if (t5) return t5;
  1846. }
  1847. }
  1848. return '';
  1849. }
  1850. /**
  1851. * 唤醒 App 前尽力拉取视频(Cache API / prefetch / fetch);与 App 沙箱缓存独立。
  1852. * 返回 Promise,便于「先缓存再跳转」;失败仍 resolve,避免阻断打开 App。
  1853. */
  1854. function prefetchShareVideoBeforeAppOpen() {
  1855. try {
  1856. var u = pickFirstHttpsMp4ForShareVideoPrecache();
  1857. if (!u) return Promise.resolve();
  1858. var tasks = [];
  1859. if ('caches' in window && window.caches && window.caches.open) {
  1860. tasks.push(
  1861. window.caches
  1862. .open('shopro-share-dynamic-video-v1')
  1863. .then(function (cache) {
  1864. return cache.add(u);
  1865. })
  1866. .catch(function () {})
  1867. );
  1868. }
  1869. try {
  1870. var lnk = document.createElement('link');
  1871. lnk.rel = 'prefetch';
  1872. lnk.as = 'video';
  1873. lnk.href = u;
  1874. document.head.appendChild(lnk);
  1875. } catch (eL) {}
  1876. if (typeof fetch === 'function') {
  1877. tasks.push(
  1878. fetch(u, { method: 'GET', mode: 'cors', cache: 'force-cache' }).catch(function () {})
  1879. );
  1880. }
  1881. return tasks.length ? Promise.all(tasks) : Promise.resolve();
  1882. } catch (eP) {
  1883. return Promise.resolve();
  1884. }
  1885. }
  1886. var heroI = 0;
  1887. var heroTimer = null;
  1888. var dynHeroSwipeInited = false;
  1889. function getHeroSlideCount() {
  1890. return document.getElementById('dynHeroDots').querySelectorAll('.dyn-hero__dot').length;
  1891. }
  1892. function initDynCarousel(slides) {
  1893. var track = document.getElementById('dynHeroTrack');
  1894. var dotsWrap = document.getElementById('dynHeroDots');
  1895. function go(n) {
  1896. var count = getHeroSlideCount();
  1897. if (count < 1) return;
  1898. heroI = ((n % count) + count) % count;
  1899. track.style.transform = 'translateX(' + (-heroI * 100) + '%)';
  1900. dotsWrap.querySelectorAll('.dyn-hero__dot').forEach(function (el, idx) {
  1901. el.classList.toggle('is-active', idx === heroI);
  1902. });
  1903. }
  1904. if (heroTimer) {
  1905. clearInterval(heroTimer);
  1906. heroTimer = null;
  1907. }
  1908. if (slides < 2) {
  1909. heroI = 0;
  1910. track.style.transform = 'translateX(0)';
  1911. return;
  1912. }
  1913. heroTimer = setInterval(function () { go(heroI + 1); }, 4000);
  1914. if (!dynHeroSwipeInited) {
  1915. dynHeroSwipeInited = true;
  1916. var hero = document.getElementById('dynHero');
  1917. var startX = 0;
  1918. hero.addEventListener('touchstart', function (e) {
  1919. startX = e.changedTouches[0].clientX;
  1920. }, { passive: true });
  1921. hero.addEventListener('touchend', function (e) {
  1922. var dx = e.changedTouches[0].clientX - startX;
  1923. if (Math.abs(dx) > 40) go(dx < 0 ? heroI + 1 : heroI - 1);
  1924. }, { passive: true });
  1925. }
  1926. }
  1927. function buildDynSlides(urls) {
  1928. heroI = 0;
  1929. var track = document.getElementById('dynHeroTrack');
  1930. var dotsWrap = document.getElementById('dynHeroDots');
  1931. var heroEl = document.getElementById('dynHero');
  1932. track.innerHTML = '';
  1933. dotsWrap.innerHTML = '';
  1934. track.style.transform = 'translateX(0)';
  1935. if (heroEl) heroEl.classList.remove('dyn-hero--no-dots');
  1936. var list = (urls || []).map(function (u) {
  1937. return normalizeMediaUrl(u);
  1938. }).filter(Boolean);
  1939. if (!list.length) {
  1940. var slide = document.createElement('div');
  1941. slide.className = 'dyn-hero__slide';
  1942. var img = document.createElement('img');
  1943. img.src = 'images/hero.png';
  1944. img.alt = '';
  1945. slide.appendChild(img);
  1946. track.appendChild(slide);
  1947. var dot = document.createElement('span');
  1948. dot.className = 'dyn-hero__dot is-active';
  1949. dotsWrap.appendChild(dot);
  1950. return initDynCarousel(1);
  1951. }
  1952. /** 任意一条为 .mp4:只展示第一个的首帧静图,不轮播、无指示点(与纯图多条轮播区分) */
  1953. var firstMp4 = '';
  1954. for (var mi = 0; mi < list.length; mi++) {
  1955. if (isMp4VideoUrl(list[mi])) {
  1956. firstMp4 = list[mi];
  1957. break;
  1958. }
  1959. }
  1960. if (firstMp4) {
  1961. if (heroEl) heroEl.classList.add('dyn-hero--no-dots');
  1962. var vSlide = document.createElement('div');
  1963. vSlide.className = 'dyn-hero__slide';
  1964. appendMp4FirstFrameToSlide(vSlide, firstMp4);
  1965. track.appendChild(vSlide);
  1966. return initDynCarousel(1);
  1967. }
  1968. list.forEach(function (url, d) {
  1969. var s = document.createElement('div');
  1970. s.className = 'dyn-hero__slide';
  1971. if (isMp4VideoUrl(url)) {
  1972. appendMp4FirstFrameToSlide(s, url);
  1973. } else {
  1974. var im = document.createElement('img');
  1975. if (/^https?:/i.test(url)) {
  1976. applyMediaNoReferrer(im);
  1977. }
  1978. im.src = url;
  1979. im.alt = '';
  1980. s.appendChild(im);
  1981. }
  1982. track.appendChild(s);
  1983. var dot = document.createElement('span');
  1984. dot.className = 'dyn-hero__dot' + (d === 0 ? ' is-active' : '');
  1985. dotsWrap.appendChild(dot);
  1986. });
  1987. initDynCarousel(list.length);
  1988. }
  1989. function resolveCommentRequestParams() {
  1990. var item = parseOptionsItem();
  1991. var sourceType = getMergedParam('sourceType') || getMergedParam('businessType') || q('sourceType') || q('businessType');
  1992. if (sourceType === '' && item && item.type != null) sourceType = String(item.type);
  1993. var sourceId = getMergedParam('sourceId') || getMergedParam('dynamicId') || q('dynamicId') || q('id');
  1994. if (!sourceId && item && item.id != null) sourceId = String(item.id);
  1995. var userId = getMergedParam('userId') || q('userId');
  1996. if (!sourceType || !sourceId || !userId) return null;
  1997. return {
  1998. sourceType: String(sourceType),
  1999. sourceId: String(sourceId),
  2000. userId: String(userId),
  2001. pageNum: COMMENT_PAGE_NUM,
  2002. pageSize: COMMENT_PAGE_SIZE
  2003. };
  2004. }
  2005. function normalizeCommentPayload(data) {
  2006. if (!data) return { list: [], total: 0 };
  2007. if (Array.isArray(data)) return { list: data, total: data.length };
  2008. var list = data.list || data.records || data.rows;
  2009. if (!Array.isArray(list) && Array.isArray(data.data)) list = data.data;
  2010. if (!Array.isArray(list)) list = [];
  2011. var total = data.total != null ? Number(data.total) : (data.count != null ? Number(data.count) : list.length);
  2012. if (isNaN(total)) total = list.length;
  2013. return { list: list, total: total };
  2014. }
  2015. function formatCommentCountParens(n) {
  2016. return '(' + String(n) + ')';
  2017. }
  2018. function renderComments(resData) {
  2019. var norm = normalizeCommentPayload(resData);
  2020. var list = norm.list;
  2021. var total = norm.total;
  2022. document.getElementById('commentCount').textContent = formatCommentCountParens(total);
  2023. document.getElementById('storeReviews').textContent = String(total);
  2024. var wrap = document.getElementById('commentList');
  2025. var empty = document.getElementById('commentEmpty');
  2026. wrap.innerHTML = '';
  2027. wrap.classList.remove('is-visible');
  2028. if (!list.length) {
  2029. empty.style.display = 'flex';
  2030. return;
  2031. }
  2032. empty.style.display = 'none';
  2033. wrap.classList.add('is-visible');
  2034. list.forEach(function (row) {
  2035. if (!row || typeof row !== 'object') return;
  2036. var text = row.content != null ? String(row.content) : (row.commentContent != null ? String(row.commentContent) : (row.context != null ? String(row.context) : (row.text != null ? String(row.text) : '')));
  2037. var uname = row.userName != null ? String(row.userName) : (row.nickName != null ? String(row.nickName) : (row.nickname != null ? String(row.nickname) : (row.headName != null ? String(row.headName) : '用户')));
  2038. var time = row.createdTime != null ? String(row.createdTime) : (row.createTime != null ? String(row.createTime) : (row.time != null ? String(row.time) : ''));
  2039. var headImg = row.headImg != null ? String(row.headImg).trim() : '';
  2040. if (!headImg || headImg === '0') headImg = COMMENT_AVATAR_FALLBACK;
  2041. var rowEl = document.createElement('div');
  2042. rowEl.className = 'comment-item';
  2043. var avatar = document.createElement('img');
  2044. avatar.className = 'comment-item__avatar';
  2045. avatar.alt = '';
  2046. avatar.src = headImg;
  2047. avatar.onerror = function () {
  2048. this.onerror = null;
  2049. this.src = COMMENT_AVATAR_FALLBACK;
  2050. };
  2051. var col = document.createElement('div');
  2052. col.className = 'comment-item__col';
  2053. var meta = document.createElement('div');
  2054. meta.className = 'comment-item__meta';
  2055. var nameEl = document.createElement('span');
  2056. nameEl.className = 'comment-item__name';
  2057. nameEl.textContent = uname;
  2058. meta.appendChild(nameEl);
  2059. var body = document.createElement('div');
  2060. body.className = 'comment-item__text';
  2061. body.textContent = text;
  2062. col.appendChild(meta);
  2063. col.appendChild(body);
  2064. if (time) {
  2065. var timeEl = document.createElement('div');
  2066. timeEl.className = 'comment-item__time';
  2067. timeEl.textContent = time;
  2068. col.appendChild(timeEl);
  2069. }
  2070. rowEl.appendChild(avatar);
  2071. rowEl.appendChild(col);
  2072. wrap.appendChild(rowEl);
  2073. });
  2074. }
  2075. function loadComments() {
  2076. var p = resolveCommentRequestParams();
  2077. if (!p) return;
  2078. var path = '/commonComment/getListBySourceType?' +
  2079. 'sourceType=' + encodeURIComponent(p.sourceType) +
  2080. '&sourceId=' + encodeURIComponent(p.sourceId) +
  2081. '&userId=' + encodeURIComponent(p.userId) +
  2082. '&pageNum=' + encodeURIComponent(String(p.pageNum)) +
  2083. '&pageSize=' + encodeURIComponent(String(p.pageSize));
  2084. apiFetch(path)
  2085. .then(function (res) {
  2086. if (res.code !== 200) {
  2087. renderComments(null);
  2088. return;
  2089. }
  2090. renderComments(res.data);
  2091. })
  2092. .catch(function (e) {
  2093. console.error(e);
  2094. renderComments(null);
  2095. });
  2096. }
  2097. function applyQueryContent() {
  2098. /** 优先展示分享者昵称(App 传 shareUserName);否则动态作者 userName、店铺名 */
  2099. var name =
  2100. getMergedParam('shareUserName') ||
  2101. q('shareUserName') ||
  2102. q('userName') ||
  2103. q('storeName');
  2104. if (name) document.getElementById('userName').textContent = decodeURIComponent(name);
  2105. var avatarUrl = resolveUserAvatarUrl();
  2106. if (avatarUrl) {
  2107. document.getElementById('userAvatar').src = avatarUrl;
  2108. }
  2109. var desc = q('content') || q('desc') || q('text');
  2110. if (desc) {
  2111. document.getElementById('userDesc').textContent = decodeURIComponent(desc);
  2112. }
  2113. var storeNameParam = getMergedParam('storeName') || q('storeName');
  2114. if (storeNameParam) {
  2115. try {
  2116. document.getElementById('storeTitle').textContent = decodeURIComponent(storeNameParam);
  2117. } catch (e) {
  2118. document.getElementById('storeTitle').textContent = storeNameParam;
  2119. }
  2120. }
  2121. var scoreAvgParam = getMergedParam('scoreAvg') || q('scoreAvg');
  2122. if (scoreAvgParam !== '') {
  2123. var scNum = parseFloat(scoreAvgParam, 10);
  2124. document.getElementById('storeScore').textContent =
  2125. !isNaN(scNum) ? scNum.toFixed(1) : String(scoreAvgParam);
  2126. }
  2127. buildDynSlides(collectImageUrlsFromUrl());
  2128. if (!resolveCommentRequestParams()) {
  2129. var cc = getMergedParam('commentCount') || q('commentCount');
  2130. if (cc !== '') {
  2131. var n = parseInt(cc, 10);
  2132. if (!isNaN(n)) {
  2133. document.getElementById('commentCount').textContent = formatCommentCountParens(n);
  2134. document.getElementById('storeReviews').textContent = String(n);
  2135. document.getElementById('commentEmpty').style.display = n > 0 ? 'none' : 'flex';
  2136. }
  2137. }
  2138. }
  2139. var tag = q('tagline');
  2140. if (tag) document.getElementById('storeTagline').textContent = decodeURIComponent(tag);
  2141. var cat = q('category') || q('storeCat');
  2142. if (cat) document.getElementById('storeCat').textContent = decodeURIComponent(cat);
  2143. }
  2144. function boot() {
  2145. fetchGetDeleteFlagByIdIfId().then(function (res) {
  2146. if (isGetOneBusinessStatus99(res)) {
  2147. applyShareDynamicClosedMerchantUi(true);
  2148. return;
  2149. }
  2150. // if (shouldRedirectToShareCheckInUndefined(res)) {
  2151. // window.location.replace(buildShareCheckInUndefinedHref(res));
  2152. // return;
  2153. // }
  2154. if (isNoCarryingDataDeleteFlagMsg(res)) {
  2155. showDynNoCarryingDataState();
  2156. return;
  2157. }
  2158. applyQueryContent();
  2159. loadComments();
  2160. });
  2161. var openBtn = document.getElementById('openApp');
  2162. if (openBtn) {
  2163. openBtn.addEventListener('click', function () {
  2164. tryOpenHBuilderApp();
  2165. });
  2166. }
  2167. }
  2168. if (document.readyState === 'complete') {
  2169. boot();
  2170. } else {
  2171. window.onload = boot;
  2172. }
  2173. })();
  2174. </script>
  2175. </body>
  2176. </html>