// ==UserScript== // @name 链动小铺商家增强工具 // @namespace https://www.ldxp.cn/ // @version 1.1.5 // @description 货源广场增强搜索与一键对接;支持上架和库存筛选,商品管理可批量修改分类、价格、状态并复制文字报表。 // @author miku1130 // @license MIT // @homepageURL https://github.com/miku1130/ldxp-merchant-toolkit // @supportURL https://github.com/miku1130/ldxp-merchant-toolkit/issues // @match https://www.ldxp.cn/merchant/* // @grant none // @run-at document-idle // ==/UserScript== (function () { "use strict"; const APP_ID = "ldxp-merchant-toolkit"; const STYLE_ID = `${APP_ID}-style`; const SOURCE_PATH = "/merchant/my_parent/source_square"; const GOODS_PATH = "/merchant/goods/list"; const API = { sourceSearch: "/merchantApi/MyParent/searchGoodsList", sourceConnect: "/merchantApi/MyParent/fetchConnectGoods", goodsList: "/merchantApi/Goods/list", goodsInfo: "/merchantApi/Goods/info", goodsUpdate: "/merchantApi/Goods/update", goodsStatus: "/merchantApi/Goods/statusUpdate", categoryList: "/merchantApi/GoodsCategory/listAll", goodsLink: "/merchantApi/Goods/getLink", }; const GOODS_TYPES = { card: "卡密", article: "知识", resource: "资源", equity: "权益", }; const sourceState = { raw: [], filtered: [], selected: new Set(), loading: false, }; const goodsState = { rawItems: [], items: [], selected: new Set(), categories: [], infoById: new Map(), loading: false, goodsType: "card", }; function getToken() { try { const raw = localStorage.getItem("auth-token"); if (!raw) return ""; try { const parsed = JSON.parse(raw); return parsed?.value || parsed?.token || ""; } catch (_) { return raw; } } catch (_) { return ""; } } async function postJson(url, body) { if (!url.startsWith("/merchantApi/")) { throw new Error("已阻止非站内接口请求"); } const token = getToken(); const response = await fetch(url, { method: "POST", credentials: "include", headers: { "Content-Type": "application/json", Accept: "application/json, text/plain, */*", ...(token ? { "Merchant-Token": token } : {}), }, body: JSON.stringify(body || {}), }); if (!response.ok) throw new Error(`HTTP ${response.status}`); const result = await response.json(); if (result.code !== 1) throw new Error(result.msg || "接口请求失败"); return result.data; } function escapeHtml(value) { return String(value ?? "") .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } function toNumber(value) { if (value === "" || value === null || value === undefined) return null; const number = Number(String(value).replace(/[^\d.-]/g, "")); return Number.isFinite(number) ? number : null; } function money(value) { const number = toNumber(value); return number === null ? "-" : `¥${number.toFixed(2)}`; } function listedState(item) { const value = item.is_listed ?? item.is_on_sale ?? item.is_on_shelf ?? item.on_sale ?? item.on_shelf ?? item.status; if (value === true || value === 1 || value === "1") return true; if (value === false || value === 0 || value === "0") return false; const normalized = String(value ?? "").trim().toLowerCase(); if (["listed", "on_sale", "on_shelf", "sale", "selling", "active", "已上架", "上架", "销售中"].includes(normalized)) return true; if (["unlisted", "off_sale", "off_shelf", "warehouse", "inactive", "已下架", "下架", "仓库中"].includes(normalized)) return false; return null; } function listedLabel(item) { const state = listedState(item); if (state === true) return { text: "已上架", className: "ok" }; if (state === false) return { text: "未上架", className: "warn" }; return { text: "未知", className: "warn" }; } function safeUrl(value) { try { const url = new URL(String(value || ""), location.origin); if (url.protocol !== "https:" && url.protocol !== "http:") return "#"; return url.href; } catch (_) { return "#"; } } function notify(message, type = "info") { let toast = document.getElementById(`${APP_ID}-toast`); if (!toast) { toast = document.createElement("div"); toast.id = `${APP_ID}-toast`; document.body.appendChild(toast); } toast.className = `ldxp-toolkit-toast ${type}`; toast.textContent = message; toast.hidden = false; clearTimeout(notify.timer); notify.timer = setTimeout(() => { toast.hidden = true; }, 3500); } function setBusy(button, busy, text = "处理中...") { if (!(button instanceof HTMLButtonElement)) return; if (busy) { button.dataset.oldText = button.textContent || ""; button.textContent = text; button.disabled = true; } else { button.textContent = button.dataset.oldText || button.textContent; button.disabled = false; } } function installStyles() { if (document.getElementById(STYLE_ID)) return; const style = document.createElement("style"); style.id = STYLE_ID; style.textContent = ` .ldxp-toolkit-launcher { position: fixed; right: 22px; bottom: 24px; z-index: 2147483646; height: 42px; padding: 0 18px; border: 0; border-radius: 22px; color: #fff; background: #165dff; box-shadow: 0 10px 28px rgba(22,93,255,.3); font-size: 14px; font-weight: 700; cursor: pointer; } .ldxp-toolkit-launcher.goto { bottom: 76px; background: #344054; box-shadow: 0 10px 28px rgba(52,64,84,.28); } .ldxp-toolkit-panel { position: fixed; top: 76px; right: 22px; z-index: 2147483647; width: min(1120px, calc(100vw - 44px)); height: min(760px, calc(100vh - 100px)); min-height: min(420px, calc(100vh - 100px)); max-height: calc(100vh - 100px); display: flex; flex-direction: column; overflow: hidden; resize: both; color: #1d2939; background: #fff; border: 1px solid #d0d5dd; border-radius: 10px; box-shadow: 0 20px 60px rgba(15,23,42,.24); font: 14px/1.45 Arial,"Microsoft YaHei",sans-serif; } .ldxp-toolkit-panel[hidden] { display: none !important; } .ldxp-toolkit-panel > [data-panel-body] { flex: 1; min-height: 0; display: flex; flex-direction: column; overflow: hidden; } .ldxp-toolkit-head { display: flex; align-items: center; gap: 12px; padding: 12px 14px; flex: 0 0 auto; background: #f8fafc; border-bottom: 1px solid #eaecf0; user-select: none; } .ldxp-toolkit-title { font-size: 16px; font-weight: 700; white-space: nowrap; } .ldxp-toolkit-contact { color: #475467; font-size: 12px; white-space: nowrap; } .ldxp-toolkit-contact strong { color: #165dff; } .ldxp-toolkit-status { flex: 1; min-width: 0; color: #667085; font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .ldxp-toolkit-controls { display: grid; grid-template-columns: repeat(6,minmax(125px,1fr)); gap: 10px; flex: 0 0 auto; max-height: 38vh; overflow-y: auto; padding: 12px 14px; border-bottom: 1px solid #eaecf0; } .ldxp-toolkit-controls label { display: flex; flex-direction: column; gap: 4px; color: #667085; font-size: 12px; } .ldxp-toolkit-controls input, .ldxp-toolkit-controls select, .ldxp-toolkit-report textarea { box-sizing: border-box; width: 100%; min-height: 34px; padding: 6px 9px; color: #1d2939; background: #fff; border: 1px solid #d0d5dd; border-radius: 6px; outline: none; } .ldxp-toolkit-actions { display: flex; align-items: end; gap: 8px; flex-wrap: wrap; } .ldxp-toolkit-panel button { min-height: 32px; padding: 0 12px; color: #344054; background: #fff; border: 1px solid #d0d5dd; border-radius: 6px; cursor: pointer; } .ldxp-toolkit-panel button.primary { color: #fff; border-color: #165dff; background: #165dff; } .ldxp-toolkit-panel button.success { color: #fff; border-color: #079455; background: #079455; } .ldxp-toolkit-panel button.danger { color: #b42318; border-color: #fda29b; background: #fff; } .ldxp-toolkit-panel button:disabled { opacity: .55; cursor: not-allowed; } .ldxp-toolkit-table-wrap { flex: 1; min-height: 0; overflow: auto; overscroll-behavior: contain; } .ldxp-toolkit-table { width: 100%; min-width: 980px; border-collapse: collapse; } .ldxp-toolkit-table th, .ldxp-toolkit-table td { padding: 9px 8px; border-bottom: 1px solid #eaecf0; text-align: left; vertical-align: top; } .ldxp-toolkit-table th { position: sticky; top: 0; z-index: 1; color: #667085; background: #f8fafc; font-size: 12px; } .ldxp-toolkit-table a { color: #165dff; text-decoration: none; } .ldxp-toolkit-table .name { min-width: 250px; max-width: 420px; word-break: break-word; } .ldxp-toolkit-muted { color: #98a2b3; } .ldxp-toolkit-badge { display: inline-block; padding: 2px 7px; border-radius: 999px; font-size: 12px; white-space: nowrap; } .ldxp-toolkit-badge.ok { color: #067647; background: #ecfdf3; } .ldxp-toolkit-badge.warn { color: #b54708; background: #fffaeb; } .ldxp-toolkit-empty { padding: 36px; color: #667085; text-align: center; } .ldxp-toolkit-footer { display: flex; flex: 0 0 auto; align-items: center; gap: 8px; padding: 10px 14px; background: #fff; border-top: 1px solid #eaecf0; } .ldxp-toolkit-footer .spacer { flex: 1; } .ldxp-toolkit-report { position: fixed; inset: 0; z-index: 2147483647; display: grid; place-items: center; background: rgba(15,23,42,.4); } .ldxp-toolkit-report[hidden] { display: none; } .ldxp-toolkit-report-box { width: min(720px,calc(100vw - 40px)); padding: 16px; background: #fff; border-radius: 10px; box-shadow: 0 20px 60px rgba(15,23,42,.3); } .ldxp-toolkit-report textarea { height: min(60vh,520px); margin: 12px 0; resize: vertical; font: 13px/1.65 Consolas,"Microsoft YaHei",monospace; } .ldxp-toolkit-report-actions { display: flex; justify-content: flex-end; gap: 8px; } .ldxp-toolkit-toast { position: fixed; left: 50%; top: 26px; transform: translateX(-50%); z-index: 2147483647; padding: 10px 16px; color: #fff; background: #344054; border-radius: 7px; box-shadow: 0 8px 24px rgba(15,23,42,.22); } .ldxp-toolkit-toast.success { background: #067647; } .ldxp-toolkit-toast.error { background: #b42318; } @media (max-width: 1000px) { .ldxp-toolkit-controls { grid-template-columns: repeat(3,minmax(125px,1fr)); } } `; document.head.appendChild(style); } function createLauncher(text, onClick) { const button = document.createElement("button"); button.className = "ldxp-toolkit-launcher"; button.textContent = text; button.addEventListener("click", onClick); document.body.appendChild(button); return button; } function createPanel(title) { const panel = document.createElement("section"); panel.className = "ldxp-toolkit-panel"; panel.innerHTML = `
${escapeHtml(title)}
QQ交流群:1076144676
准备就绪
`; panel.querySelector("[data-close]").addEventListener("click", () => { panel.hidden = true; }); document.body.appendChild(panel); return panel; } function setPanelStatus(panel, message) { const status = panel.querySelector(".ldxp-toolkit-status"); if (status) status.textContent = message; } function getSourceCost(item) { const candidates = [item.cost_price, item.agent_price_limit, item.agent_price1, item.agent_price2, item.agent_price3] .map(toNumber) .filter((value) => value !== null && value >= 0); return candidates.length ? candidates[0] : null; } function getSourceStock(item) { if (item.goods_type !== "card") return null; return toNumber(item.stock_count ?? item.extend?.stock_count); } function sourceMatches(item, filters) { const price = getSourceCost(item); const stock = getSourceStock(item); const minPrice = toNumber(filters.minPrice); const maxPrice = toNumber(filters.maxPrice); if (minPrice !== null && (price === null || price < minPrice)) return false; if (maxPrice !== null && (price === null || price > maxPrice)) return false; if (filters.stock === "in" && (stock === null || stock <= 0)) return false; if (filters.stock === "out" && (stock === null || stock > 0)) return false; if (filters.listed === "yes" && listedState(item) !== true) return false; if (filters.listed === "no" && listedState(item) !== false) return false; if (filters.connect === "yes" && !item.child) return false; if (filters.connect === "no" && item.child) return false; return true; } function mountSourceTool() { const panel = createPanel("货源广场增强搜索"); const body = panel.querySelector("[data-panel-body]"); body.innerHTML = `
输入关键词后点击“增强搜索”
`; function readSourceFilters() { const result = {}; body.querySelectorAll("[data-source]").forEach((input) => { result[input.dataset.source] = input.value.trim(); }); result.maxPages = Math.max(1, Math.min(50, Number(result.maxPages) || 1)); result.addRate = Math.max(0, Number(result.addRate) || 0); return result; } function updateSourceCount() { body.querySelector("[data-source-count]").textContent = `已选 ${sourceState.selected.size} 项`; } function renderSourceResults() { const wrap = body.querySelector("[data-source-results]"); if (sourceState.loading) { wrap.innerHTML = '
正在拉取货源...
'; return; } if (!sourceState.filtered.length) { wrap.innerHTML = '
当前条件下没有结果
'; updateSourceCount(); return; } wrap.innerHTML = ` ${sourceState.filtered.map((item) => { const stock = getSourceStock(item); const connected = Boolean(item.child); const listed = listedLabel(item); return ``; }).join("")}
选择商品类型成本上架状态库存商家对接状态操作
${escapeHtml(item.name || "-")} ${escapeHtml(GOODS_TYPES[item.goods_type] || item.goods_type || "-")} ${escapeHtml(money(getSourceCost(item)))} ${listed.text} ${item.goods_type !== "card" ? '无需库存' : stock === null ? '-' : escapeHtml(stock)} ${escapeHtml(item.user?.nickname || "-")} ${connected ? "已对接" : "未对接"}
`; updateSourceCount(); } function applySourceFilters() { const filters = readSourceFilters(); sourceState.filtered = sourceState.raw.filter((item) => sourceMatches(item, filters)); sourceState.filtered.sort((a, b) => (getSourceCost(a) ?? Infinity) - (getSourceCost(b) ?? Infinity)); sourceState.selected = new Set([...sourceState.selected].filter((id) => sourceState.filtered.some((item) => String(item.id) === id))); renderSourceResults(); setPanelStatus(panel, `共拉取 ${sourceState.raw.length} 条,筛选后 ${sourceState.filtered.length} 条`); } async function searchSource(button) { const filters = readSourceFilters(); if (!filters.keywords) { notify("请先输入关键词", "error"); return; } setBusy(button, true, "搜索中..."); sourceState.loading = true; sourceState.raw = []; sourceState.filtered = []; sourceState.selected.clear(); renderSourceResults(); try { const pageSize = 50; let total = Infinity; for (let current = 1; current <= filters.maxPages && sourceState.raw.length < total; current += 1) { setPanelStatus(panel, `正在拉取第 ${current}/${filters.maxPages} 页...`); const data = await postJson(API.sourceSearch, { current, pageSize, name: "", goods_type: filters.goodsType, keywords: filters.keywords, }); const list = Array.isArray(data?.list) ? data.list : []; total = Number(data?.total) || sourceState.raw.length + list.length; sourceState.raw.push(...list); if (list.length < pageSize) break; } sourceState.loading = false; applySourceFilters(); } catch (error) { sourceState.loading = false; renderSourceResults(); setPanelStatus(panel, error.message || String(error)); notify(error.message || String(error), "error"); } finally { setBusy(button, false); } } async function connectSourceItems(ids, button) { const targets = ids.filter((id) => { const item = sourceState.raw.find((entry) => String(entry.id) === String(id)); return item && !item.child; }); if (!targets.length) { notify("没有可对接的未对接商品", "error"); return; } const { addRate } = readSourceFilters(); if (!confirm(`确定对接 ${targets.length} 个商品吗?\n默认按成本价加价 ${addRate}% 建立商品。`)) return; setBusy(button, true, "对接中..."); try { const data = await postJson(API.sourceConnect, { add_type: 1, add_rate: addRate, add_price: 0, name_sync: 1, description_sync: 1, goods_ids: targets.map((id) => Number(id)), }); targets.forEach((id) => { const item = sourceState.raw.find((entry) => String(entry.id) === String(id)); if (item) item.child = item.child || { connected: true }; sourceState.selected.delete(String(id)); }); applySourceFilters(); notify(typeof data === "string" ? data : `成功对接 ${targets.length} 个商品`, "success"); } catch (error) { notify(error.message || String(error), "error"); } finally { setBusy(button, false); } } body.addEventListener("click", (event) => { const target = event.target instanceof HTMLElement ? event.target : null; if (!target) return; const actionButton = target.closest("[data-source-action]"); if (actionButton?.dataset.sourceAction === "search") searchSource(actionButton); if (actionButton?.dataset.sourceAction === "filter") applySourceFilters(); if (actionButton?.dataset.sourceAction === "batch-connect") connectSourceItems([...sourceState.selected], actionButton); const connectButton = target.closest("[data-connect-id]"); if (connectButton) connectSourceItems([connectButton.dataset.connectId], connectButton); }); body.addEventListener("change", (event) => { const target = event.target; if (!(target instanceof HTMLInputElement || target instanceof HTMLSelectElement)) return; if (target.matches("[data-source-id]")) { const id = String(target.dataset.sourceId); target.checked ? sourceState.selected.add(id) : sourceState.selected.delete(id); updateSourceCount(); } if (target.matches("[data-source-select-all]")) { sourceState.filtered.forEach((item) => { const id = String(item.id); target.checked ? sourceState.selected.add(id) : sourceState.selected.delete(id); }); renderSourceResults(); } }); panel.hidden = true; createLauncher("增强货源搜索", () => { panel.hidden = !panel.hidden; }); } function itemStock(item) { if ((item.goods_type || goodsState.goodsType) !== "card") return "无需库存"; const stock = toNumber(item.extend?.stock_count ?? item.stock_count); return stock === null ? "-" : String(stock); } function createReportModal() { const modal = document.createElement("div"); modal.className = "ldxp-toolkit-report"; modal.hidden = true; modal.innerHTML = `
商品文字报表
`; modal.querySelector("[data-report-close]").addEventListener("click", () => { modal.hidden = true; }); modal.querySelector("[data-report-copy]").addEventListener("click", async () => { const text = modal.querySelector("[data-report-text]").value; try { await navigator.clipboard.writeText(text); notify("报表文字已复制", "success"); } catch (_) { const textarea = modal.querySelector("[data-report-text]"); textarea.focus(); textarea.select(); document.execCommand("copy"); notify("报表文字已复制", "success"); } }); document.body.appendChild(modal); return modal; } function mountGoodsTool() { const panel = createPanel("商品管理批量工具"); const reportModal = createReportModal(); const body = panel.querySelector("[data-panel-body]"); body.innerHTML = `
点击“加载商品”后,在这里勾选需要处理的商品
`; function flattenCategories(categories, parents = []) { const result = []; (Array.isArray(categories) ? categories : []).forEach((category) => { const value = category.value ?? category.id; const name = category.label ?? category.name ?? (value !== undefined ? `分类 ${value}` : "未命名分类"); const path = [...parents, String(name)]; if (value !== undefined && value !== null && value !== "") { result.push({ value: String(value), label: path.join(" / ") }); } const children = category.children ?? category.child ?? category.list; if (Array.isArray(children) && children.length) { result.push(...flattenCategories(children, path)); } }); return result; } function categoryIdForItem(item) { const cached = goodsState.infoById.get(String(item.id)); const categoryString = typeof item.category === "string" ? item.category.trim() : ""; return ( item.category_id ?? item.goods_category_id ?? item.categoryId ?? item.cate_id ?? item.category?.id ?? item.category?.value ?? cached?.category_id ?? cached?.goods_category_id ?? cached?.categoryId ?? cached?.cate_id ?? (categoryString && /^\d+$/.test(categoryString) ? categoryString : undefined) ); } function categoryLabelForItem(item) { const categoryString = typeof item.category === "string" ? item.category.trim() : ""; const cached = goodsState.infoById.get(String(item.id)); const direct = (categoryString && !/^\d+$/.test(categoryString) ? categoryString : "") || item.category?.name || item.category?.label || item.category_name || item.category_title || cached?.category?.name || cached?.category?.label || cached?.category_name || cached?.category_title; if (direct) return direct; const categoryId = categoryIdForItem(item); const matched = goodsState.categories.find((category) => String(category.value) === String(categoryId)); if (matched) return matched.label; return categoryId !== undefined && categoryId !== null && categoryId !== "" ? `分类 ID:${categoryId}` : "未分类"; } async function enrichGoodsInfo(items) { const pending = items.filter((item) => categoryIdForItem(item) === undefined && !goodsState.infoById.has(String(item.id))); if (!pending.length) return; let nextIndex = 0; let completed = 0; const worker = async () => { while (nextIndex < pending.length) { const item = pending[nextIndex]; nextIndex += 1; try { const info = await postJson(API.goodsInfo, { id: item.id }); goodsState.infoById.set(String(item.id), info || {}); } catch (_) { goodsState.infoById.set(String(item.id), {}); } completed += 1; setPanelStatus(panel, `正在识别商品分类 ${completed}/${pending.length}...`); } }; await Promise.all(Array.from({ length: Math.min(6, pending.length) }, () => worker())); } function fillCategorySelect(select, emptyLabel, searchText = "") { if (!(select instanceof HTMLSelectElement)) return; const oldValue = select.value; const keyword = searchText.trim().toLowerCase(); const options = goodsState.categories.filter((category) => !keyword || category.label.toLowerCase().includes(keyword)); select.innerHTML = ""; const empty = document.createElement("option"); empty.value = ""; empty.textContent = emptyLabel; select.appendChild(empty); options.forEach((category) => { const option = document.createElement("option"); option.value = category.value; option.textContent = category.label; select.appendChild(option); }); if ([...select.options].some((option) => option.value === oldValue)) select.value = oldValue; } function renderCategorySelects() { fillCategorySelect( body.querySelector('[data-goods="filterCategory"]'), "全部分类", body.querySelector("[data-goods-filter-category-search]")?.value || "", ); fillCategorySelect( body.querySelector('[data-goods="category"]'), "保持不变", body.querySelector("[data-goods-category-search]")?.value || "", ); } function readGoodsControls() { const values = {}; body.querySelectorAll("[data-goods]").forEach((input) => { values[input.dataset.goods] = input.value.trim(); }); values.maxPages = Math.max(1, Math.min(20, Number(values.maxPages) || 1)); values.pageSize = Math.max(20, Math.min(100, Number(values.pageSize) || 50)); return values; } function updateGoodsCount() { body.querySelector("[data-goods-count]").textContent = `已选 ${goodsState.selected.size} 项`; } function sortGoodsItems() { const sort = body.querySelector('[data-goods="priceSort"]')?.value || "default"; if (sort === "default") return; goodsState.items.sort((a, b) => { const priceA = toNumber(a.price); const priceB = toNumber(b.price); if (priceA === null && priceB === null) return 0; if (priceA === null) return 1; if (priceB === null) return -1; return sort === "desc" ? priceB - priceA : priceA - priceB; }); } function applyGoodsDisplay() { const stockFilter = body.querySelector('[data-goods="stockFilter"]')?.value || "all"; goodsState.items = goodsState.rawItems.filter((item) => { if (stockFilter === "all") return true; if ((item.goods_type || goodsState.goodsType) !== "card") return false; const stock = toNumber(item.extend?.stock_count ?? item.stock_count); if (stock === null) return false; return stockFilter === "in" ? stock > 0 : stock <= 0; }); const visibleIds = new Set(goodsState.items.map((item) => String(item.id))); goodsState.selected = new Set([...goodsState.selected].filter((id) => visibleIds.has(id))); sortGoodsItems(); renderGoodsResults(); } function renderGoodsResults() { const wrap = body.querySelector("[data-goods-results]"); if (goodsState.loading) { wrap.innerHTML = '
正在加载商品...
'; return; } if (!goodsState.items.length) { wrap.innerHTML = '
没有查到商品
'; updateGoodsCount(); return; } wrap.innerHTML = ` ${goodsState.items.map((item) => ``).join("")}
选择ID商品名称分类上架状态库存价格
${escapeHtml(item.id)} ${escapeHtml(item.name || "-")} ${escapeHtml(categoryLabelForItem(item))} ${listedLabel(item).text} ${escapeHtml(itemStock(item))} ${escapeHtml(money(item.price))}
`; updateGoodsCount(); } async function loadCategories(goodsType) { try { const data = await postJson(API.categoryList, { goods_type: goodsType }); goodsState.categories = flattenCategories(data); renderCategorySelects(); } catch (error) { notify(`分类加载失败:${error.message || error}`, "error"); } } async function loadGoods(button) { const controls = readGoodsControls(); goodsState.loading = true; goodsState.goodsType = controls.goodsType; goodsState.rawItems = []; goodsState.items = []; goodsState.selected.clear(); goodsState.infoById.clear(); renderGoodsResults(); setBusy(button, true, "加载中..."); try { await loadCategories(controls.goodsType); let total = Infinity; for (let current = 1; current <= controls.maxPages && goodsState.rawItems.length < total; current += 1) { setPanelStatus(panel, `正在加载第 ${current}/${controls.maxPages} 页...`); const data = await postJson(API.goodsList, { current, pageSize: controls.pageSize, goods_type: controls.goodsType, status: Number(controls.status), name: controls.name, category_id: controls.filterCategory ? Number(controls.filterCategory) : undefined, is_proxy: new URLSearchParams(location.search).get("is_proxy") || "1", }); const list = Array.isArray(data?.list) ? data.list : []; total = Number(data?.total) || goodsState.items.length + list.length; goodsState.rawItems.push(...list); if (list.length < controls.pageSize) break; } await enrichGoodsInfo(goodsState.rawItems); applyGoodsDisplay(); setPanelStatus(panel, `已拉取 ${goodsState.rawItems.length} 个,当前显示 ${goodsState.items.length} 个商品`); } catch (error) { goodsState.loading = false; renderGoodsResults(); notify(error.message || String(error), "error"); } finally { goodsState.loading = false; renderGoodsResults(); setBusy(button, false); } } function selectedGoods() { return goodsState.items.filter((item) => goodsState.selected.has(String(item.id))); } function calculatePrice(oldPrice, mode, value) { const price = Math.max(0, toNumber(oldPrice) ?? 0); if (mode === "fixed") return Math.max(0, value); if (mode === "percent") return Math.max(0, Math.round(price * (1 + value / 100) * 100) / 100); if (mode === "add") return Math.max(0, Math.round((price + value) * 100) / 100); return price; } async function batchEdit(button) { const items = selectedGoods(); if (!items.length) { notify("请先勾选商品", "error"); return; } const controls = readGoodsControls(); const categoryId = controls.category; const priceMode = controls.priceMode; const priceValue = toNumber(controls.priceValue); const status = controls.newStatus; if (!categoryId && priceMode === "keep" && status === "") { notify("请至少选择一个需要修改的字段", "error"); return; } if (priceMode !== "keep" && priceValue === null) { notify("请输入价格修改数值", "error"); return; } const categoryLabel = body.querySelector('[data-goods="category"] option:checked')?.textContent || "保持不变"; const changes = [ categoryId ? `分类:${categoryLabel}` : "", priceMode !== "keep" ? `价格方式:${{ fixed: "统一价格", percent: "按比例调整", add: "加减金额" }[priceMode]} ${priceValue}` : "", status !== "" ? `状态:${status === "1" ? "销售中" : "仓库中"}` : "", ].filter(Boolean).join("\n"); if (!confirm(`即将修改 ${items.length} 个商品:\n${changes}\n\n操作会直接保存到店铺,确定继续吗?`)) return; setBusy(button, true, "保存中..."); let success = 0; const failures = []; for (let index = 0; index < items.length; index += 1) { const item = items[index]; setPanelStatus(panel, `正在保存 ${index + 1}/${items.length}:${item.name}`); try { if (categoryId || priceMode !== "keep") { const cachedInfo = goodsState.infoById.get(String(item.id)); const info = cachedInfo && Object.keys(cachedInfo).length ? cachedInfo : await postJson(API.goodsInfo, { id: item.id }); const payload = JSON.parse(JSON.stringify(info || {})); if (!payload.id) payload.id = item.id; if (categoryId) payload.category_id = Number(categoryId); if (priceMode !== "keep") payload.price = calculatePrice(payload.price, priceMode, priceValue); await postJson(API.goodsUpdate, payload); } if (status !== "") { await postJson(API.goodsStatus, { id: item.id, status: Number(status) }); } success += 1; } catch (error) { failures.push(`${item.name}:${error.message || error}`); } } setBusy(button, false); setPanelStatus(panel, `批量修改完成:成功 ${success},失败 ${failures.length}`); if (failures.length) { alert(`成功 ${success} 个,失败 ${failures.length} 个:\n\n${failures.join("\n")}`); } else { notify(`成功修改 ${success} 个商品`, "success"); } await loadGoods(body.querySelector('[data-goods-action="load"]')); } async function buildReport(button) { const items = selectedGoods(); if (!items.length) { notify("请先勾选商品", "error"); return; } setBusy(button, true, "生成中..."); const blocks = []; for (let index = 0; index < items.length; index += 1) { const item = items[index]; setPanelStatus(panel, `正在获取链接 ${index + 1}/${items.length}...`); let link = safeUrl(item.link); if (link === "#") { try { const data = await postJson(API.goodsLink, { id: item.id }); link = safeUrl(data?.link || data?.short_link); } catch (_) { link = "获取失败"; } } blocks.push([ `商品名称:${item.name || "-"}`, `库存:${itemStock(item)}`, `价格:${money(item.price)}`, `链接:${link}`, ].join("\n")); } const report = blocks.join("\n--------------------\n"); reportModal.querySelector("[data-report-text]").value = report; reportModal.hidden = false; try { await navigator.clipboard.writeText(report); notify("文字报表已生成并复制", "success"); } catch (_) { notify("报表已生成,请点击“复制全部文字”", "info"); } setPanelStatus(panel, `已生成 ${items.length} 个商品的文字报表`); setBusy(button, false); } body.addEventListener("click", (event) => { const target = event.target instanceof HTMLElement ? event.target : null; if (!target) return; const button = target.closest("[data-goods-action]"); if (!button) return; const action = button.dataset.goodsAction; if (action === "load") loadGoods(button); if (action === "batch-edit") batchEdit(button); if (action === "report") buildReport(button); }); body.addEventListener("change", (event) => { const target = event.target; if (!(target instanceof HTMLInputElement || target instanceof HTMLSelectElement)) return; if (target.matches("[data-goods-id]")) { const id = String(target.dataset.goodsId); target.checked ? goodsState.selected.add(id) : goodsState.selected.delete(id); updateGoodsCount(); } if (target.matches("[data-goods-select-all]")) { goodsState.items.forEach((item) => { const id = String(item.id); target.checked ? goodsState.selected.add(id) : goodsState.selected.delete(id); }); renderGoodsResults(); } if (target.matches('[data-goods="goodsType"]')) { goodsState.categories = []; body.querySelector("[data-goods-filter-category-search]").value = ""; body.querySelector("[data-goods-category-search]").value = ""; loadCategories(target.value); } if (target.matches('[data-goods="priceSort"]')) { applyGoodsDisplay(); } if (target.matches('[data-goods="stockFilter"]')) { applyGoodsDisplay(); setPanelStatus(panel, `已拉取 ${goodsState.rawItems.length} 个,当前显示 ${goodsState.items.length} 个商品`); } }); body.addEventListener("input", (event) => { const target = event.target; if (!(target instanceof HTMLInputElement)) return; if (target.matches("[data-goods-filter-category-search]")) { fillCategorySelect(body.querySelector('[data-goods="filterCategory"]'), "全部分类", target.value); } if (target.matches("[data-goods-category-search]")) { fillCategorySelect(body.querySelector('[data-goods="category"]'), "保持不变", target.value); } }); panel.hidden = true; createLauncher("商品批量工具", () => { panel.hidden = !panel.hidden; if (!panel.hidden && !goodsState.categories.length) loadCategories(readGoodsControls().goodsType); }); createLauncher("货源广场", () => { location.href = SOURCE_PATH; }).classList.add("goto"); } function mount() { if (document.getElementById(APP_ID)) return; installStyles(); const marker = document.createElement("div"); marker.id = APP_ID; marker.hidden = true; document.body.appendChild(marker); if (location.pathname === SOURCE_PATH) mountSourceTool(); if (location.pathname === GOODS_PATH) mountGoodsTool(); } function boot() { let lastPath = `${location.pathname}${location.search}`; mount(); const observer = new MutationObserver(() => { const currentPath = `${location.pathname}${location.search}`; if (currentPath !== lastPath) { lastPath = currentPath; document.querySelectorAll(`.ldxp-toolkit-launcher,.ldxp-toolkit-panel,.ldxp-toolkit-report,#${APP_ID}`).forEach((node) => node.remove()); mount(); } }); observer.observe(document.documentElement, { childList: true, subtree: true }); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", boot, { once: true }); } else { boot(); } })();