promotion-query-logs.html 22 KB

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