| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887 |
- <!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>
|