|
@@ -0,0 +1,887 @@
|
|
|
|
|
+<!doctype html>
|
|
|
|
|
+<html lang="zh-CN">
|
|
|
|
|
+<head>
|
|
|
|
|
+ <meta charset="utf-8">
|
|
|
|
|
+ <meta name="viewport" content="width=device-width, initial-scale=1">
|
|
|
|
|
+ <title>PromotionQuery Logs</title>
|
|
|
|
|
+ <style>
|
|
|
|
|
+ :root {
|
|
|
|
|
+ color-scheme: light;
|
|
|
|
|
+ --bg: #f6f7f9;
|
|
|
|
|
+ --panel: #ffffff;
|
|
|
|
|
+ --text: #1f2328;
|
|
|
|
|
+ --muted: #68707d;
|
|
|
|
|
+ --border: #d8dee4;
|
|
|
|
|
+ --primary: #0969da;
|
|
|
|
|
+ --primary-hover: #0759b8;
|
|
|
|
|
+ --danger: #b42318;
|
|
|
|
|
+ --code-bg: #f0f3f6;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ * {
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ body {
|
|
|
|
|
+ margin: 0;
|
|
|
|
|
+ background: var(--bg);
|
|
|
|
|
+ color: var(--text);
|
|
|
|
|
+ font: 14px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ main {
|
|
|
|
|
+ max-width: 1180px;
|
|
|
|
|
+ margin: 0 auto;
|
|
|
|
|
+ padding: 24px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ h1 {
|
|
|
|
|
+ margin: 0 0 16px;
|
|
|
|
|
+ font-size: 22px;
|
|
|
|
|
+ font-weight: 650;
|
|
|
|
|
+ letter-spacing: 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .toolbar {
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+ grid-template-columns: minmax(260px, 1fr) 120px auto auto;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+ align-items: end;
|
|
|
|
|
+ margin-bottom: 14px;
|
|
|
|
|
+ padding: 14px;
|
|
|
|
|
+ background: var(--panel);
|
|
|
|
|
+ border: 1px solid var(--border);
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ label {
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+ gap: 5px;
|
|
|
|
|
+ color: var(--muted);
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ input {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 36px;
|
|
|
|
|
+ padding: 7px 9px;
|
|
|
|
|
+ border: 1px solid var(--border);
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ color: var(--text);
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ font: inherit;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ button {
|
|
|
|
|
+ height: 36px;
|
|
|
|
|
+ padding: 0 14px;
|
|
|
|
|
+ border: 1px solid var(--primary);
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ background: var(--primary);
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+ font: inherit;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ button:hover {
|
|
|
|
|
+ background: var(--primary-hover);
|
|
|
|
|
+ border-color: var(--primary-hover);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ button.secondary {
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ color: var(--primary);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ button.secondary:hover {
|
|
|
|
|
+ background: #f1f6fd;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .status {
|
|
|
|
|
+ margin: 0 0 14px;
|
|
|
|
|
+ color: var(--muted);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .status.error {
|
|
|
|
|
+ color: var(--danger);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .logs {
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .log-item {
|
|
|
|
|
+ background: var(--panel);
|
|
|
|
|
+ border: 1px solid var(--border);
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .log-head {
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+ grid-template-columns: minmax(0, 1fr) auto;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ padding: 12px 14px;
|
|
|
|
|
+ border-bottom: 1px solid var(--border);
|
|
|
|
|
+ background: #fbfcfd;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .log-title {
|
|
|
|
|
+ min-width: 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .file-name {
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ text-overflow: ellipsis;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+ font-weight: 650;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .time {
|
|
|
|
|
+ color: var(--muted);
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .image-url {
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ margin-top: 4px;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ color: var(--primary);
|
|
|
|
|
+ text-overflow: ellipsis;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+ text-decoration: none;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .source-pill {
|
|
|
|
|
+ display: inline-block;
|
|
|
|
|
+ margin-top: 6px;
|
|
|
|
|
+ padding: 2px 7px;
|
|
|
|
|
+ border: 1px solid var(--border);
|
|
|
|
|
+ border-radius: 999px;
|
|
|
|
|
+ color: var(--muted);
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .image-preview {
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+ grid-template-columns: 112px minmax(0, 1fr);
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ padding: 12px 14px;
|
|
|
|
|
+ border-bottom: 1px solid var(--border);
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .image-preview img {
|
|
|
|
|
+ width: 112px;
|
|
|
|
|
+ height: 112px;
|
|
|
|
|
+ object-fit: contain;
|
|
|
|
|
+ border: 1px solid var(--border);
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ background: #f8fafc;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .image-meta {
|
|
|
|
|
+ min-width: 0;
|
|
|
|
|
+ color: var(--muted);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .image-meta strong {
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ margin-bottom: 4px;
|
|
|
|
|
+ color: var(--text);
|
|
|
|
|
+ font-weight: 650;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .response-view {
|
|
|
|
|
+ padding: 14px;
|
|
|
|
|
+ border-bottom: 1px solid var(--border);
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .response-title {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+ align-items: baseline;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ margin-bottom: 12px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .response-title strong {
|
|
|
|
|
+ font-size: 15px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .response-title span {
|
|
|
|
|
+ color: var(--muted);
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .response-message {
|
|
|
|
|
+ margin: 0;
|
|
|
|
|
+ padding: 10px 12px;
|
|
|
|
|
+ color: var(--muted);
|
|
|
|
|
+ background: #f8fafc;
|
|
|
|
|
+ border: 1px solid var(--border);
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .product-grid {
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+ grid-template-columns: repeat(auto-fill, minmax(285px, 1fr));
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .product-card {
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+ grid-template-columns: 92px minmax(0, 1fr);
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+ min-width: 0;
|
|
|
|
|
+ padding: 10px;
|
|
|
|
|
+ border: 1px solid var(--border);
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .product-card img,
|
|
|
|
|
+ .product-image-empty {
|
|
|
|
|
+ width: 92px;
|
|
|
|
|
+ height: 92px;
|
|
|
|
|
+ object-fit: contain;
|
|
|
|
|
+ border: 1px solid var(--border);
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ background: #f8fafc;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .product-image-empty {
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+ place-items: center;
|
|
|
|
|
+ color: var(--muted);
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .product-info {
|
|
|
|
|
+ min-width: 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .product-name {
|
|
|
|
|
+ display: -webkit-box;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ color: var(--text);
|
|
|
|
|
+ font-weight: 650;
|
|
|
|
|
+ line-height: 1.4;
|
|
|
|
|
+ text-decoration: none;
|
|
|
|
|
+ -webkit-line-clamp: 2;
|
|
|
|
|
+ -webkit-box-orient: vertical;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .product-name:hover {
|
|
|
|
|
+ color: var(--primary);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .product-price {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-wrap: wrap;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+ align-items: baseline;
|
|
|
|
|
+ margin-top: 7px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .sale-price {
|
|
|
|
|
+ color: var(--danger);
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .origin-price {
|
|
|
|
|
+ color: var(--muted);
|
|
|
|
|
+ text-decoration: line-through;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .coupon {
|
|
|
|
|
+ padding: 1px 6px;
|
|
|
|
|
+ color: var(--danger);
|
|
|
|
|
+ border: 1px solid #f0b8b2;
|
|
|
|
|
+ border-radius: 999px;
|
|
|
|
|
+ background: #fff5f3;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .product-meta {
|
|
|
|
|
+ margin-top: 6px;
|
|
|
|
|
+ color: var(--muted);
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .product-actions {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-wrap: wrap;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+ margin-top: 8px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .product-actions a {
|
|
|
|
|
+ padding: 3px 8px;
|
|
|
|
|
+ color: var(--primary);
|
|
|
|
|
+ border: 1px solid var(--border);
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ text-decoration: none;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .product-actions a:hover {
|
|
|
|
|
+ background: #f1f6fd;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .raw-log {
|
|
|
|
|
+ border-bottom: 1px solid var(--border);
|
|
|
|
|
+ background: var(--code-bg);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .raw-log summary {
|
|
|
|
|
+ padding: 10px 14px;
|
|
|
|
|
+ color: var(--muted);
|
|
|
|
|
+ background: #fbfcfd;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ user-select: none;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .log-body {
|
|
|
|
|
+ margin: 0;
|
|
|
|
|
+ max-height: 520px;
|
|
|
|
|
+ overflow: auto;
|
|
|
|
|
+ padding: 14px;
|
|
|
|
|
+ background: var(--code-bg);
|
|
|
|
|
+ font: 12px/1.55 "SFMono-Regular", Consolas, "Liberation Mono", monospace;
|
|
|
|
|
+ white-space: pre-wrap;
|
|
|
|
|
+ word-break: break-word;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .empty {
|
|
|
|
|
+ padding: 32px 18px;
|
|
|
|
|
+ color: var(--muted);
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ background: var(--panel);
|
|
|
|
|
+ border: 1px solid var(--border);
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @media (max-width: 760px) {
|
|
|
|
|
+ main {
|
|
|
|
|
+ padding: 14px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .toolbar {
|
|
|
|
|
+ grid-template-columns: 1fr;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .log-head {
|
|
|
|
|
+ grid-template-columns: 1fr;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .image-preview {
|
|
|
|
|
+ grid-template-columns: 84px minmax(0, 1fr);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .image-preview img {
|
|
|
|
|
+ width: 84px;
|
|
|
|
|
+ height: 84px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .product-grid {
|
|
|
|
|
+ grid-template-columns: 1fr;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ </style>
|
|
|
|
|
+</head>
|
|
|
|
|
+<body>
|
|
|
|
|
+ <main>
|
|
|
|
|
+ <h1>PromotionQuery 日志</h1>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="toolbar">
|
|
|
|
|
+ <label>
|
|
|
|
|
+ API 地址
|
|
|
|
|
+ <input id="apiBase" autocomplete="off">
|
|
|
|
|
+ </label>
|
|
|
|
|
+ <label>
|
|
|
|
|
+ 条数
|
|
|
|
|
+ <input id="count" type="number" min="1" max="100" value="20">
|
|
|
|
|
+ </label>
|
|
|
|
|
+ <button id="loadBtn" type="button">查询</button>
|
|
|
|
|
+ <button id="copyUrlBtn" class="secondary" type="button">复制接口</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <p id="status" class="status"></p>
|
|
|
|
|
+ <section id="logs" class="logs"></section>
|
|
|
|
|
+ </main>
|
|
|
|
|
+
|
|
|
|
|
+ <script>
|
|
|
|
|
+ const endpointPath = "/api/PromotionQuerySampleLogs";
|
|
|
|
|
+ const apiBaseInput = document.getElementById("apiBase");
|
|
|
|
|
+ const countInput = document.getElementById("count");
|
|
|
|
|
+ const loadBtn = document.getElementById("loadBtn");
|
|
|
|
|
+ const copyUrlBtn = document.getElementById("copyUrlBtn");
|
|
|
|
|
+ const statusEl = document.getElementById("status");
|
|
|
|
|
+ const logsEl = document.getElementById("logs");
|
|
|
|
|
+
|
|
|
|
|
+ apiBaseInput.value = "http://similar.molilian.com:5005";
|
|
|
|
|
+
|
|
|
|
|
+ function buildUrl() {
|
|
|
|
|
+ const base = apiBaseInput.value.replace(/\/+$/, "");
|
|
|
|
|
+ const count = Math.min(Math.max(Number(countInput.value) || 20, 1), 100);
|
|
|
|
|
+ countInput.value = String(count);
|
|
|
|
|
+ return `${base}${endpointPath}?n=${encodeURIComponent(count)}`;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function setStatus(text, isError = false) {
|
|
|
|
|
+ statusEl.textContent = text;
|
|
|
|
|
+ statusEl.classList.toggle("error", isError);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function getLogSection(content, name) {
|
|
|
|
|
+ const marker = new RegExp(`\\t${name}\\r?\\n`);
|
|
|
|
|
+ const match = marker.exec(content);
|
|
|
|
|
+ if (!match) {
|
|
|
|
|
+ return "";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const start = match.index + match[0].length;
|
|
|
|
|
+ const rest = content.slice(start);
|
|
|
|
|
+ const next = rest.search(/\r?\n\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}\t/);
|
|
|
|
|
+ return (next >= 0 ? rest.slice(0, next) : rest).trim();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function normalizeImageUrl(value) {
|
|
|
|
|
+ const text = (value || "").trim();
|
|
|
|
|
+ if (text.startsWith("//")) {
|
|
|
|
|
+ return `https:${text}`;
|
|
|
|
|
+ }
|
|
|
|
|
+ return text;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function detectImageMime(base64) {
|
|
|
|
|
+ if (base64.startsWith("/9j/")) {
|
|
|
|
|
+ return "image/jpeg";
|
|
|
|
|
+ }
|
|
|
|
|
+ if (base64.startsWith("iVBOR")) {
|
|
|
|
|
+ return "image/png";
|
|
|
|
|
+ }
|
|
|
|
|
+ if (base64.startsWith("R0lGOD")) {
|
|
|
|
|
+ return "image/gif";
|
|
|
|
|
+ }
|
|
|
|
|
+ if (base64.startsWith("UklGR")) {
|
|
|
|
|
+ return "image/webp";
|
|
|
|
|
+ }
|
|
|
|
|
+ return "image/jpeg";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function buildImageSrcFromImg(value) {
|
|
|
|
|
+ const text = (value || "").trim();
|
|
|
|
|
+ if (!text) {
|
|
|
|
|
+ return "";
|
|
|
|
|
+ }
|
|
|
|
|
+ if (/^data:image\//i.test(text)) {
|
|
|
|
|
+ return text;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (/^https?:\/\//i.test(text) || text.startsWith("//")) {
|
|
|
|
|
+ return normalizeImageUrl(text);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const base64 = text.replace(/\s/g, "");
|
|
|
|
|
+ if (base64.length < 32 || !/^[A-Za-z0-9+/=]+$/.test(base64)) {
|
|
|
|
|
+ return "";
|
|
|
|
|
+ }
|
|
|
|
|
+ return `data:${detectImageMime(base64)};base64,${base64}`;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function getClientPost(content) {
|
|
|
|
|
+ const text = getLogSection(content, "clientPost");
|
|
|
|
|
+ if (!text) {
|
|
|
|
|
+ return {};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ return JSON.parse(text);
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ return {};
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function getImageInfo(content) {
|
|
|
|
|
+ const loggedUrl = normalizeImageUrl(getLogSection(content, "imageUrl"));
|
|
|
|
|
+ if (loggedUrl) {
|
|
|
|
|
+ return {
|
|
|
|
|
+ source: "url参数",
|
|
|
|
|
+ src: loggedUrl,
|
|
|
|
|
+ href: loggedUrl,
|
|
|
|
|
+ label: loggedUrl
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const post = getClientPost(content);
|
|
|
|
|
+ const requestUrl = normalizeImageUrl(post.url || post.pic || post.imageUrl || post.pictureUrl || "");
|
|
|
|
|
+ if (requestUrl) {
|
|
|
|
|
+ return {
|
|
|
|
|
+ source: "url参数",
|
|
|
|
|
+ src: requestUrl,
|
|
|
|
|
+ href: requestUrl,
|
|
|
|
|
+ label: requestUrl
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const imgValue = post.img || post.image || "";
|
|
|
|
|
+ const imgSrc = buildImageSrcFromImg(imgValue);
|
|
|
|
|
+ if (!imgSrc) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const isUrl = /^https?:\/\//i.test(imgSrc);
|
|
|
|
|
+ return {
|
|
|
|
|
+ source: isUrl ? "img参数URL" : "img参数base64",
|
|
|
|
|
+ src: imgSrc,
|
|
|
|
|
+ href: isUrl ? imgSrc : "",
|
|
|
|
|
+ label: isUrl ? imgSrc : `base64 图片,约 ${Math.round(String(imgValue).length / 1024)} KB`
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function createImagePreview(imageInfo) {
|
|
|
|
|
+ if (!imageInfo) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const preview = document.createElement("div");
|
|
|
|
|
+ preview.className = "image-preview";
|
|
|
|
|
+
|
|
|
|
|
+ const img = document.createElement("img");
|
|
|
|
|
+ img.src = imageInfo.src;
|
|
|
|
|
+ img.alt = imageInfo.source;
|
|
|
|
|
+ img.loading = "lazy";
|
|
|
|
|
+ img.referrerPolicy = "no-referrer";
|
|
|
|
|
+
|
|
|
|
|
+ const meta = document.createElement("div");
|
|
|
|
|
+ meta.className = "image-meta";
|
|
|
|
|
+
|
|
|
|
|
+ const title = document.createElement("strong");
|
|
|
|
|
+ title.textContent = imageInfo.source;
|
|
|
|
|
+ meta.appendChild(title);
|
|
|
|
|
+
|
|
|
|
|
+ if (imageInfo.href) {
|
|
|
|
|
+ const link = document.createElement("a");
|
|
|
|
|
+ link.className = "image-url";
|
|
|
|
|
+ link.href = imageInfo.href;
|
|
|
|
|
+ link.target = "_blank";
|
|
|
|
|
+ link.rel = "noreferrer";
|
|
|
|
|
+ link.textContent = imageInfo.label;
|
|
|
|
|
+ meta.appendChild(link);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ const text = document.createElement("span");
|
|
|
|
|
+ text.textContent = imageInfo.label;
|
|
|
|
|
+ meta.appendChild(text);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ preview.append(img, meta);
|
|
|
|
|
+ return preview;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function parseJson(text) {
|
|
|
|
|
+ if (!text) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ return JSON.parse(text);
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function getResponseData(content) {
|
|
|
|
|
+ return parseJson(getLogSection(content, "responseJson"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function getProducts(response) {
|
|
|
|
|
+ if (Array.isArray(response)) {
|
|
|
|
|
+ return response.filter((item) => item && typeof item === "object");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!response || typeof response !== "object") {
|
|
|
|
|
+ return [];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const containers = [
|
|
|
|
|
+ response,
|
|
|
|
|
+ response.data,
|
|
|
|
|
+ response.result,
|
|
|
|
|
+ response.response
|
|
|
|
|
+ ].filter((item) => item && typeof item === "object");
|
|
|
|
|
+
|
|
|
|
|
+ const keys = ["promotionImg", "PromotionImg", "items", "Items", "list", "List"];
|
|
|
|
|
+ for (const container of containers) {
|
|
|
|
|
+ if (Array.isArray(container)) {
|
|
|
|
|
+ return container.filter((item) => item && typeof item === "object");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ for (const key of keys) {
|
|
|
|
|
+ if (Array.isArray(container[key])) {
|
|
|
|
|
+ return container[key].filter((item) => item && typeof item === "object");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return [];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function textNode(tag, className, text) {
|
|
|
|
|
+ const node = document.createElement(tag);
|
|
|
|
|
+ if (className) {
|
|
|
|
|
+ node.className = className;
|
|
|
|
|
+ }
|
|
|
|
|
+ node.textContent = text || "";
|
|
|
|
|
+ return node;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function money(value) {
|
|
|
|
|
+ const text = String(value ?? "").trim();
|
|
|
|
|
+ return text ? `¥${text}` : "";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function appendAction(container, href, text) {
|
|
|
|
|
+ const url = normalizeImageUrl(href || "");
|
|
|
|
|
+ if (!url) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const link = document.createElement("a");
|
|
|
|
|
+ link.href = url;
|
|
|
|
|
+ link.target = "_blank";
|
|
|
|
|
+ link.rel = "noreferrer";
|
|
|
|
|
+ link.textContent = text;
|
|
|
|
|
+ container.appendChild(link);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function createProductCard(product, index) {
|
|
|
|
|
+ const card = document.createElement("div");
|
|
|
|
|
+ card.className = "product-card";
|
|
|
|
|
+
|
|
|
|
|
+ const pic = normalizeImageUrl(product.pic || product.imageUrl || product.pictUrl || product.itemPic || "");
|
|
|
|
|
+ if (pic) {
|
|
|
|
|
+ const image = document.createElement("img");
|
|
|
|
|
+ image.src = pic;
|
|
|
|
|
+ image.alt = product.itemName || product.title || `商品 ${index + 1}`;
|
|
|
|
|
+ image.loading = "lazy";
|
|
|
|
|
+ image.referrerPolicy = "no-referrer";
|
|
|
|
|
+ card.appendChild(image);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ card.appendChild(textNode("div", "product-image-empty", "无图"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const info = document.createElement("div");
|
|
|
|
|
+ info.className = "product-info";
|
|
|
|
|
+
|
|
|
|
|
+ const nameText = product.itemName || product.title || product.goodsName || `商品 ${index + 1}`;
|
|
|
|
|
+ const name = document.createElement(product.h5_url ? "a" : "div");
|
|
|
|
|
+ name.className = "product-name";
|
|
|
|
|
+ name.textContent = nameText;
|
|
|
|
|
+ if (product.h5_url) {
|
|
|
|
|
+ name.href = normalizeImageUrl(product.h5_url);
|
|
|
|
|
+ name.target = "_blank";
|
|
|
|
|
+ name.rel = "noreferrer";
|
|
|
|
|
+ }
|
|
|
|
|
+ info.appendChild(name);
|
|
|
|
|
+
|
|
|
|
|
+ const prices = document.createElement("div");
|
|
|
|
|
+ prices.className = "product-price";
|
|
|
|
|
+ const sale = money(product.promotionPrice || product.zkFinalPrice || product.price);
|
|
|
|
|
+ if (sale) {
|
|
|
|
|
+ prices.appendChild(textNode("span", "sale-price", sale));
|
|
|
|
|
+ }
|
|
|
|
|
+ const origin = money(product.price);
|
|
|
|
|
+ if (origin && origin !== sale) {
|
|
|
|
|
+ prices.appendChild(textNode("span", "origin-price", origin));
|
|
|
|
|
+ }
|
|
|
|
|
+ const couponAmount = Number(product.couponAmount || 0);
|
|
|
|
|
+ if (couponAmount > 0) {
|
|
|
|
|
+ prices.appendChild(textNode("span", "coupon", `券 ${couponAmount}`));
|
|
|
|
|
+ }
|
|
|
|
|
+ info.appendChild(prices);
|
|
|
|
|
+
|
|
|
|
|
+ const metaParts = [
|
|
|
|
|
+ product.shopTitle,
|
|
|
|
|
+ product.provcity,
|
|
|
|
|
+ product.sellCountStr ? `销量 ${product.sellCountStr}` : "",
|
|
|
|
|
+ product.itemId ? `ID ${product.itemId}` : ""
|
|
|
|
|
+ ].filter(Boolean);
|
|
|
|
|
+ if (metaParts.length) {
|
|
|
|
|
+ info.appendChild(textNode("div", "product-meta", metaParts.join(" / ")));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const actions = document.createElement("div");
|
|
|
|
|
+ actions.className = "product-actions";
|
|
|
|
|
+ appendAction(actions, product.h5_url, "H5");
|
|
|
|
|
+ appendAction(actions, product.deeplink_url, "Deeplink");
|
|
|
|
|
+ appendAction(actions, product.clickTracks, "ClickTrack");
|
|
|
|
|
+ if (actions.children.length) {
|
|
|
|
|
+ info.appendChild(actions);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ card.appendChild(info);
|
|
|
|
|
+ return card;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function createResponseView(response) {
|
|
|
|
|
+ const section = document.createElement("section");
|
|
|
|
|
+ section.className = "response-view";
|
|
|
|
|
+
|
|
|
|
|
+ const products = getProducts(response);
|
|
|
|
|
+ const title = document.createElement("div");
|
|
|
|
|
+ title.className = "response-title";
|
|
|
|
|
+ title.append(
|
|
|
|
|
+ textNode("strong", "", "响应商品"),
|
|
|
|
|
+ textNode("span", "", `${products.length} 个商品`)
|
|
|
|
|
+ );
|
|
|
|
|
+ section.appendChild(title);
|
|
|
|
|
+
|
|
|
|
|
+ if (!products.length) {
|
|
|
|
|
+ const message = [
|
|
|
|
|
+ response?.message,
|
|
|
|
|
+ response?.reason,
|
|
|
|
|
+ response?.code ? `code: ${response.code}` : ""
|
|
|
|
|
+ ].filter(Boolean).join(" / ");
|
|
|
|
|
+ section.appendChild(textNode("p", "response-message", message || "响应中没有商品列表"));
|
|
|
|
|
+ return section;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const grid = document.createElement("div");
|
|
|
|
|
+ grid.className = "product-grid";
|
|
|
|
|
+ products.forEach((product, index) => grid.appendChild(createProductCard(product, index)));
|
|
|
|
|
+ section.appendChild(grid);
|
|
|
|
|
+ return section;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function createRawLog(content) {
|
|
|
|
|
+ const details = document.createElement("details");
|
|
|
|
|
+ details.className = "raw-log";
|
|
|
|
|
+
|
|
|
|
|
+ const summary = document.createElement("summary");
|
|
|
|
|
+ summary.textContent = "Raw 日志";
|
|
|
|
|
+
|
|
|
|
|
+ const body = document.createElement("pre");
|
|
|
|
|
+ body.className = "log-body";
|
|
|
|
|
+ body.textContent = content || "";
|
|
|
|
|
+
|
|
|
|
|
+ details.append(summary, body);
|
|
|
|
|
+ return details;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function renderLogs(logs) {
|
|
|
|
|
+ logsEl.replaceChildren();
|
|
|
|
|
+
|
|
|
|
|
+ if (!logs.length) {
|
|
|
|
|
+ const empty = document.createElement("div");
|
|
|
|
|
+ empty.className = "empty";
|
|
|
|
|
+ empty.textContent = "暂无日志";
|
|
|
|
|
+ logsEl.appendChild(empty);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ logs.forEach((item) => {
|
|
|
|
|
+ const article = document.createElement("article");
|
|
|
|
|
+ article.className = "log-item";
|
|
|
|
|
+
|
|
|
|
|
+ const head = document.createElement("div");
|
|
|
|
|
+ head.className = "log-head";
|
|
|
|
|
+
|
|
|
|
|
+ const title = document.createElement("div");
|
|
|
|
|
+ title.className = "log-title";
|
|
|
|
|
+
|
|
|
|
|
+ const fileName = document.createElement("span");
|
|
|
|
|
+ fileName.className = "file-name";
|
|
|
|
|
+ fileName.textContent = item.fileName || "";
|
|
|
|
|
+
|
|
|
|
|
+ const time = document.createElement("span");
|
|
|
|
|
+ time.className = "time";
|
|
|
|
|
+ time.textContent = item.writeTime || "";
|
|
|
|
|
+
|
|
|
|
|
+ title.append(fileName, time);
|
|
|
|
|
+
|
|
|
|
|
+ const imageInfo = getImageInfo(item.content || "");
|
|
|
|
|
+ if (imageInfo) {
|
|
|
|
|
+ const source = document.createElement("span");
|
|
|
|
|
+ source.className = "source-pill";
|
|
|
|
|
+ source.textContent = imageInfo.source;
|
|
|
|
|
+ title.appendChild(source);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (imageInfo?.href) {
|
|
|
|
|
+ const link = document.createElement("a");
|
|
|
|
|
+ link.className = "image-url";
|
|
|
|
|
+ link.href = imageInfo.href;
|
|
|
|
|
+ link.target = "_blank";
|
|
|
|
|
+ link.rel = "noreferrer";
|
|
|
|
|
+ link.textContent = imageInfo.label;
|
|
|
|
|
+ title.appendChild(link);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const copyBtn = document.createElement("button");
|
|
|
|
|
+ copyBtn.className = "secondary";
|
|
|
|
|
+ copyBtn.type = "button";
|
|
|
|
|
+ copyBtn.textContent = "复制 Raw";
|
|
|
|
|
+ copyBtn.addEventListener("click", async () => {
|
|
|
|
|
+ await navigator.clipboard.writeText(item.content || "");
|
|
|
|
|
+ copyBtn.textContent = "已复制";
|
|
|
|
|
+ setTimeout(() => copyBtn.textContent = "复制 Raw", 1200);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const preview = createImagePreview(imageInfo);
|
|
|
|
|
+ const response = getResponseData(item.content || "");
|
|
|
|
|
+ head.append(title, copyBtn);
|
|
|
|
|
+ article.append(head);
|
|
|
|
|
+ if (preview) {
|
|
|
|
|
+ article.appendChild(preview);
|
|
|
|
|
+ }
|
|
|
|
|
+ article.appendChild(createResponseView(response));
|
|
|
|
|
+ article.appendChild(createRawLog(item.content || ""));
|
|
|
|
|
+ logsEl.appendChild(article);
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async function loadLogs() {
|
|
|
|
|
+ const url = buildUrl();
|
|
|
|
|
+ loadBtn.disabled = true;
|
|
|
|
|
+ setStatus(`请求中:${url}`);
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const response = await fetch(url, { cache: "no-store" });
|
|
|
|
|
+ if (!response.ok) {
|
|
|
|
|
+ throw new Error(`HTTP ${response.status}`);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const data = await response.json();
|
|
|
|
|
+ const logs = Array.isArray(data.logs) ? data.logs : [];
|
|
|
|
|
+ renderLogs(logs);
|
|
|
|
|
+ setStatus(`已加载 ${logs.length} 条,${new Date().toLocaleString()}`);
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ renderLogs([]);
|
|
|
|
|
+ setStatus(`查询失败:${error.message}`, true);
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ loadBtn.disabled = false;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ loadBtn.addEventListener("click", loadLogs);
|
|
|
|
|
+ copyUrlBtn.addEventListener("click", async () => {
|
|
|
|
|
+ await navigator.clipboard.writeText(buildUrl());
|
|
|
|
|
+ copyUrlBtn.textContent = "已复制";
|
|
|
|
|
+ setTimeout(() => copyUrlBtn.textContent = "复制接口", 1200);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ loadLogs();
|
|
|
|
|
+ </script>
|
|
|
|
|
+</body>
|
|
|
|
|
+</html>
|