shareDynamic.html 45 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638
  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. /* 顶部轮播 */
  38. .dyn-hero {
  39. position: relative;
  40. margin: 12px 15px 0;
  41. border-radius: 12px 12px 0 0;
  42. overflow: hidden;
  43. background: #e8e8e8;
  44. }
  45. .dyn-hero__track {
  46. display: flex;
  47. transition: transform 0.35s ease;
  48. }
  49. .dyn-hero__slide {
  50. flex: 0 0 100%;
  51. aspect-ratio: 16 / 10;
  52. background: #e0e0e0;
  53. }
  54. .dyn-hero__slide img,
  55. .dyn-hero__slide video {
  56. width: 100%;
  57. height: 100%;
  58. object-fit: cover;
  59. display: block;
  60. }
  61. /* 仅展示首帧:禁止点击触发播放;叠层 poster 时视频背景透明,避免 iOS 黑底盖住图 */
  62. .dyn-hero__slide .dyn-hero__media-wrap {
  63. position: relative;
  64. width: 100%;
  65. height: 100%;
  66. overflow: hidden;
  67. background: #e0e0e0;
  68. }
  69. /* 封面叠在 video 之上:微信/iOS 里 video 常呈灰底,否则会盖住已加载的 poster/img */
  70. .dyn-hero__slide .dyn-hero__poster-swap {
  71. position: absolute;
  72. left: 0;
  73. top: 0;
  74. width: 100%;
  75. height: 100%;
  76. object-fit: cover;
  77. z-index: 2;
  78. transition: opacity 0.2s ease;
  79. }
  80. .dyn-hero__slide video.dyn-hero__video--poster-only {
  81. position: relative;
  82. z-index: 1;
  83. pointer-events: none;
  84. background: transparent;
  85. }
  86. .dyn-hero--no-dots .dyn-hero__dots {
  87. display: none;
  88. }
  89. .dyn-hero__dots {
  90. position: absolute;
  91. bottom: 12px;
  92. left: 0;
  93. right: 0;
  94. display: flex;
  95. justify-content: center;
  96. align-items: center;
  97. gap: 6px;display: none;
  98. }
  99. .dyn-hero__dot {
  100. width: 6px;
  101. height: 6px;
  102. border-radius: 3px;
  103. background: rgba(255, 255, 255, 0.45);
  104. transition: width 0.25s ease, background 0.25s ease;
  105. }
  106. .dyn-hero__dot.is-active {
  107. width: 16px;
  108. background: #fff;
  109. }
  110. /* 用户信息 */
  111. .dyn-user {
  112. padding: 16px 15px 12px;
  113. background:#fff;
  114. }
  115. .dyn-user__row {
  116. display: flex;
  117. align-items: flex-start;
  118. gap: 12px;
  119. }
  120. .dyn-user__avatar {
  121. width: 48px;
  122. height: 48px;
  123. border-radius: 50%;
  124. object-fit: cover;
  125. background: #ddd;
  126. flex-shrink: 0;
  127. }
  128. .dyn-user__name {
  129. font-size: 17px;
  130. font-weight: 700;
  131. color: var(--text);
  132. line-height: 1.3;
  133. padding-top: 2px;
  134. }
  135. .dyn-user__desc {
  136. margin-top: 10px;
  137. font-size: 15px;
  138. line-height: 1.6;
  139. color: var(--text-muted);
  140. }
  141. /* 卡片通用 */
  142. .dyn-card {
  143. margin: 0 15px 12px;
  144. padding: 14px;
  145. background: #fff;
  146. border-radius: 12px;
  147. box-shadow: var(--card-shadow);
  148. }
  149. /* 店铺卡片 */
  150. .store-row {
  151. display: flex;
  152. gap: 12px;
  153. align-items: flex-start;
  154. }
  155. .store-row__thumb {
  156. width: 80px;
  157. height: 80px;
  158. border-radius: 8px;
  159. object-fit: cover;
  160. background: #eee;
  161. flex-shrink: 0;
  162. }
  163. .store-row__main {
  164. flex: 1;
  165. min-width: 0;
  166. }
  167. .store-row__title {
  168. font-size: 16px;
  169. font-weight: 700;
  170. color: var(--text);
  171. margin-bottom: 6px;
  172. }
  173. .store-row__rating {
  174. display: flex;
  175. align-items: center;
  176. flex-wrap: wrap;
  177. gap: 4px 8px;
  178. margin-bottom: 6px;
  179. }
  180. .store-row__rating .stars {
  181. display: inline-flex;
  182. align-items: center;
  183. gap: 2px;
  184. color: var(--orange);
  185. }
  186. .store-row__rating .star {
  187. width: 12px;
  188. height: 12px;
  189. color: var(--orange);
  190. }
  191. .store-row__rating .rating-num {
  192. font-size: 13px;
  193. font-weight: 600;
  194. color: var(--orange);
  195. }
  196. .store-row__meta {
  197. font-size: 12px;
  198. color: var(--text-secondary);
  199. }
  200. .store-row__cat {
  201. font-size: 12px;
  202. color: var(--text-secondary);
  203. margin-bottom: 4px;
  204. }
  205. .store-row__tagline {
  206. font-size: 13px;
  207. color: var(--text);
  208. line-height: 1.45;
  209. }
  210. /* 评价 */
  211. .comment-card__title {
  212. font-size: 15px;
  213. font-weight: 700;
  214. color: var(--text);
  215. margin-bottom: 20px;
  216. }
  217. .comment-empty {
  218. display: flex;
  219. flex-direction: column;
  220. align-items: center;
  221. justify-content: center;
  222. padding: 28px 16px 20px;
  223. color: #aaa;
  224. font-size: 14px;
  225. }
  226. .comment-empty__icon {
  227. width: 56px;
  228. height: 56px;
  229. margin-bottom: 12px;
  230. object-fit: contain;
  231. display: block;
  232. }
  233. .comment-list {
  234. display: none;
  235. }
  236. .comment-list.is-visible {
  237. display: block;
  238. }
  239. .comment-item {
  240. display: flex;
  241. align-items: flex-start;
  242. gap: 10px;
  243. padding: 12px 0;
  244. border-top: 1px solid var(--border);
  245. }
  246. .comment-item:first-child {
  247. border-top: none;
  248. padding-top: 0;
  249. }
  250. .comment-item__avatar {
  251. width: 40px;
  252. height: 40px;
  253. border-radius: 50%;
  254. object-fit: cover;
  255. flex-shrink: 0;
  256. background: #eee;
  257. }
  258. .comment-item__col {
  259. flex: 1;
  260. min-width: 0;
  261. }
  262. .comment-item__meta {
  263. font-size: 13px;
  264. color: var(--text-secondary);
  265. margin-bottom: 6px;
  266. }
  267. .comment-item__name {
  268. font-weight: 600;
  269. color: var(--text);
  270. }
  271. .comment-item__text {
  272. font-size: 14px;
  273. line-height: 1.55;
  274. color: var(--text);
  275. word-break: break-word;
  276. margin-bottom: 4px;
  277. }
  278. .comment-item__time {
  279. display: block;
  280. font-size: 12px;
  281. font-weight: 400;
  282. color: var(--text-secondary);
  283. margin-top: 2px;
  284. }
  285. /* 底部按钮 */
  286. .fab-wrap {
  287. position: fixed;
  288. left: 0;
  289. right: 0;
  290. bottom: 0;
  291. z-index: 200;
  292. padding: 12px 24px calc(12px + var(--safe-bottom));
  293. background: linear-gradient(to top, rgba(255, 255, 255, 0.98) 70%, transparent);
  294. pointer-events: none;
  295. }
  296. .fab-wrap .fab {
  297. pointer-events: auto;
  298. }
  299. .fab {
  300. display: flex;
  301. align-items: center;
  302. justify-content: center;
  303. gap: 10px;
  304. width: 100%;
  305. max-width: 320px;
  306. margin: 0 auto;
  307. height: 48px;
  308. border: none;
  309. border-radius: 24px;
  310. background: var(--orange);
  311. color: #fff;
  312. font-size: 16px;
  313. font-weight: 600;
  314. box-shadow: 0 4px 16px rgba(245, 130, 32, 0.45);
  315. cursor: pointer;
  316. background: #F47D1F;
  317. }
  318. .fab__logo {
  319. width: 28px;
  320. height: 28px;
  321. flex-shrink: 0;
  322. }
  323. .home-indicator {
  324. height: 5px;
  325. background: #000;
  326. border-radius: 3px;
  327. width: 134px;
  328. margin: 8px auto 4px;
  329. opacity: 0.2;
  330. }
  331. </style>
  332. </head>
  333. <body class="page-dynamic">
  334. <!-- 顶部配图:含 .mp4 时只展示首帧静图(不播放视频)、不轮播;纯图片可多图轮播。 -->
  335. <div class="dyn-hero" id="dynHero">
  336. <div class="dyn-hero__track" id="dynHeroTrack"></div>
  337. <div class="dyn-hero__dots" id="dynHeroDots" aria-hidden="true"></div>
  338. </div>
  339. <section class="dyn-user">
  340. <div class="dyn-user__row">
  341. <img class="dyn-user__avatar" id="userAvatar" src="images/hero.png" alt="">
  342. <div>
  343. <div class="dyn-user__name" id="userName">用户名称</div>
  344. <p class="dyn-user__desc" id="userDesc">文案描述文案描述文案描述文案描述文案描述文案描述文案描述文案描述文案描述文案描述文案描述</p>
  345. </div>
  346. </div>
  347. </section>
  348. <div class="dyn-card store-card">
  349. <div class="store-row">
  350. <img class="store-row__thumb" id="storeThumb" src="images/dynamic.png" alt="">
  351. <div class="store-row__main">
  352. <div class="store-row__title" id="storeTitle">Sober</div>
  353. <div class="store-row__rating">
  354. <span class="stars" aria-hidden="true">
  355. <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>
  356. <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>
  357. <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>
  358. <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>
  359. <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>
  360. </span>
  361. <span class="rating-num" id="storeScore">4.9</span>
  362. <span class="store-row__meta"><span id="storeReviews">200</span>条</span>
  363. </div>
  364. </div>
  365. </div>
  366. </div>
  367. <div class="dyn-card comment-card">
  368. <div class="comment-card__title">评价 <span id="commentCount" style="color:#aaa">(0)</span></div>
  369. <div id="commentList" class="comment-list" aria-live="polite"></div>
  370. <div class="comment-empty" id="commentEmpty">
  371. <img class="comment-empty__icon" src="images/zwpl.png" alt="" width="56" height="56" decoding="async">
  372. <span>暂无评论</span>
  373. </div>
  374. </div>
  375. <div class="fab-wrap">
  376. <button type="button" class="fab" id="openApp">
  377. <img src="images/uBtn.png" alt="APP内打开" decoding="async">
  378. </button>
  379. <div class="home-indicator" aria-hidden="true"></div>
  380. </div>
  381. <script>
  382. (function () {
  383. 'use strict';
  384. var API_BASE = 'https://test.ailien.shop/alienStore';
  385. var COMMENT_PAGE_NUM = 1;
  386. var COMMENT_PAGE_SIZE = 20;
  387. var COMMENT_AVATAR_FALLBACK = 'images/demouser.png';
  388. function apiFetch(path) {
  389. return fetch(API_BASE + path, {
  390. method: 'GET',
  391. mode: 'cors',
  392. credentials: 'omit',
  393. headers: { Accept: 'application/json' }
  394. }).then(function (res) {
  395. if (!res.ok) throw new Error('HTTP ' + res.status);
  396. return res.json();
  397. });
  398. }
  399. /**
  400. * 与 shareIndex / group_user manifest 一致
  401. * 动态详情默认落地:pages/newdetails/index(onLoad 使用 item 等,与当前页合并 query 一致)
  402. * 打开其它 App 页:H5 URL 上带 appPath 或 appPage,如 ?appPath=pages%2FcheckIn%2Findex(不要前导 /)
  403. */
  404. var APP_ANDROID_PACKAGE = 'com.alien.Udianzaizhe';
  405. var APP_IOS_URL_SCHEME = 'shopro://';
  406. var APP_UNI_STORE_PATH = 'pages/newdetails/index';
  407. function showDownloadTip() {
  408. var msg = '请到应用商店下载';
  409. if (typeof uni !== 'undefined' && typeof uni.showToast === 'function') {
  410. uni.showToast({ title: msg, icon: 'none', duration: 2500 });
  411. } else {
  412. window.alert(msg);
  413. }
  414. }
  415. /**
  416. * 唤起 App 时携带当前 H5 全部查询参数:先 location.search,再 hash 中 ? 后一段;
  417. * 逐项 append,避免丢参、并保留重名键顺序(与 getMergedQueryString 合并规则一致)。
  418. */
  419. function mergeSearchAndHashParams() {
  420. var params = new URLSearchParams();
  421. function ingestAppend(querySlice) {
  422. if (!querySlice) return;
  423. var p = new URLSearchParams(querySlice);
  424. p.forEach(function (val, key) {
  425. params.append(key, val);
  426. });
  427. }
  428. function ingestSet(querySlice) {
  429. if (!querySlice) return;
  430. var p = new URLSearchParams(querySlice);
  431. p.forEach(function (val, key) {
  432. params.set(key, val);
  433. });
  434. }
  435. if (location.search && location.search.length > 1) {
  436. ingestAppend(location.search.slice(1));
  437. }
  438. var hash = location.hash || '';
  439. var qi = hash.indexOf('?');
  440. if (qi >= 0) {
  441. /** hash 内 query 覆盖 search(如 localhost:5173/#/pages/...?item= 场景) */
  442. ingestSet(hash.slice(qi + 1));
  443. }
  444. return params;
  445. }
  446. /**
  447. * hash 形如 #/pages/newdetails/index?item=...&needShowMore=... 时取出 Uni 页面路径(与 App 内 route 一致)
  448. */
  449. function extractUniPagePathFromHash() {
  450. var h = location.hash || '';
  451. if (h.length < 3 || h.charAt(0) !== '#' || h.charAt(1) !== '/') return '';
  452. var rest = h.slice(2).trim();
  453. if (!rest) return '';
  454. var qpos = rest.indexOf('?');
  455. var pathPart = (qpos >= 0 ? rest.slice(0, qpos) : rest).trim();
  456. if (!pathPart || pathPart.indexOf('pages/') !== 0) return '';
  457. return pathPart.replace(/^\/+/, '');
  458. }
  459. function buildAppOpenQueryStringMerged() {
  460. var params = mergeSearchAndHashParams();
  461. /** newdetails onLoad:仅当地址栏未带 item 时补全;同时按需补顶层 imagePath */
  462. if (!params.has('item')) {
  463. var itemObj = parseOptionsItem();
  464. var imagePath = getMergedParam('imagePath');
  465. if (!imagePath && itemObj && itemObj.imagePath != null && String(itemObj.imagePath).trim() !== '') {
  466. imagePath = String(itemObj.imagePath);
  467. }
  468. if (!imagePath) {
  469. var carouselUrls = collectImageUrlsFromUrl();
  470. if (carouselUrls && carouselUrls.length) {
  471. imagePath = carouselUrls.join(',');
  472. }
  473. } else {
  474. imagePath = normalizeMediaUrl(imagePath) || imagePath;
  475. }
  476. var dynId =
  477. getMergedParam('sourceId') ||
  478. getMergedParam('dynamicId') ||
  479. getMergedParam('id') ||
  480. q('id');
  481. if (!dynId && itemObj && itemObj.id != null && String(itemObj.id).trim() !== '') {
  482. dynId = String(itemObj.id);
  483. }
  484. var base = {};
  485. if (itemObj) {
  486. try {
  487. base = JSON.parse(JSON.stringify(itemObj));
  488. } catch (eCopy) {
  489. base = {};
  490. }
  491. }
  492. if (imagePath) {
  493. base.imagePath = imagePath;
  494. }
  495. if (dynId != null && String(dynId).trim() !== '' && base.id == null) {
  496. base.id = dynId;
  497. }
  498. if (Object.keys(base).length) {
  499. try {
  500. params.set('item', JSON.stringify(base));
  501. } catch (eItem) {}
  502. }
  503. }
  504. if (!params.has('imagePath')) {
  505. var ipTop = getMergedParam('imagePath');
  506. if (!ipTop) {
  507. var it2 = parseOptionsItem();
  508. if (it2 && it2.imagePath != null && String(it2.imagePath).trim() !== '') {
  509. ipTop = String(it2.imagePath);
  510. }
  511. }
  512. if (!ipTop) {
  513. var cu = collectImageUrlsFromUrl();
  514. if (cu && cu.length) {
  515. ipTop = cu.join(',');
  516. }
  517. }
  518. if (ipTop) {
  519. params.set('imagePath', normalizeMediaUrl(ipTop) || ipTop);
  520. }
  521. }
  522. var sid = params.get('storeId') || params.get('id') || '';
  523. if (sid && !params.has('storeId')) {
  524. params.set('storeId', sid);
  525. }
  526. /** 顶层 imagePath 若未 encode,URLSearchParams 会截断在 OSS 的 &fm= 等处;用原始串解析结果覆盖 */
  527. var ipMerged = getMergedParam('imagePath');
  528. if (ipMerged) {
  529. params.set('imagePath', normalizeMediaUrl(ipMerged) || ipMerged);
  530. }
  531. var qsOut = params.toString();
  532. return qsOut ? ('?' + qsOut) : '';
  533. }
  534. /**
  535. * 真机系统浏览器对超长 shopro:// 常截断;去掉超大 item,用 id/type 让 App 内再拉详情。
  536. */
  537. function compactShoproDeepLinkIfTooLong(fullUrl, maxLen) {
  538. maxLen = maxLen || 7200;
  539. if (!fullUrl || fullUrl.length <= maxLen) return fullUrl;
  540. var scheme = 'shopro://';
  541. if (fullUrl.indexOf(scheme) !== 0) return fullUrl;
  542. var after = fullUrl.slice(scheme.length).replace(/^\/+/, '');
  543. var qI = after.indexOf('?');
  544. var pathPart = qI >= 0 ? after.slice(0, qI) : after;
  545. var queryPart = qI >= 0 ? after.slice(qI + 1) : '';
  546. if (!queryPart) return fullUrl;
  547. var sp = new URLSearchParams(queryPart);
  548. var item = sp.get('item');
  549. if (!item || item.length < 800) return fullUrl;
  550. var idm = /"id"\s*:\s*(\d+)/.exec(item);
  551. if (idm) {
  552. if (!sp.get('dynamicId')) sp.set('dynamicId', idm[1]);
  553. if (!sp.get('sourceId')) sp.set('sourceId', idm[1]);
  554. }
  555. var tpm = /"type"\s*:\s*"(\d+)"/.exec(item) || /"type"\s*:\s*(\d+)/.exec(item);
  556. if (tpm && !sp.get('sourceType')) sp.set('sourceType', tpm[1]);
  557. sp.delete('item');
  558. var qs = sp.toString();
  559. var out = scheme + pathPart + (qs ? ('?' + qs) : '');
  560. return out.length <= maxLen ? out : fullUrl;
  561. }
  562. function buildAppDeepLink() {
  563. var explicit = (getMergedParam('appPath') || getMergedParam('appPage') || '').trim().replace(/^\//, '');
  564. var fromHash = extractUniPagePathFromHash();
  565. var defaultPath = String(APP_UNI_STORE_PATH || '/pages/newdetails/index').replace(/^\//, '');
  566. /**
  567. * 不要用「任意 pages/ 的 hash」当 App 路径:分享页常在 #/pages/shareDynamic?…,
  568. * 会误唤起成该页而不是 newdetails。仅当 hash 明确含 newdetails 时才采用 hash 路径。
  569. */
  570. var path = explicit;
  571. if (!path && fromHash && /newdetails/i.test(fromHash)) {
  572. path = fromHash.replace(/^\//, '');
  573. }
  574. if (!path) {
  575. path = defaultPath;
  576. }
  577. path = String(path || defaultPath).replace(/^\//, '');
  578. var s = buildAppOpenQueryStringMerged();
  579. var root = APP_IOS_URL_SCHEME.replace(/\/$/, '');
  580. var raw = !s ? root + '/' + path : root + '/' + path + s;
  581. return compactShoproDeepLinkIfTooLong(raw, 7200);
  582. }
  583. function isWeChatInAppBrowser() {
  584. return /MicroMessenger/i.test(navigator.userAgent || '');
  585. }
  586. /** 微信内置浏览器、iOS:OSS 截图/封面易因 Referer 被拒;video 首帧不可靠 */
  587. function preferStaticStillOverVideo() {
  588. return isIOSVideoEnv() || isWeChatInAppBrowser();
  589. }
  590. /** 减轻微信/X5 对阿里云 OSS 图片、video poster 的 Referer 拦截 */
  591. function applyMediaNoReferrer(el) {
  592. if (!el) return;
  593. try {
  594. el.setAttribute('referrerpolicy', 'no-referrer');
  595. if ('referrerPolicy' in el) {
  596. el.referrerPolicy = 'no-referrer';
  597. }
  598. } catch (e) {}
  599. }
  600. function launchAppDeepLink(deepLink) {
  601. try {
  602. var a = document.createElement('a');
  603. a.href = deepLink;
  604. a.setAttribute('target', '_self');
  605. document.body.appendChild(a);
  606. a.click();
  607. document.body.removeChild(a);
  608. } catch (e1) {}
  609. try {
  610. window.location.href = deepLink;
  611. } catch (e2) {}
  612. }
  613. function tryOpenHBuilderApp() {
  614. var deepLink = buildAppDeepLink();
  615. if (typeof plus !== 'undefined' && plus.runtime) {
  616. var installed = null;
  617. try {
  618. if (typeof plus.runtime.isApplicationExist === 'function') {
  619. installed = plus.runtime.isApplicationExist({
  620. pname: APP_ANDROID_PACKAGE,
  621. action: APP_IOS_URL_SCHEME
  622. });
  623. }
  624. } catch (e) {
  625. console.warn(e);
  626. }
  627. /**
  628. * 不在「未安装」时直接弹下载:部分 ROM 对 isApplicationExist 误判为 false,
  629. * 仍应尝试 openURL,避免一点按钮就「请到应用商店下载」。
  630. */
  631. try {
  632. plus.runtime.openURL(deepLink);
  633. } catch (e2) {
  634. console.warn(e2);
  635. if (installed === false) {
  636. showDownloadTip();
  637. }
  638. }
  639. return;
  640. }
  641. var done = false;
  642. function finish() {
  643. if (done) return;
  644. done = true;
  645. document.removeEventListener('visibilitychange', onVis);
  646. window.removeEventListener('pagehide', onHide);
  647. }
  648. function onVis() {
  649. if (document.visibilityState === 'hidden') finish();
  650. }
  651. function onHide() {
  652. finish();
  653. }
  654. document.addEventListener('visibilitychange', onVis);
  655. window.addEventListener('pagehide', onHide);
  656. if (isWeChatInAppBrowser()) {
  657. window.alert('若点击后无法打开 App:请先点右上角「···」,选择「在浏览器中打开」,再点「APP内打开」。');
  658. }
  659. try {
  660. launchAppDeepLink(deepLink);
  661. } catch (e3) {
  662. finish();
  663. showDownloadTip();
  664. return;
  665. }
  666. /**
  667. * 不在超时后弹「去应用商店」:App 已打开时页面常仍 visible,易误报;
  668. * 若未安装,用户无反应可自行去商店,避免打断操作。
  669. */
  670. window.setTimeout(function () {
  671. finish();
  672. }, 3200);
  673. }
  674. function qs() {
  675. return new URLSearchParams(location.search || '');
  676. }
  677. function q(name) {
  678. var v = qs().get(name);
  679. return v == null ? '' : String(v);
  680. }
  681. /** 与 mergeSearchAndHashParams 一致:hash 内 query 覆盖 search,供 getMergedParam 与 App 唤起共用 */
  682. function getMergedQueryString() {
  683. var m = mergeSearchAndHashParams().toString();
  684. return m || '';
  685. }
  686. function forEachQueryParam(queryStr, fn) {
  687. if (!queryStr) return;
  688. var p = new URLSearchParams(queryStr);
  689. p.forEach(function (val, key) {
  690. fn(val, key);
  691. });
  692. }
  693. /**
  694. * App 分享链接里 URL 类参数常未 encode,值里的 &(如 OSS 的 &fm=)会被拆成多个 query;
  695. * 从合并后的原始 query 串中按「下一个已知分享参数名」截取整段值。
  696. */
  697. var SHARE_QUERY_URL_PARAM_KEYS = [
  698. 'options',
  699. 'item',
  700. 'sourceType',
  701. 'businessType',
  702. 'sourceId',
  703. 'dynamicId',
  704. 'id',
  705. 'userId',
  706. 'userImage',
  707. 'coverUrl',
  708. 'imagePath',
  709. 'imagePaths',
  710. 'images',
  711. 'image_path',
  712. 'storeName',
  713. 'scoreAvg',
  714. 'commentCount',
  715. 'shareUserName',
  716. 'storeId',
  717. 'content',
  718. 'desc',
  719. 'text',
  720. 'tagline',
  721. 'category',
  722. 'storeCat',
  723. 'userName',
  724. 'phoneId',
  725. 'longitude',
  726. 'latitude',
  727. 'goodsId',
  728. 'pageNum',
  729. 'pageSize',
  730. 'needShowMore',
  731. 'fromHomeFeed',
  732. 'appPath',
  733. 'appPage'
  734. ];
  735. function findShareQueryValueStart(q, paramName) {
  736. var nlow = String(paramName || '').toLowerCase() + '=';
  737. var low = String(q || '').toLowerCase();
  738. var i = low.indexOf('&' + nlow);
  739. if (i >= 0) return i + 1 + nlow.length;
  740. i = low.indexOf('?' + nlow);
  741. if (i >= 0) return i + 1 + nlow.length;
  742. if (low.indexOf(nlow) === 0) return nlow.length;
  743. return -1;
  744. }
  745. function extractUrlLikeShareQueryParam(fullQuery, paramName) {
  746. var nm = String(paramName || '').trim();
  747. if (!nm || !fullQuery) return '';
  748. var start = findShareQueryValueStart(fullQuery, nm);
  749. if (start < 0) return '';
  750. var rest = fullQuery.slice(start);
  751. var nml = nm.toLowerCase();
  752. var keys = SHARE_QUERY_URL_PARAM_KEYS.filter(function (k) {
  753. return String(k).toLowerCase() !== nml;
  754. });
  755. keys.sort(function (a, b) {
  756. return b.length - a.length;
  757. });
  758. if (!keys.length) {
  759. try {
  760. return decodeURIComponent(rest.replace(/\+/g, ' '));
  761. } catch (e) {
  762. return rest;
  763. }
  764. }
  765. var esc = keys.map(function (k) {
  766. return String(k).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  767. });
  768. var re = new RegExp('&(' + esc.join('|') + ')=', 'i');
  769. var m = re.exec(rest);
  770. var end = m ? m.index : rest.length;
  771. var rawVal = rest.slice(0, end).replace(/\+/g, ' ');
  772. try {
  773. return decodeURIComponent(rawVal);
  774. } catch (e2) {
  775. return rawVal;
  776. }
  777. }
  778. function getAllParamValuesCI(queryStr, nameLower) {
  779. if (!queryStr) return [];
  780. if (nameLower === 'imagepath' || nameLower === 'imagepaths') {
  781. var one =
  782. extractUrlLikeShareQueryParam(queryStr, 'imagePath') ||
  783. (nameLower === 'imagepaths'
  784. ? extractUrlLikeShareQueryParam(queryStr, 'imagePaths')
  785. : '');
  786. if (one) return [one];
  787. }
  788. var list = [];
  789. forEachQueryParam(queryStr, function (val, key) {
  790. if (key && String(key).toLowerCase() === nameLower) {
  791. list.push(val);
  792. }
  793. });
  794. return list;
  795. }
  796. function getMergedParam(name) {
  797. var m = getMergedQueryString();
  798. if (!m) return '';
  799. var n = String(name);
  800. var nl = n.toLowerCase();
  801. if (nl === 'imagepath' || nl === 'coverurl' || nl === 'userimage') {
  802. var smart = extractUrlLikeShareQueryParam(m, n);
  803. if (smart) return smart;
  804. }
  805. var v = new URLSearchParams(m).get(name);
  806. if (v == null) {
  807. new URLSearchParams(m).forEach(function (val, key) {
  808. if (v == null && key && String(key).toLowerCase() === nl) {
  809. v = val;
  810. }
  811. });
  812. }
  813. return v == null ? '' : String(v);
  814. }
  815. function pushUniqueUrl(list, u) {
  816. u = String(u == null ? '' : u).trim();
  817. if (!u) return;
  818. if (list.indexOf(u) >= 0) return;
  819. list.push(u);
  820. }
  821. function parseOptionsItem() {
  822. var raw = getMergedParam('options');
  823. if (!raw) return null;
  824. var opts;
  825. try {
  826. opts = JSON.parse(raw);
  827. } catch (e) {
  828. return null;
  829. }
  830. if (!opts || typeof opts !== 'object') return null;
  831. var item = opts.item;
  832. if (typeof item === 'string' && item) {
  833. try {
  834. item = JSON.parse(item);
  835. } catch (e2) {
  836. return null;
  837. }
  838. }
  839. if (!item || typeof item !== 'object') return null;
  840. return item;
  841. }
  842. function normalizeMediaUrl(raw) {
  843. raw = String(raw == null ? '' : raw).trim();
  844. if (!raw || raw === '0') return '';
  845. if (/%[0-9A-Fa-f]{2}/.test(raw)) {
  846. try {
  847. raw = decodeURIComponent(raw.replace(/\+/g, ' '));
  848. } catch (e) {}
  849. }
  850. return raw;
  851. }
  852. /** 媒体 URL 是否以 .mp4 结尾(忽略 query/hash,大小写不敏感) */
  853. function isMp4VideoUrl(url) {
  854. if (!url || typeof url !== 'string') return false;
  855. var path = url.split(/[?#]/)[0].toLowerCase();
  856. return path.length >= 4 && path.slice(-4) === '.mp4';
  857. }
  858. /**
  859. * 播放用地址:只保留到 .mp4 为止,去掉 ?x-oss-process=video/snapshot... 等(否则会拿到截图流而非视频)
  860. */
  861. function getMp4PlaybackUrl(url) {
  862. var u = normalizeMediaUrl(String(url || ''));
  863. if (!u) return '';
  864. var q = u.indexOf('?');
  865. var h = u.indexOf('#');
  866. var cut = u.length;
  867. if (q >= 0) cut = Math.min(cut, q);
  868. if (h >= 0) cut = Math.min(cut, h);
  869. var base = u.slice(0, cut);
  870. if (/\.mp4$/i.test(base)) return base;
  871. return u;
  872. }
  873. function mp4UrlToJpgUrl(u) {
  874. var q = u.indexOf('?');
  875. var h = u.indexOf('#');
  876. var cut = u.length;
  877. if (q >= 0) cut = Math.min(cut, q);
  878. if (h >= 0) cut = Math.min(cut, h);
  879. var pathPart = u.slice(0, cut);
  880. var rest = u.slice(cut);
  881. if (!/\.mp4$/i.test(pathPart)) return '';
  882. return pathPart.slice(0, -4) + '.jpg' + rest;
  883. }
  884. function mp4UrlToOssSnapshotUrl(mp4Base) {
  885. if (!mp4Base) return '';
  886. return (
  887. mp4Base +
  888. (mp4Base.indexOf('?') >= 0 ? '&' : '?') +
  889. 'x-oss-process=video/snapshot,t_0,f_jpg,m_fast'
  890. );
  891. }
  892. /** 微信内部分 CDN 对 t_0 抽帧异常时,换非零时刻再试 */
  893. function mp4UrlToOssSnapshotUrlAtMs(mp4Base, ms) {
  894. if (!mp4Base) return '';
  895. var t = Math.max(0, parseInt(ms, 10) || 0);
  896. return (
  897. mp4Base +
  898. (mp4Base.indexOf('?') >= 0 ? '&' : '?') +
  899. 'x-oss-process=video/snapshot,t_' +
  900. t +
  901. ',f_jpg,m_fast'
  902. );
  903. }
  904. /** 完整 URL 是否更像「图片/截图」而非纯视频文件 */
  905. function urlLooksLikeStillImage(u) {
  906. if (!u) return false;
  907. var path = u.split(/[?#]/)[0].toLowerCase();
  908. if (/\.(jpg|jpeg|png|gif|webp|bmp)$/i.test(path)) return true;
  909. if (/x-oss-process=/i.test(u) && /snapshot|image|f_jpg/i.test(u)) return true;
  910. return false;
  911. }
  912. function isIOSVideoEnv() {
  913. return (
  914. /iP(hone|od|ad)/i.test(navigator.userAgent || '') ||
  915. (navigator.platform === 'MacIntel' && (navigator.maxTouchPoints || 0) > 1)
  916. );
  917. }
  918. function pushUniqueStillUrl(arr, u) {
  919. u = normalizeMediaUrl(String(u || '').trim());
  920. if (!u || arr.indexOf(u) >= 0) return;
  921. arr.push(u);
  922. }
  923. /**
  924. * iOS / 微信内置浏览器:video 首帧与 OSS 截图常灰屏或被 Referer 拦截。
  925. * 优先静态图链(封面、jpg、多档 OSS snapshot);叠层在 video 之上且不隐藏;失败再回退 video。
  926. */
  927. function appendMp4FirstFrameToSlide(slide, src) {
  928. var full = normalizeMediaUrl(String(src || ''));
  929. var playUrl = getMp4PlaybackUrl(full);
  930. if (!playUrl) return;
  931. var jpg = mp4UrlToJpgUrl(playUrl);
  932. var snap = mp4UrlToOssSnapshotUrl(playUrl);
  933. var snapAr =
  934. playUrl +
  935. (playUrl.indexOf('?') >= 0 ? '&' : '?') +
  936. 'x-oss-process=video/snapshot,t_0,f_jpg,ar_auto,m_fast';
  937. var poster = '';
  938. var item = parseOptionsItem();
  939. var optFrame =
  940. item && item.videoFirstFrame != null
  941. ? normalizeMediaUrl(String(item.videoFirstFrame).trim())
  942. : '';
  943. if (optFrame) {
  944. poster = optFrame;
  945. } else if (full.length > playUrl.length && urlLooksLikeStillImage(full)) {
  946. poster = full;
  947. } else if (jpg) {
  948. poster = jpg;
  949. } else if (snap) {
  950. poster = snap;
  951. }
  952. var stillList = [];
  953. if (optFrame) pushUniqueStillUrl(stillList, optFrame);
  954. if (item && item.coverImage != null && String(item.coverImage).trim() !== '') {
  955. pushUniqueStillUrl(stillList, item.coverImage);
  956. }
  957. if (item && item.cover != null && String(item.cover).trim() !== '') {
  958. pushUniqueStillUrl(stillList, item.cover);
  959. }
  960. if (full.length > playUrl.length && urlLooksLikeStillImage(full)) {
  961. pushUniqueStillUrl(stillList, full);
  962. }
  963. if (jpg) pushUniqueStillUrl(stillList, jpg);
  964. if (snap) pushUniqueStillUrl(stillList, snap);
  965. if (snapAr && snapAr !== snap) pushUniqueStillUrl(stillList, snapAr);
  966. if (isWeChatInAppBrowser()) {
  967. var snap400 = mp4UrlToOssSnapshotUrlAtMs(playUrl, 400);
  968. var snap1200 = mp4UrlToOssSnapshotUrlAtMs(playUrl, 1200);
  969. if (snap400 && snap400 !== snap) pushUniqueStillUrl(stillList, snap400);
  970. if (snap1200 && snap1200 !== snap && snap1200 !== snap400) {
  971. pushUniqueStillUrl(stillList, snap1200);
  972. }
  973. }
  974. var fragile = preferStaticStillOverVideo();
  975. if (fragile && stillList.length) {
  976. var wrapImg = document.createElement('div');
  977. wrapImg.className = 'dyn-hero__media-wrap';
  978. var heroIm = document.createElement('img');
  979. heroIm.className = 'dyn-hero__poster-swap';
  980. heroIm.alt = '';
  981. heroIm.style.width = '100%';
  982. heroIm.style.height = '100%';
  983. heroIm.style.objectFit = 'cover';
  984. applyMediaNoReferrer(heroIm);
  985. var idx = 0;
  986. heroIm.onerror = function () {
  987. idx += 1;
  988. if (idx < stillList.length) {
  989. heroIm.src = stillList[idx];
  990. } else {
  991. if (heroIm.parentNode) heroIm.parentNode.removeChild(heroIm);
  992. appendMp4VideoFirstFrameBranch(wrapImg, playUrl, poster, snap, true);
  993. }
  994. };
  995. heroIm.src = stillList[0];
  996. wrapImg.appendChild(heroIm);
  997. slide.appendChild(wrapImg);
  998. return;
  999. }
  1000. var wrap = document.createElement('div');
  1001. wrap.className = 'dyn-hero__media-wrap';
  1002. appendMp4VideoFirstFrameBranch(wrap, playUrl, poster, snap, fragile);
  1003. slide.appendChild(wrap);
  1004. }
  1005. function appendMp4VideoFirstFrameBranch(wrap, playUrl, poster, snap, fragilePosterEnv) {
  1006. var posterIm = null;
  1007. if (poster) {
  1008. posterIm = document.createElement('img');
  1009. posterIm.className = 'dyn-hero__poster-swap';
  1010. posterIm.alt = '';
  1011. applyMediaNoReferrer(posterIm);
  1012. posterIm.src = poster;
  1013. posterIm.decoding = 'async';
  1014. posterIm.onerror = function () {
  1015. this.style.display = 'none';
  1016. };
  1017. wrap.appendChild(posterIm);
  1018. }
  1019. var vid = document.createElement('video');
  1020. vid.className = 'dyn-hero__video--poster-only';
  1021. applyMediaNoReferrer(vid);
  1022. var playSrc = playUrl;
  1023. if (fragilePosterEnv && playUrl.indexOf('#') < 0) {
  1024. try {
  1025. playSrc = playUrl + '#t=0.12';
  1026. } catch (e0) {
  1027. playSrc = playUrl;
  1028. }
  1029. }
  1030. vid.src = playSrc;
  1031. if (poster) vid.setAttribute('poster', poster);
  1032. vid.setAttribute('playsinline', '');
  1033. vid.setAttribute('webkit-playsinline', '');
  1034. vid.setAttribute('disableremoteplayback', '');
  1035. vid.setAttribute('preload', 'auto');
  1036. vid.muted = true;
  1037. vid.setAttribute('muted', '');
  1038. vid.playsInline = true;
  1039. vid.loop = false;
  1040. vid.defaultMuted = true;
  1041. function hidePosterSwap() {
  1042. if (fragilePosterEnv) return;
  1043. if (!posterIm) return;
  1044. posterIm.style.opacity = '0';
  1045. window.setTimeout(function () {
  1046. if (posterIm && posterIm.parentNode) {
  1047. posterIm.style.visibility = 'hidden';
  1048. }
  1049. }, 220);
  1050. }
  1051. function iosKickDecodePaint() {
  1052. if (!fragilePosterEnv) return;
  1053. var pr = vid.play();
  1054. if (pr && typeof pr.then === 'function') {
  1055. pr.then(function () {
  1056. vid.pause();
  1057. }).catch(function () {});
  1058. }
  1059. }
  1060. function seekToFirstFrame() {
  1061. try {
  1062. if (vid.readyState < 2) return;
  1063. var dur = vid.duration;
  1064. var floorT = fragilePosterEnv ? 0.12 : 0.04;
  1065. if (typeof dur === 'number' && isFinite(dur) && dur > 0) {
  1066. var t = Math.max(floorT, Math.min(dur * 0.06, dur - 0.05));
  1067. vid.currentTime = t;
  1068. } else {
  1069. vid.currentTime = floorT;
  1070. }
  1071. } catch (e) {}
  1072. }
  1073. vid.addEventListener('loadedmetadata', function () {
  1074. seekToFirstFrame();
  1075. });
  1076. vid.addEventListener('loadeddata', function () {
  1077. seekToFirstFrame();
  1078. });
  1079. vid.addEventListener('seeked', function () {
  1080. vid.pause();
  1081. iosKickDecodePaint();
  1082. if (!fragilePosterEnv && vid.videoWidth > 0) {
  1083. hidePosterSwap();
  1084. }
  1085. });
  1086. vid.addEventListener('canplay', function once() {
  1087. vid.removeEventListener('canplay', once);
  1088. seekToFirstFrame();
  1089. });
  1090. vid.addEventListener('timeupdate', function onTU() {
  1091. if (!fragilePosterEnv && vid.currentTime >= 0.08 && vid.videoWidth > 0) {
  1092. vid.removeEventListener('timeupdate', onTU);
  1093. hidePosterSwap();
  1094. }
  1095. });
  1096. vid.addEventListener('error', function () {
  1097. if (vid.getAttribute('data-fallback') === '1') return;
  1098. vid.setAttribute('data-fallback', '1');
  1099. if (snap && String(vid.src || '').indexOf('x-oss-process') < 0) {
  1100. vid.src = snap;
  1101. try {
  1102. vid.load();
  1103. } catch (e2) {}
  1104. }
  1105. });
  1106. wrap.appendChild(vid);
  1107. }
  1108. /** 顶层 userImage;若无则 options.item.userImage(须为字符串地址) */
  1109. function resolveUserAvatarUrl() {
  1110. var top = normalizeMediaUrl(getMergedParam('userImage'));
  1111. if (top) return top;
  1112. var item = parseOptionsItem();
  1113. if (!item) return '';
  1114. var u = item.userImage;
  1115. if (u == null || u === '') return '';
  1116. if (typeof u === 'number') return '';
  1117. return normalizeMediaUrl(String(u));
  1118. }
  1119. /**
  1120. * App 分享链接里 imagePath 常在 options JSON 的 item 字符串内:
  1121. * options={ "item": "{\"imagePath\":\"https://...\",\"imageList\":[...]}" }
  1122. */
  1123. function collectImagesFromOptionsParam() {
  1124. var item = parseOptionsItem();
  1125. if (!item) return [];
  1126. var out = [];
  1127. if (Array.isArray(item.imageList)) {
  1128. item.imageList.forEach(function (u) {
  1129. pushUniqueUrl(out, u);
  1130. });
  1131. }
  1132. if (item.imagePath) {
  1133. String(item.imagePath).split(',').forEach(function (seg) {
  1134. pushUniqueUrl(out, seg);
  1135. });
  1136. }
  1137. pushUniqueUrl(out, item.cover);
  1138. pushUniqueUrl(out, item.src);
  1139. return out;
  1140. }
  1141. /**
  1142. * 从地址栏收集轮播图:① 顶层 imagePath;② options.item 内 imagePath / imageList;
  1143. * ③ 顶层 coverUrl。
  1144. */
  1145. function collectImageUrlsFromUrl() {
  1146. var merged = getMergedQueryString();
  1147. var vals = getAllParamValuesCI(merged, 'imagepath');
  1148. if (!vals.length) {
  1149. vals = getAllParamValuesCI(merged, 'imagepaths');
  1150. }
  1151. if (!vals.length) {
  1152. vals = getAllParamValuesCI(merged, 'images');
  1153. }
  1154. if (!vals.length) {
  1155. vals = getAllParamValuesCI(merged, 'image_path');
  1156. }
  1157. var urls = [];
  1158. vals.forEach(function (val) {
  1159. val = String(val == null ? '' : val).trim();
  1160. if (!val) return;
  1161. if (val.charAt(0) === '[') {
  1162. try {
  1163. var arr = JSON.parse(val);
  1164. if (Array.isArray(arr)) {
  1165. arr.forEach(function (u) {
  1166. pushUniqueUrl(urls, u);
  1167. });
  1168. return;
  1169. }
  1170. } catch (e1) {
  1171. try {
  1172. var arr2 = JSON.parse(decodeURIComponent(val));
  1173. if (Array.isArray(arr2)) {
  1174. arr2.forEach(function (u) {
  1175. pushUniqueUrl(urls, u);
  1176. });
  1177. return;
  1178. }
  1179. } catch (e2) {}
  1180. }
  1181. }
  1182. val.split(',').forEach(function (seg) {
  1183. seg = String(seg).trim();
  1184. if (!seg) return;
  1185. if (/%[0-9A-Fa-f]{2}/.test(seg)) {
  1186. try {
  1187. seg = decodeURIComponent(seg.replace(/\+/g, ' '));
  1188. } catch (e) {}
  1189. }
  1190. pushUniqueUrl(urls, seg);
  1191. });
  1192. });
  1193. if (!urls.length) {
  1194. collectImagesFromOptionsParam().forEach(function (u) {
  1195. pushUniqueUrl(urls, u);
  1196. });
  1197. }
  1198. if (!urls.length) {
  1199. var cover = getMergedParam('coverUrl');
  1200. if (cover) {
  1201. try {
  1202. cover = decodeURIComponent(cover);
  1203. } catch (e) {}
  1204. pushUniqueUrl(urls, cover);
  1205. }
  1206. }
  1207. return urls;
  1208. }
  1209. var heroI = 0;
  1210. var heroTimer = null;
  1211. var dynHeroSwipeInited = false;
  1212. function getHeroSlideCount() {
  1213. return document.getElementById('dynHeroDots').querySelectorAll('.dyn-hero__dot').length;
  1214. }
  1215. function initDynCarousel(slides) {
  1216. var track = document.getElementById('dynHeroTrack');
  1217. var dotsWrap = document.getElementById('dynHeroDots');
  1218. function go(n) {
  1219. var count = getHeroSlideCount();
  1220. if (count < 1) return;
  1221. heroI = ((n % count) + count) % count;
  1222. track.style.transform = 'translateX(' + (-heroI * 100) + '%)';
  1223. dotsWrap.querySelectorAll('.dyn-hero__dot').forEach(function (el, idx) {
  1224. el.classList.toggle('is-active', idx === heroI);
  1225. });
  1226. }
  1227. if (heroTimer) {
  1228. clearInterval(heroTimer);
  1229. heroTimer = null;
  1230. }
  1231. if (slides < 2) {
  1232. heroI = 0;
  1233. track.style.transform = 'translateX(0)';
  1234. return;
  1235. }
  1236. heroTimer = setInterval(function () { go(heroI + 1); }, 4000);
  1237. if (!dynHeroSwipeInited) {
  1238. dynHeroSwipeInited = true;
  1239. var hero = document.getElementById('dynHero');
  1240. var startX = 0;
  1241. hero.addEventListener('touchstart', function (e) {
  1242. startX = e.changedTouches[0].clientX;
  1243. }, { passive: true });
  1244. hero.addEventListener('touchend', function (e) {
  1245. var dx = e.changedTouches[0].clientX - startX;
  1246. if (Math.abs(dx) > 40) go(dx < 0 ? heroI + 1 : heroI - 1);
  1247. }, { passive: true });
  1248. }
  1249. }
  1250. function buildDynSlides(urls) {
  1251. heroI = 0;
  1252. var track = document.getElementById('dynHeroTrack');
  1253. var dotsWrap = document.getElementById('dynHeroDots');
  1254. var heroEl = document.getElementById('dynHero');
  1255. track.innerHTML = '';
  1256. dotsWrap.innerHTML = '';
  1257. track.style.transform = 'translateX(0)';
  1258. if (heroEl) heroEl.classList.remove('dyn-hero--no-dots');
  1259. var list = (urls || []).map(function (u) {
  1260. return normalizeMediaUrl(u);
  1261. }).filter(Boolean);
  1262. if (!list.length) {
  1263. var slide = document.createElement('div');
  1264. slide.className = 'dyn-hero__slide';
  1265. var img = document.createElement('img');
  1266. img.src = 'images/hero.png';
  1267. img.alt = '';
  1268. slide.appendChild(img);
  1269. track.appendChild(slide);
  1270. var dot = document.createElement('span');
  1271. dot.className = 'dyn-hero__dot is-active';
  1272. dotsWrap.appendChild(dot);
  1273. return initDynCarousel(1);
  1274. }
  1275. /** 任意一条为 .mp4:只展示第一个的首帧静图,不轮播、无指示点(与纯图多条轮播区分) */
  1276. var firstMp4 = '';
  1277. for (var mi = 0; mi < list.length; mi++) {
  1278. if (isMp4VideoUrl(list[mi])) {
  1279. firstMp4 = list[mi];
  1280. break;
  1281. }
  1282. }
  1283. if (firstMp4) {
  1284. if (heroEl) heroEl.classList.add('dyn-hero--no-dots');
  1285. var vSlide = document.createElement('div');
  1286. vSlide.className = 'dyn-hero__slide';
  1287. appendMp4FirstFrameToSlide(vSlide, firstMp4);
  1288. track.appendChild(vSlide);
  1289. return initDynCarousel(1);
  1290. }
  1291. list.forEach(function (url, d) {
  1292. var s = document.createElement('div');
  1293. s.className = 'dyn-hero__slide';
  1294. if (isMp4VideoUrl(url)) {
  1295. appendMp4FirstFrameToSlide(s, url);
  1296. } else {
  1297. var im = document.createElement('img');
  1298. if (/^https?:/i.test(url)) {
  1299. applyMediaNoReferrer(im);
  1300. }
  1301. im.src = url;
  1302. im.alt = '';
  1303. s.appendChild(im);
  1304. }
  1305. track.appendChild(s);
  1306. var dot = document.createElement('span');
  1307. dot.className = 'dyn-hero__dot' + (d === 0 ? ' is-active' : '');
  1308. dotsWrap.appendChild(dot);
  1309. });
  1310. initDynCarousel(list.length);
  1311. }
  1312. function resolveCommentRequestParams() {
  1313. var item = parseOptionsItem();
  1314. var sourceType = getMergedParam('sourceType') || getMergedParam('businessType') || q('sourceType') || q('businessType');
  1315. if (sourceType === '' && item && item.type != null) sourceType = String(item.type);
  1316. var sourceId = getMergedParam('sourceId') || getMergedParam('dynamicId') || q('dynamicId') || q('id');
  1317. if (!sourceId && item && item.id != null) sourceId = String(item.id);
  1318. var userId = getMergedParam('userId') || q('userId');
  1319. if (!sourceType || !sourceId || !userId) return null;
  1320. return {
  1321. sourceType: String(sourceType),
  1322. sourceId: String(sourceId),
  1323. userId: String(userId),
  1324. pageNum: COMMENT_PAGE_NUM,
  1325. pageSize: COMMENT_PAGE_SIZE
  1326. };
  1327. }
  1328. function normalizeCommentPayload(data) {
  1329. if (!data) return { list: [], total: 0 };
  1330. if (Array.isArray(data)) return { list: data, total: data.length };
  1331. var list = data.list || data.records || data.rows;
  1332. if (!Array.isArray(list) && Array.isArray(data.data)) list = data.data;
  1333. if (!Array.isArray(list)) list = [];
  1334. var total = data.total != null ? Number(data.total) : (data.count != null ? Number(data.count) : list.length);
  1335. if (isNaN(total)) total = list.length;
  1336. return { list: list, total: total };
  1337. }
  1338. function formatCommentCountParens(n) {
  1339. return '(' + String(n) + ')';
  1340. }
  1341. function renderComments(resData) {
  1342. var norm = normalizeCommentPayload(resData);
  1343. var list = norm.list;
  1344. var total = norm.total;
  1345. document.getElementById('commentCount').textContent = formatCommentCountParens(total);
  1346. document.getElementById('storeReviews').textContent = String(total);
  1347. var wrap = document.getElementById('commentList');
  1348. var empty = document.getElementById('commentEmpty');
  1349. wrap.innerHTML = '';
  1350. wrap.classList.remove('is-visible');
  1351. if (!list.length) {
  1352. empty.style.display = 'flex';
  1353. return;
  1354. }
  1355. empty.style.display = 'none';
  1356. wrap.classList.add('is-visible');
  1357. list.forEach(function (row) {
  1358. if (!row || typeof row !== 'object') return;
  1359. 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) : '')));
  1360. 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) : '用户')));
  1361. var time = row.createdTime != null ? String(row.createdTime) : (row.createTime != null ? String(row.createTime) : (row.time != null ? String(row.time) : ''));
  1362. var headImg = row.headImg != null ? String(row.headImg).trim() : '';
  1363. if (!headImg || headImg === '0') headImg = COMMENT_AVATAR_FALLBACK;
  1364. var rowEl = document.createElement('div');
  1365. rowEl.className = 'comment-item';
  1366. var avatar = document.createElement('img');
  1367. avatar.className = 'comment-item__avatar';
  1368. avatar.alt = '';
  1369. avatar.src = headImg;
  1370. avatar.onerror = function () {
  1371. this.onerror = null;
  1372. this.src = COMMENT_AVATAR_FALLBACK;
  1373. };
  1374. var col = document.createElement('div');
  1375. col.className = 'comment-item__col';
  1376. var meta = document.createElement('div');
  1377. meta.className = 'comment-item__meta';
  1378. var nameEl = document.createElement('span');
  1379. nameEl.className = 'comment-item__name';
  1380. nameEl.textContent = uname;
  1381. meta.appendChild(nameEl);
  1382. var body = document.createElement('div');
  1383. body.className = 'comment-item__text';
  1384. body.textContent = text;
  1385. col.appendChild(meta);
  1386. col.appendChild(body);
  1387. if (time) {
  1388. var timeEl = document.createElement('div');
  1389. timeEl.className = 'comment-item__time';
  1390. timeEl.textContent = time;
  1391. col.appendChild(timeEl);
  1392. }
  1393. rowEl.appendChild(avatar);
  1394. rowEl.appendChild(col);
  1395. wrap.appendChild(rowEl);
  1396. });
  1397. }
  1398. function loadComments() {
  1399. var p = resolveCommentRequestParams();
  1400. if (!p) return;
  1401. var path = '/commonComment/getListBySourceType?' +
  1402. 'sourceType=' + encodeURIComponent(p.sourceType) +
  1403. '&sourceId=' + encodeURIComponent(p.sourceId) +
  1404. '&userId=' + encodeURIComponent(p.userId) +
  1405. '&pageNum=' + encodeURIComponent(String(p.pageNum)) +
  1406. '&pageSize=' + encodeURIComponent(String(p.pageSize));
  1407. apiFetch(path)
  1408. .then(function (res) {
  1409. if (res.code !== 200) {
  1410. renderComments(null);
  1411. return;
  1412. }
  1413. renderComments(res.data);
  1414. })
  1415. .catch(function (e) {
  1416. console.error(e);
  1417. renderComments(null);
  1418. });
  1419. }
  1420. function applyQueryContent() {
  1421. /** 优先展示分享者昵称(App 传 shareUserName);否则动态作者 userName、店铺名 */
  1422. var name =
  1423. getMergedParam('shareUserName') ||
  1424. q('shareUserName') ||
  1425. q('userName') ||
  1426. q('storeName');
  1427. if (name) document.getElementById('userName').textContent = decodeURIComponent(name);
  1428. var avatarUrl = resolveUserAvatarUrl();
  1429. if (avatarUrl) {
  1430. document.getElementById('userAvatar').src = avatarUrl;
  1431. }
  1432. var desc = q('content') || q('desc') || q('text');
  1433. if (desc) {
  1434. document.getElementById('userDesc').textContent = decodeURIComponent(desc);
  1435. }
  1436. var storeNameParam = getMergedParam('storeName') || q('storeName');
  1437. if (storeNameParam) {
  1438. try {
  1439. document.getElementById('storeTitle').textContent = decodeURIComponent(storeNameParam);
  1440. } catch (e) {
  1441. document.getElementById('storeTitle').textContent = storeNameParam;
  1442. }
  1443. }
  1444. var scoreAvgParam = getMergedParam('scoreAvg') || q('scoreAvg');
  1445. if (scoreAvgParam !== '') {
  1446. var scNum = parseFloat(scoreAvgParam, 10);
  1447. document.getElementById('storeScore').textContent =
  1448. !isNaN(scNum) ? scNum.toFixed(1) : String(scoreAvgParam);
  1449. }
  1450. buildDynSlides(collectImageUrlsFromUrl());
  1451. if (!resolveCommentRequestParams()) {
  1452. var cc = getMergedParam('commentCount') || q('commentCount');
  1453. if (cc !== '') {
  1454. var n = parseInt(cc, 10);
  1455. if (!isNaN(n)) {
  1456. document.getElementById('commentCount').textContent = formatCommentCountParens(n);
  1457. document.getElementById('storeReviews').textContent = String(n);
  1458. document.getElementById('commentEmpty').style.display = n > 0 ? 'none' : 'flex';
  1459. }
  1460. }
  1461. }
  1462. var tag = q('tagline');
  1463. if (tag) document.getElementById('storeTagline').textContent = decodeURIComponent(tag);
  1464. var cat = q('category') || q('storeCat');
  1465. if (cat) document.getElementById('storeCat').textContent = decodeURIComponent(cat);
  1466. }
  1467. function boot() {
  1468. applyQueryContent();
  1469. loadComments();
  1470. var openBtn = document.getElementById('openApp');
  1471. if (openBtn) {
  1472. openBtn.addEventListener('click', function () {
  1473. tryOpenHBuilderApp();
  1474. });
  1475. }
  1476. }
  1477. if (document.readyState === 'complete') {
  1478. boot();
  1479. } else {
  1480. window.onload = boot;
  1481. }
  1482. })();
  1483. </script>
  1484. </body>
  1485. </html>