promotion-query-logs.html 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970
  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">
  6. <title>PromotionQuery Logs</title>
  7. <style>
  8. :root {
  9. color-scheme: light;
  10. --bg: #f6f7f9;
  11. --panel: #ffffff;
  12. --text: #1f2328;
  13. --muted: #68707d;
  14. --border: #d8dee4;
  15. --primary: #0969da;
  16. --primary-hover: #0759b8;
  17. --danger: #b42318;
  18. --code-bg: #f0f3f6;
  19. }
  20. * {
  21. box-sizing: border-box;
  22. }
  23. body {
  24. margin: 0;
  25. background: var(--bg);
  26. color: var(--text);
  27. font: 14px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  28. }
  29. main {
  30. max-width: 1180px;
  31. margin: 0 auto;
  32. padding: 24px;
  33. }
  34. h1 {
  35. margin: 0 0 16px;
  36. font-size: 22px;
  37. font-weight: 650;
  38. letter-spacing: 0;
  39. }
  40. .toolbar {
  41. display: grid;
  42. grid-template-columns: minmax(260px, 1fr) 180px 110px 110px auto auto;
  43. gap: 10px;
  44. align-items: end;
  45. margin-bottom: 14px;
  46. padding: 14px;
  47. background: var(--panel);
  48. border: 1px solid var(--border);
  49. border-radius: 8px;
  50. }
  51. label {
  52. display: grid;
  53. gap: 5px;
  54. color: var(--muted);
  55. font-size: 12px;
  56. font-weight: 600;
  57. }
  58. input {
  59. width: 100%;
  60. height: 36px;
  61. padding: 7px 9px;
  62. border: 1px solid var(--border);
  63. border-radius: 6px;
  64. color: var(--text);
  65. background: #fff;
  66. font: inherit;
  67. }
  68. button {
  69. height: 36px;
  70. padding: 0 14px;
  71. border: 1px solid var(--primary);
  72. border-radius: 6px;
  73. background: var(--primary);
  74. color: #fff;
  75. font: inherit;
  76. font-weight: 600;
  77. cursor: pointer;
  78. white-space: nowrap;
  79. }
  80. button:hover {
  81. background: var(--primary-hover);
  82. border-color: var(--primary-hover);
  83. }
  84. button.secondary {
  85. background: #fff;
  86. color: var(--primary);
  87. }
  88. button.secondary:hover {
  89. background: #f1f6fd;
  90. }
  91. button:disabled {
  92. opacity: 0.6;
  93. cursor: not-allowed;
  94. }
  95. .status {
  96. margin: 0 0 8px;
  97. color: var(--muted);
  98. }
  99. .status.error {
  100. color: var(--danger);
  101. }
  102. .pager {
  103. display: flex;
  104. gap: 10px;
  105. align-items: center;
  106. margin: 0 0 14px;
  107. }
  108. .page-info {
  109. color: var(--muted);
  110. }
  111. .logs {
  112. display: grid;
  113. gap: 12px;
  114. }
  115. .log-item {
  116. background: var(--panel);
  117. border: 1px solid var(--border);
  118. border-radius: 8px;
  119. overflow: hidden;
  120. }
  121. .log-head {
  122. display: grid;
  123. grid-template-columns: minmax(0, 1fr) auto;
  124. gap: 12px;
  125. align-items: center;
  126. padding: 12px 14px;
  127. border-bottom: 1px solid var(--border);
  128. background: #fbfcfd;
  129. }
  130. .log-title {
  131. min-width: 0;
  132. }
  133. .file-name {
  134. display: block;
  135. overflow: hidden;
  136. text-overflow: ellipsis;
  137. white-space: nowrap;
  138. font-weight: 650;
  139. }
  140. .time {
  141. color: var(--muted);
  142. font-size: 12px;
  143. }
  144. .image-url {
  145. display: block;
  146. margin-top: 4px;
  147. overflow: hidden;
  148. color: var(--primary);
  149. text-overflow: ellipsis;
  150. white-space: nowrap;
  151. text-decoration: none;
  152. }
  153. .source-pill {
  154. display: inline-block;
  155. margin-top: 6px;
  156. padding: 2px 7px;
  157. border: 1px solid var(--border);
  158. border-radius: 999px;
  159. color: var(--muted);
  160. background: #fff;
  161. font-size: 12px;
  162. }
  163. .image-preview {
  164. display: grid;
  165. grid-template-columns: 112px minmax(0, 1fr);
  166. gap: 12px;
  167. align-items: center;
  168. padding: 12px 14px;
  169. border-bottom: 1px solid var(--border);
  170. background: #fff;
  171. }
  172. .image-preview img {
  173. width: 112px;
  174. height: 112px;
  175. object-fit: contain;
  176. border: 1px solid var(--border);
  177. border-radius: 6px;
  178. background: #f8fafc;
  179. }
  180. .image-meta {
  181. min-width: 0;
  182. color: var(--muted);
  183. }
  184. .image-meta strong {
  185. display: block;
  186. margin-bottom: 4px;
  187. color: var(--text);
  188. font-weight: 650;
  189. }
  190. .response-view {
  191. padding: 14px;
  192. border-bottom: 1px solid var(--border);
  193. background: #fff;
  194. }
  195. .response-title {
  196. display: flex;
  197. gap: 10px;
  198. align-items: baseline;
  199. justify-content: space-between;
  200. margin-bottom: 12px;
  201. }
  202. .response-title strong {
  203. font-size: 15px;
  204. }
  205. .response-title span {
  206. color: var(--muted);
  207. font-size: 12px;
  208. }
  209. .response-message {
  210. margin: 0;
  211. padding: 10px 12px;
  212. color: var(--muted);
  213. background: #f8fafc;
  214. border: 1px solid var(--border);
  215. border-radius: 6px;
  216. }
  217. .product-grid {
  218. display: grid;
  219. grid-template-columns: repeat(auto-fill, minmax(285px, 1fr));
  220. gap: 10px;
  221. }
  222. .product-card {
  223. display: grid;
  224. grid-template-columns: 92px minmax(0, 1fr);
  225. gap: 10px;
  226. min-width: 0;
  227. padding: 10px;
  228. border: 1px solid var(--border);
  229. border-radius: 8px;
  230. background: #fff;
  231. }
  232. .product-card img,
  233. .product-image-empty {
  234. width: 92px;
  235. height: 92px;
  236. object-fit: contain;
  237. border: 1px solid var(--border);
  238. border-radius: 6px;
  239. background: #f8fafc;
  240. }
  241. .product-image-empty {
  242. display: grid;
  243. place-items: center;
  244. color: var(--muted);
  245. font-size: 12px;
  246. }
  247. .product-info {
  248. min-width: 0;
  249. }
  250. .product-name {
  251. display: -webkit-box;
  252. overflow: hidden;
  253. color: var(--text);
  254. font-weight: 650;
  255. line-height: 1.4;
  256. text-decoration: none;
  257. -webkit-line-clamp: 2;
  258. -webkit-box-orient: vertical;
  259. }
  260. .product-name:hover {
  261. color: var(--primary);
  262. }
  263. .product-price {
  264. display: flex;
  265. flex-wrap: wrap;
  266. gap: 8px;
  267. align-items: baseline;
  268. margin-top: 7px;
  269. }
  270. .sale-price {
  271. color: var(--danger);
  272. font-size: 16px;
  273. font-weight: 700;
  274. }
  275. .origin-price {
  276. color: var(--muted);
  277. text-decoration: line-through;
  278. }
  279. .coupon {
  280. padding: 1px 6px;
  281. color: var(--danger);
  282. border: 1px solid #f0b8b2;
  283. border-radius: 999px;
  284. background: #fff5f3;
  285. font-size: 12px;
  286. }
  287. .product-meta {
  288. margin-top: 6px;
  289. color: var(--muted);
  290. font-size: 12px;
  291. }
  292. .product-actions {
  293. display: flex;
  294. flex-wrap: wrap;
  295. gap: 8px;
  296. margin-top: 8px;
  297. }
  298. .product-actions a {
  299. padding: 3px 8px;
  300. color: var(--primary);
  301. border: 1px solid var(--border);
  302. border-radius: 6px;
  303. text-decoration: none;
  304. font-size: 12px;
  305. }
  306. .product-actions a:hover {
  307. background: #f1f6fd;
  308. }
  309. .raw-log {
  310. border-bottom: 1px solid var(--border);
  311. background: var(--code-bg);
  312. }
  313. .raw-log summary {
  314. padding: 10px 14px;
  315. color: var(--muted);
  316. background: #fbfcfd;
  317. cursor: pointer;
  318. user-select: none;
  319. }
  320. .log-body {
  321. margin: 0;
  322. max-height: 520px;
  323. overflow: auto;
  324. padding: 14px;
  325. background: var(--code-bg);
  326. font: 12px/1.55 "SFMono-Regular", Consolas, "Liberation Mono", monospace;
  327. white-space: pre-wrap;
  328. word-break: break-word;
  329. }
  330. .empty {
  331. padding: 32px 18px;
  332. color: var(--muted);
  333. text-align: center;
  334. background: var(--panel);
  335. border: 1px solid var(--border);
  336. border-radius: 8px;
  337. }
  338. @media (max-width: 760px) {
  339. main {
  340. padding: 14px;
  341. }
  342. .toolbar {
  343. grid-template-columns: 1fr;
  344. }
  345. .log-head {
  346. grid-template-columns: 1fr;
  347. }
  348. .image-preview {
  349. grid-template-columns: 84px minmax(0, 1fr);
  350. }
  351. .image-preview img {
  352. width: 84px;
  353. height: 84px;
  354. }
  355. .product-grid {
  356. grid-template-columns: 1fr;
  357. }
  358. }
  359. </style>
  360. </head>
  361. <body>
  362. <main>
  363. <h1>PromotionQuery 日志</h1>
  364. <div class="toolbar">
  365. <label>
  366. API 地址
  367. <input id="apiBase" autocomplete="off">
  368. </label>
  369. <label>
  370. 日志 ID
  371. <input id="keyword" autocomplete="off" placeholder="支持输入 .log">
  372. </label>
  373. <label>
  374. 每页
  375. <input id="count" type="number" min="1" max="1100" value="20">
  376. </label>
  377. <label>
  378. 页码
  379. <input id="page" type="number" min="1" value="1">
  380. </label>
  381. <button id="loadBtn" type="button">查询</button>
  382. <button id="copyUrlBtn" class="secondary" type="button">复制接口</button>
  383. </div>
  384. <p id="status" class="status"></p>
  385. <div class="pager">
  386. <button id="prevBtn" class="secondary" type="button">上一页</button>
  387. <span id="pageInfo" class="page-info">共 0 条</span>
  388. <button id="nextBtn" class="secondary" type="button">下一页</button>
  389. </div>
  390. <section id="logs" class="logs"></section>
  391. </main>
  392. <script>
  393. const endpointPath = "/api/PromotionQuerySampleLogs";
  394. const apiBaseInput = document.getElementById("apiBase");
  395. const keywordInput = document.getElementById("keyword");
  396. const countInput = document.getElementById("count");
  397. const pageInput = document.getElementById("page");
  398. const loadBtn = document.getElementById("loadBtn");
  399. const copyUrlBtn = document.getElementById("copyUrlBtn");
  400. const statusEl = document.getElementById("status");
  401. const prevBtn = document.getElementById("prevBtn");
  402. const nextBtn = document.getElementById("nextBtn");
  403. const pageInfoEl = document.getElementById("pageInfo");
  404. const logsEl = document.getElementById("logs");
  405. let currentPage = 1;
  406. let currentTotalPages = 0;
  407. apiBaseInput.value = "http://similar.molilian.com:5005";
  408. function buildUrl(pageOverride) {
  409. const base = apiBaseInput.value.replace(/\/+$/, "");
  410. const pageSize = Math.min(Math.max(Number(countInput.value) || 20, 1), 1100);
  411. const requestedPage = Number(pageOverride);
  412. const page = Math.max(
  413. Number.isFinite(requestedPage) ? requestedPage : (Number(pageInput.value) || 1),
  414. 1
  415. );
  416. const keyword = keywordInput.value.trim();
  417. countInput.value = String(pageSize);
  418. pageInput.value = String(page);
  419. const url = new URL(`${base}${endpointPath}`);
  420. url.searchParams.set("page", String(page));
  421. url.searchParams.set("pageSize", String(pageSize));
  422. if (keyword) {
  423. url.searchParams.set("keyword", keyword);
  424. }
  425. return url.toString();
  426. }
  427. function setStatus(text, isError = false) {
  428. statusEl.textContent = text;
  429. statusEl.classList.toggle("error", isError);
  430. }
  431. function updatePager(page, totalPages, totalCount) {
  432. currentPage = Math.max(page, 1);
  433. currentTotalPages = Math.max(totalPages, 0);
  434. pageInput.value = String(currentPage);
  435. prevBtn.disabled = currentPage <= 1;
  436. nextBtn.disabled = currentTotalPages <= 0 || currentPage >= currentTotalPages;
  437. pageInfoEl.textContent = totalCount > 0
  438. ? `第 ${currentPage} / ${Math.max(currentTotalPages, 1)} 页,共 ${totalCount} 条`
  439. : "共 0 条";
  440. }
  441. function getLogSection(content, name) {
  442. const marker = new RegExp(`\\t${name}\\r?\\n`);
  443. const match = marker.exec(content);
  444. if (!match) {
  445. return "";
  446. }
  447. const start = match.index + match[0].length;
  448. const rest = content.slice(start);
  449. const next = rest.search(/\r?\n\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}\t/);
  450. return (next >= 0 ? rest.slice(0, next) : rest).trim();
  451. }
  452. function normalizeImageUrl(value) {
  453. const text = (value || "").trim();
  454. if (text.startsWith("//")) {
  455. return `https:${text}`;
  456. }
  457. return text;
  458. }
  459. function detectImageMime(base64) {
  460. if (base64.startsWith("/9j/")) {
  461. return "image/jpeg";
  462. }
  463. if (base64.startsWith("iVBOR")) {
  464. return "image/png";
  465. }
  466. if (base64.startsWith("R0lGOD")) {
  467. return "image/gif";
  468. }
  469. if (base64.startsWith("UklGR")) {
  470. return "image/webp";
  471. }
  472. return "image/jpeg";
  473. }
  474. function buildImageSrcFromImg(value) {
  475. const text = (value || "").trim();
  476. if (!text) {
  477. return "";
  478. }
  479. if (/^data:image\//i.test(text)) {
  480. return text;
  481. }
  482. if (/^https?:\/\//i.test(text) || text.startsWith("//")) {
  483. return normalizeImageUrl(text);
  484. }
  485. const base64 = text.replace(/\s/g, "");
  486. if (base64.length < 32 || !/^[A-Za-z0-9+/=]+$/.test(base64)) {
  487. return "";
  488. }
  489. return `data:${detectImageMime(base64)};base64,${base64}`;
  490. }
  491. function getClientPost(content) {
  492. const text = getLogSection(content, "clientPost");
  493. if (!text) {
  494. return {};
  495. }
  496. try {
  497. return JSON.parse(text);
  498. } catch {
  499. return {};
  500. }
  501. }
  502. function getImageInfo(content) {
  503. const loggedUrl = normalizeImageUrl(getLogSection(content, "imageUrl"));
  504. if (loggedUrl) {
  505. return {
  506. source: "url参数",
  507. src: loggedUrl,
  508. href: loggedUrl,
  509. label: loggedUrl
  510. };
  511. }
  512. const post = getClientPost(content);
  513. const requestUrl = normalizeImageUrl(post.url || post.pic || post.imageUrl || post.pictureUrl || "");
  514. if (requestUrl) {
  515. return {
  516. source: "url参数",
  517. src: requestUrl,
  518. href: requestUrl,
  519. label: requestUrl
  520. };
  521. }
  522. const imgValue = post.img || post.image || "";
  523. const imgSrc = buildImageSrcFromImg(imgValue);
  524. if (!imgSrc) {
  525. return null;
  526. }
  527. const isUrl = /^https?:\/\//i.test(imgSrc);
  528. return {
  529. source: isUrl ? "img参数URL" : "img参数base64",
  530. src: imgSrc,
  531. href: isUrl ? imgSrc : "",
  532. label: isUrl ? imgSrc : `base64 图片,约 ${Math.round(String(imgValue).length / 1024)} KB`
  533. };
  534. }
  535. function createImagePreview(imageInfo) {
  536. if (!imageInfo) {
  537. return null;
  538. }
  539. const preview = document.createElement("div");
  540. preview.className = "image-preview";
  541. const img = document.createElement("img");
  542. img.src = imageInfo.src;
  543. img.alt = imageInfo.source;
  544. img.loading = "lazy";
  545. img.referrerPolicy = "no-referrer";
  546. const meta = document.createElement("div");
  547. meta.className = "image-meta";
  548. const title = document.createElement("strong");
  549. title.textContent = imageInfo.source;
  550. meta.appendChild(title);
  551. if (imageInfo.href) {
  552. const link = document.createElement("a");
  553. link.className = "image-url";
  554. link.href = imageInfo.href;
  555. link.target = "_blank";
  556. link.rel = "noreferrer";
  557. link.textContent = imageInfo.label;
  558. meta.appendChild(link);
  559. } else {
  560. const text = document.createElement("span");
  561. text.textContent = imageInfo.label;
  562. meta.appendChild(text);
  563. }
  564. preview.append(img, meta);
  565. return preview;
  566. }
  567. function parseJson(text) {
  568. if (!text) {
  569. return null;
  570. }
  571. try {
  572. return JSON.parse(text);
  573. } catch {
  574. return null;
  575. }
  576. }
  577. function getResponseData(content) {
  578. return parseJson(getLogSection(content, "responseJson"));
  579. }
  580. function getProducts(response) {
  581. if (Array.isArray(response)) {
  582. return response.filter((item) => item && typeof item === "object");
  583. }
  584. if (!response || typeof response !== "object") {
  585. return [];
  586. }
  587. const containers = [
  588. response,
  589. response.data,
  590. response.result,
  591. response.response
  592. ].filter((item) => item && typeof item === "object");
  593. const keys = ["promotionImg", "PromotionImg", "items", "Items", "list", "List"];
  594. for (const container of containers) {
  595. if (Array.isArray(container)) {
  596. return container.filter((item) => item && typeof item === "object");
  597. }
  598. for (const key of keys) {
  599. if (Array.isArray(container[key])) {
  600. return container[key].filter((item) => item && typeof item === "object");
  601. }
  602. }
  603. }
  604. return [];
  605. }
  606. function textNode(tag, className, text) {
  607. const node = document.createElement(tag);
  608. if (className) {
  609. node.className = className;
  610. }
  611. node.textContent = text || "";
  612. return node;
  613. }
  614. function money(value) {
  615. const text = String(value ?? "").trim();
  616. return text ? `¥${text}` : "";
  617. }
  618. function appendAction(container, href, text) {
  619. const url = normalizeImageUrl(href || "");
  620. if (!url) {
  621. return;
  622. }
  623. const link = document.createElement("a");
  624. link.href = url;
  625. link.target = "_blank";
  626. link.rel = "noreferrer";
  627. link.textContent = text;
  628. container.appendChild(link);
  629. }
  630. function createProductCard(product, index) {
  631. const card = document.createElement("div");
  632. card.className = "product-card";
  633. const pic = normalizeImageUrl(product.pic || product.imageUrl || product.pictUrl || product.itemPic || "");
  634. if (pic) {
  635. const image = document.createElement("img");
  636. image.src = pic;
  637. image.alt = product.itemName || product.title || `商品 ${index + 1}`;
  638. image.loading = "lazy";
  639. image.referrerPolicy = "no-referrer";
  640. card.appendChild(image);
  641. } else {
  642. card.appendChild(textNode("div", "product-image-empty", "无图"));
  643. }
  644. const info = document.createElement("div");
  645. info.className = "product-info";
  646. const nameText = product.itemName || product.title || product.goodsName || `商品 ${index + 1}`;
  647. const name = document.createElement(product.h5_url ? "a" : "div");
  648. name.className = "product-name";
  649. name.textContent = nameText;
  650. if (product.h5_url) {
  651. name.href = normalizeImageUrl(product.h5_url);
  652. name.target = "_blank";
  653. name.rel = "noreferrer";
  654. }
  655. info.appendChild(name);
  656. const prices = document.createElement("div");
  657. prices.className = "product-price";
  658. const sale = money(product.promotionPrice || product.zkFinalPrice || product.price);
  659. if (sale) {
  660. prices.appendChild(textNode("span", "sale-price", sale));
  661. }
  662. const origin = money(product.price);
  663. if (origin && origin !== sale) {
  664. prices.appendChild(textNode("span", "origin-price", origin));
  665. }
  666. const couponAmount = Number(product.couponAmount || 0);
  667. if (couponAmount > 0) {
  668. prices.appendChild(textNode("span", "coupon", `券 ${couponAmount}`));
  669. }
  670. info.appendChild(prices);
  671. const metaParts = [
  672. product.shopTitle,
  673. product.provcity,
  674. product.sellCountStr ? `销量 ${product.sellCountStr}` : "",
  675. product.itemId ? `ID ${product.itemId}` : ""
  676. ].filter(Boolean);
  677. if (metaParts.length) {
  678. info.appendChild(textNode("div", "product-meta", metaParts.join(" / ")));
  679. }
  680. const actions = document.createElement("div");
  681. actions.className = "product-actions";
  682. appendAction(actions, product.h5_url, "H5");
  683. appendAction(actions, product.deeplink_url, "Deeplink");
  684. appendAction(actions, product.clickTracks, "ClickTrack");
  685. if (actions.children.length) {
  686. info.appendChild(actions);
  687. }
  688. card.appendChild(info);
  689. return card;
  690. }
  691. function createResponseView(response) {
  692. const section = document.createElement("section");
  693. section.className = "response-view";
  694. const products = getProducts(response);
  695. const title = document.createElement("div");
  696. title.className = "response-title";
  697. title.append(
  698. textNode("strong", "", "响应商品"),
  699. textNode("span", "", `${products.length} 个商品`)
  700. );
  701. section.appendChild(title);
  702. if (!products.length) {
  703. const message = [
  704. response?.message,
  705. response?.reason,
  706. response?.code ? `code: ${response.code}` : ""
  707. ].filter(Boolean).join(" / ");
  708. section.appendChild(textNode("p", "response-message", message || "响应中没有商品列表"));
  709. return section;
  710. }
  711. const grid = document.createElement("div");
  712. grid.className = "product-grid";
  713. products.forEach((product, index) => grid.appendChild(createProductCard(product, index)));
  714. section.appendChild(grid);
  715. return section;
  716. }
  717. function createRawLog(content) {
  718. const details = document.createElement("details");
  719. details.className = "raw-log";
  720. const summary = document.createElement("summary");
  721. summary.textContent = "Raw 日志";
  722. const body = document.createElement("pre");
  723. body.className = "log-body";
  724. body.textContent = content || "";
  725. details.append(summary, body);
  726. return details;
  727. }
  728. function renderLogs(logs) {
  729. logsEl.replaceChildren();
  730. if (!logs.length) {
  731. const empty = document.createElement("div");
  732. empty.className = "empty";
  733. empty.textContent = "暂无日志";
  734. logsEl.appendChild(empty);
  735. return;
  736. }
  737. logs.forEach((item) => {
  738. const article = document.createElement("article");
  739. article.className = "log-item";
  740. const head = document.createElement("div");
  741. head.className = "log-head";
  742. const title = document.createElement("div");
  743. title.className = "log-title";
  744. const fileName = document.createElement("span");
  745. fileName.className = "file-name";
  746. fileName.textContent = item.fileName || "";
  747. const time = document.createElement("span");
  748. time.className = "time";
  749. time.textContent = item.writeTime || "";
  750. title.append(fileName, time);
  751. const imageInfo = getImageInfo(item.content || "");
  752. if (imageInfo) {
  753. const source = document.createElement("span");
  754. source.className = "source-pill";
  755. source.textContent = imageInfo.source;
  756. title.appendChild(source);
  757. }
  758. if (imageInfo?.href) {
  759. const link = document.createElement("a");
  760. link.className = "image-url";
  761. link.href = imageInfo.href;
  762. link.target = "_blank";
  763. link.rel = "noreferrer";
  764. link.textContent = imageInfo.label;
  765. title.appendChild(link);
  766. }
  767. const copyBtn = document.createElement("button");
  768. copyBtn.className = "secondary";
  769. copyBtn.type = "button";
  770. copyBtn.textContent = "复制 Raw";
  771. copyBtn.addEventListener("click", async () => {
  772. await navigator.clipboard.writeText(item.content || "");
  773. copyBtn.textContent = "已复制";
  774. setTimeout(() => copyBtn.textContent = "复制 Raw", 1200);
  775. });
  776. const preview = createImagePreview(imageInfo);
  777. const response = getResponseData(item.content || "");
  778. head.append(title, copyBtn);
  779. article.append(head);
  780. if (preview) {
  781. article.appendChild(preview);
  782. }
  783. article.appendChild(createResponseView(response));
  784. article.appendChild(createRawLog(item.content || ""));
  785. logsEl.appendChild(article);
  786. });
  787. }
  788. async function loadLogs(pageOverride) {
  789. const url = buildUrl(pageOverride);
  790. loadBtn.disabled = true;
  791. prevBtn.disabled = true;
  792. nextBtn.disabled = true;
  793. setStatus(`请求中:${url}`);
  794. try {
  795. const response = await fetch(url, { cache: "no-store" });
  796. if (!response.ok) {
  797. throw new Error(`HTTP ${response.status}`);
  798. }
  799. const data = await response.json();
  800. const logs = Array.isArray(data.logs) ? data.logs : [];
  801. const page = Math.max(Number(data.page || pageInput.value) || 1, 1);
  802. const pageSize = Math.min(Math.max(Number(data.pageSize || countInput.value) || 20, 1), 1100);
  803. const totalCount = Math.max(Number(data.totalCount || 0), 0);
  804. const totalPages = Math.max(Number(data.totalPages || 0), 0);
  805. countInput.value = String(pageSize);
  806. updatePager(page, totalPages, totalCount);
  807. renderLogs(logs);
  808. setStatus(
  809. totalCount > 0
  810. ? `已加载第 ${page} 页 ${logs.length} 条,共 ${totalCount} 条,${new Date().toLocaleString()}`
  811. : `暂无日志,${new Date().toLocaleString()}`
  812. );
  813. } catch (error) {
  814. updatePager(Math.max(Number(pageInput.value) || 1, 1), 0, 0);
  815. renderLogs([]);
  816. setStatus(`查询失败:${error.message}`, true);
  817. } finally {
  818. loadBtn.disabled = false;
  819. }
  820. }
  821. loadBtn.addEventListener("click", () => loadLogs());
  822. prevBtn.addEventListener("click", () => {
  823. if (currentPage > 1) {
  824. loadLogs(currentPage - 1);
  825. }
  826. });
  827. nextBtn.addEventListener("click", () => {
  828. if (currentTotalPages > 0 && currentPage < currentTotalPages) {
  829. loadLogs(currentPage + 1);
  830. }
  831. });
  832. copyUrlBtn.addEventListener("click", async () => {
  833. await navigator.clipboard.writeText(buildUrl());
  834. copyUrlBtn.textContent = "已复制";
  835. setTimeout(() => copyUrlBtn.textContent = "复制接口", 1200);
  836. });
  837. loadLogs();
  838. </script>
  839. </body>
  840. </html>