// ==UserScript== // @name 阳光智链市场热图订单列表 // @namespace https://easymoo.com/ssc-helper // @version 0.3.0 // @description 在阳光智链市场热图页按当前产品和月份展示发标城市、数量与客户信息 // @author Codex // @match https://www.easymoo.com/ssc-race*/view/pro-detail-heatmap* // @match http://www.easymoo.com/ssc-race*/view/pro-detail-heatmap* // @grant GM_xmlhttpRequest // @connect www.easymoo.com // ==/UserScript== (function () { 'use strict'; const PANEL_ID = 'ssc-heatmap-order-panel'; const STYLE_ID = 'ssc-heatmap-order-style'; const MONTH_GROUP_SELECTOR = '#month-group'; const MONTH_ITEM_SELECTOR = '#month-group .list-group-item'; const SKU_SELECTOR = 'input[name="filterSkuId"]'; const DISPLAY_TYPE_SELECTOR = 'input[name="displayType"]'; const monthNameByDate = { '2026-01-01': '一月', '2026-02-01': '二月', '2026-03-01': '三月', '2026-04-01': '四月', '2026-05-01': '五月', '2026-06-01': '六月', }; let debounceTimer = null; let lastRequestKey = ''; let customerLookupCache = null; let customerLookupKey = ''; let batchExporting = false; function addStyle() { if (document.getElementById(STYLE_ID)) return; const style = document.createElement('style'); style.id = STYLE_ID; style.textContent = ` #${PANEL_ID} { position: fixed; right: 18px; bottom: 18px; z-index: 1080; width: min(660px, calc(100vw - 36px)); max-height: min(680px, calc(100vh - 36px)); display: flex; flex-direction: column; background: rgba(255, 255, 255, 0.78); color: #1f2933; border: 1px solid #d7dde5; border-radius: 8px; box-shadow: 0 10px 28px rgba(15, 23, 42, 0.16); backdrop-filter: blur(4px); font-size: 13px; line-height: 1.45; overflow: hidden; } #${PANEL_ID}.is-collapsed { width: auto; } #${PANEL_ID}.is-collapsed .ssc-panel-body { display: none; } #${PANEL_ID} .ssc-panel-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 10px 12px; background: rgba(20, 60, 95, 0.82); color: #ffffff; } #${PANEL_ID} .ssc-panel-title { font-weight: 700; white-space: nowrap; } #${PANEL_ID} .ssc-panel-actions { display: flex; gap: 6px; } #${PANEL_ID} button { appearance: none; border: 1px solid rgba(255, 255, 255, 0.55); background: rgba(255, 255, 255, 0.12); color: #ffffff; border-radius: 4px; padding: 2px 7px; cursor: pointer; font-size: 12px; } #${PANEL_ID} .ssc-panel-body { overflow: auto; } #${PANEL_ID} .ssc-summary { padding: 10px 12px; border-bottom: 1px solid #e5e9ef; background: rgba(248, 250, 252, 0.72); } #${PANEL_ID} .ssc-meta { display: grid; grid-template-columns: 74px 1fr; gap: 4px 8px; } #${PANEL_ID} .ssc-meta-label { color: #5d6978; } #${PANEL_ID} .ssc-status { margin-top: 8px; color: #5d6978; } #${PANEL_ID} table { width: 100%; border-collapse: collapse; } #${PANEL_ID} th, #${PANEL_ID} td { padding: 7px 8px; border-bottom: 1px solid #e7ebf0; vertical-align: top; } #${PANEL_ID} th { position: sticky; top: 0; background: rgba(240, 244, 248, 0.86); color: #374151; font-weight: 700; } #${PANEL_ID} td.qty, #${PANEL_ID} th.qty { text-align: right; white-space: nowrap; } #${PANEL_ID} td.customer { min-width: 130px; color: #344054; } #${PANEL_ID} .ssc-empty, #${PANEL_ID} .ssc-error { padding: 16px 12px; } #${PANEL_ID} .ssc-error { color: #b42318; } `; document.head.appendChild(style); } function createPanel() { let panel = document.getElementById(PANEL_ID); if (panel) return panel; panel = document.createElement('section'); panel.id = PANEL_ID; panel.innerHTML = `
热图订单城市
产品
-
月份
-
合计
-
等待页面加载...
`; document.body.appendChild(panel); panel.querySelector('[data-action="export-all"]').addEventListener('click', exportAllProductMonths); panel.querySelector('[data-action="refresh"]').addEventListener('click', () => refreshNow(true)); panel.querySelector('[data-action="toggle"]').addEventListener('click', (event) => { panel.classList.toggle('is-collapsed'); event.currentTarget.textContent = panel.classList.contains('is-collapsed') ? '展开' : '收起'; }); return panel; } function getQueryParam(name) { return new URLSearchParams(location.search).get(name); } function getPublishId() { return getQueryParam('publishId') || window.PUBLISH_ID; } function getRunId() { return getQueryParam('runId') || window.RUN_ID; } function getRaceContextPath() { const match = location.pathname.match(/^\/(ssc-race\d+)\//); return match ? `/${match[1]}` : '/ssc-race1'; } function getSelectedSku() { const checked = document.querySelector(`${SKU_SELECTOR}:checked`); if (!checked) return { id: '', name: '所有产品' }; const id = checked.value || ''; const label = checked.closest('label') || checked.parentElement; const name = label ? label.textContent.replace(/\s+/g, ' ').trim() : ''; return { id, name: name || (id ? id : '所有产品') }; } function getAllSkus() { const seen = new Set(); return Array.from(document.querySelectorAll(SKU_SELECTOR)).map((input) => { const id = input.value || ''; const label = input.closest('label') || input.parentElement; const name = label ? label.textContent.replace(/\s+/g, ' ').trim() : ''; return { id, name: name || (id ? id : '所有产品') }; }).filter((sku) => { if (!sku.id || seen.has(sku.id)) return false; seen.add(sku.id); return true; }); } function getAllMonths() { const seen = new Set(); return Array.from(document.querySelectorAll(MONTH_ITEM_SELECTOR)).map((item) => { const start = item.dataset.monthStartDate || ''; const label = monthNameByDate[start] || item.textContent.replace(/\s+/g, ' ').trim() || start; return { start, end: start, label }; }).filter((month) => { if (!month.start || seen.has(month.start)) return false; seen.add(month.start); return true; }); } function getSelectedMonthRange() { const activeItems = Array.from(document.querySelectorAll(`${MONTH_ITEM_SELECTOR}.choose`)); const items = activeItems.length ? activeItems : Array.from(document.querySelectorAll(MONTH_ITEM_SELECTOR)).slice(0, 1); const first = items[0]; const last = items[items.length - 1]; const start = first ? first.dataset.monthStartDate : ''; const end = last ? last.dataset.monthStartDate : start; const displayType = document.querySelector(`${DISPLAY_TYPE_SELECTOR}:checked`)?.value || 'month'; let label = '-'; if (start && end && start !== end) { label = `${monthNameByDate[start] || start} - ${monthNameByDate[end] || end}`; } else if (start) { label = monthNameByDate[start] || first.textContent.replace(/\s+/g, ' ').trim() || start; } return { start, end, label, displayType }; } function setPanelState({ skuName, monthLabel, totalText, status, html }) { const panel = createPanel(); panel.querySelector('[data-field="sku"]').textContent = skuName || '-'; panel.querySelector('[data-field="month"]').textContent = monthLabel || '-'; panel.querySelector('[data-field="total"]').textContent = totalText || '-'; panel.querySelector('[data-field="status"]').textContent = status || ''; if (typeof html === 'string') { panel.querySelector('[data-field="content"]').innerHTML = html; } } function postJson(path, payload) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'POST', url: `${location.origin}${path}`, data: JSON.stringify(payload), headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest', }, withCredentials: true, onload: (response) => { if (response.status < 200 || response.status >= 300) { reject(new Error(`接口返回 ${response.status}`)); return; } try { resolve(JSON.parse(response.responseText || '[]')); } catch (error) { reject(new Error('接口返回的不是 JSON,可能登录已失效')); } }, onerror: () => reject(new Error('请求失败')), ontimeout: () => reject(new Error('请求超时')), timeout: 20000, }); }); } function normalizeCountry(row) { const provinceName = row.provinceName || ''; if (provinceName.startsWith('中国-')) { return { country: '中国', region: provinceName.slice(3) }; } return { country: provinceName || '-', region: '' }; } async function loadCustomerLookup() { const publishId = getPublishId(); const runId = getRunId(); const cacheKey = JSON.stringify({ publishId, runId }); if (customerLookupCache && customerLookupKey === cacheKey) return customerLookupCache; if (!publishId || !runId) { customerLookupCache = { byCityCode: new Map(), byCityName: new Map() }; customerLookupKey = cacheKey; return customerLookupCache; } const data = await postJson(`${getRaceContextPath()}/ppPublish/loadPublishCustomer`, { publishId: Number(publishId), runId: Number(runId), }); const partyNameByCode = new Map(); (data.partyQueries || []).forEach((party) => { if (party.partyCode) partyNameByCode.set(String(party.partyCode), party.partyName || String(party.partyCode)); }); const byCityCode = new Map(); const byCityName = new Map(); (data.cityQueries || []).forEach((city) => { const partyCode = String(city.partyCode || ''); const partyName = partyNameByCode.get(partyCode) || partyCode; if (!partyName) return; if (city.cityCode) addCustomerName(byCityCode, String(city.cityCode), partyName); const cityNameKey = makeCityNameKey(city); if (cityNameKey) addCustomerName(byCityName, cityNameKey, partyName); }); customerLookupCache = { byCityCode, byCityName }; customerLookupKey = cacheKey; return customerLookupCache; } function addCustomerName(map, key, partyName) { const names = map.get(key) || []; if (!names.includes(partyName)) names.push(partyName); map.set(key, names); } function makeCityNameKey(row) { const province = row.provinceCode || row.provinceName || ''; const city = row.cityName || ''; return city ? `${province}::${city}` : ''; } function getCustomerNames(row, lookup) { if (!lookup) return []; if (row.cityCode && lookup.byCityCode.has(String(row.cityCode))) { return lookup.byCityCode.get(String(row.cityCode)); } const cityNameKey = makeCityNameKey(row); if (cityNameKey && lookup.byCityName.has(cityNameKey)) { return lookup.byCityName.get(cityNameKey); } return []; } function renderRows(rows, customerLookup) { if (!rows.length) return '
当前选择没有发标城市。
'; const sorted = rows.slice().sort((a, b) => { const ca = normalizeCountry(a); const cb = normalizeCountry(b); return ca.country.localeCompare(cb.country, 'zh-Hans-CN') || ca.region.localeCompare(cb.region, 'zh-Hans-CN') || String(a.cityName || '').localeCompare(String(b.cityName || ''), 'zh-Hans-CN'); }); const body = sorted.map((row) => { const loc = normalizeCountry(row); const place = loc.region ? `${loc.country}-${loc.region}` : loc.country; const qty = Number(row.quantity || 0); const customers = getCustomerNames(row, customerLookup).join('、') || '-'; return ` ${escapeHtml(place)} ${escapeHtml(row.cityName || '-')} ${escapeHtml(customers)} ${formatNumber(qty)} `; }).join(''); return `${body}
国家/地区城市客户信息数量
`; } function buildExportRows(rows, customerLookup, meta) { return rows.slice().sort((a, b) => { const ca = normalizeCountry(a); const cb = normalizeCountry(b); return ca.country.localeCompare(cb.country, 'zh-Hans-CN') || ca.region.localeCompare(cb.region, 'zh-Hans-CN') || String(a.cityName || '').localeCompare(String(b.cityName || ''), 'zh-Hans-CN'); }).map((row) => { const loc = normalizeCountry(row); return { country: loc.country, region: loc.region, city: row.cityName || '-', customer: getCustomerNames(row, customerLookup).join('、') || '-', productMonth: getHeatmapProductMonth(row, meta), skuId: meta?.skuId || '-', monthDate: meta?.monthDate || '-', longitude: row.longitude ?? '', latitude: row.latitude ?? '', quantity: Number(row.quantity || 0), }; }); } async function exportAllProductMonths() { if (batchExporting) return; const publishId = getPublishId(); const skus = getAllSkus(); const months = getAllMonths(); const panel = createPanel(); const status = panel.querySelector('[data-field="status"]'); if (!publishId || !skus.length || !months.length) { status.textContent = '没有识别到产品或月份,无法批量导出。'; return; } batchExporting = true; const button = panel.querySelector('[data-action="export-all"]'); button.disabled = true; button.textContent = '导出中'; try { const customerLookup = await loadCustomerLookup(); const allRows = []; const totalTasks = skus.length * months.length; let doneTasks = 0; for (const sku of skus) { for (const month of months) { doneTasks += 1; status.textContent = `批量导出中 ${doneTasks}/${totalTasks}:${sku.name} ${month.label}`; const rows = await postJson(`${getRaceContextPath()}/execute/loadCityTender`, { publishId: Number(publishId), startMonthDate: month.start, endMonthDate: month.end, skuId: sku.id, }); const meta = { skuId: sku.id, skuName: sku.name, monthLabel: month.label, monthDate: month.start, }; allRows.push(...buildExportRows(rows, customerLookup, meta).map((row) => ({ ...row, skuName: sku.name, monthLabel: month.label, }))); } } downloadCsv('阳光智链_全部产品月份发标城市.csv', buildCsvRows(allRows)); status.textContent = `批量导出完成:${skus.length} 个产品 × ${months.length} 个月,共 ${allRows.length} 条城市数据。`; } catch (error) { status.textContent = `批量导出失败:${error.message || error}`; } finally { batchExporting = false; button.disabled = false; button.textContent = '批量导出'; } } function buildCsvRows(rows, meta) { const header = ['产品', '产品ID', '月份', '月份日期', '产品月份', '国家', '地区', '城市', '客户信息', '数量', '经度', '纬度']; return [header].concat(rows.map((row) => [ row.skuName || meta?.skuName || '', row.skuId, row.monthLabel || meta?.monthLabel || '', row.monthDate, row.productMonth, row.country, row.region, row.city, row.customer, row.quantity, row.longitude, row.latitude, ])); } function downloadCsv(fileName, csvRows) { const csv = `\ufeff${csvRows.map((row) => row.map(csvCell).join(',')).join('\n')}`; const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = fileName; document.body.appendChild(link); link.click(); link.remove(); window.setTimeout(() => URL.revokeObjectURL(url), 1000); } function csvCell(value) { const text = String(value ?? ''); return `"${text.replace(/"/g, '""')}"`; } function getHeatmapProductMonth(row, meta) { const directValue = firstText(row, [ 'productMonth', 'productMonthName', 'skuMonth', 'skuMonthName', 'tenderMonthName', 'monthProductName', ]); if (directValue) return directValue; const product = firstText(row, [ 'skuName', 'productName', 'product', 'goodsName', 'materialName', ]) || meta?.skuName || ''; const month = formatHeatmapMonth(firstText(row, [ 'monthName', 'tenderMonth', 'month', 'monthDate', 'monthStartDate', 'startMonthDate', 'bidMonth', ])) || meta?.monthDate || meta?.monthLabel || ''; return `${product || '-'} ${month || '-'}`.trim(); } function firstText(source, keys) { if (!source) return ''; for (const key of keys) { const value = source[key]; if (value !== undefined && value !== null && String(value).trim() !== '') { return String(value).replace(/\s+/g, ' ').trim(); } } return ''; } function formatHeatmapMonth(value) { if (!value) return ''; return monthNameByDate[value] || value; } function escapeHtml(value) { return String(value).replace(/[&<>"]/g, (char) => ({ '&': '&', '<': '<', '>': '>', '"': '"', }[char])); } function formatNumber(value) { return new Intl.NumberFormat('zh-CN', { maximumFractionDigits: 0 }).format(value); } async function refreshNow(force) { const publishId = getPublishId(); const sku = getSelectedSku(); const month = getSelectedMonthRange(); if (!publishId || !month.start || !month.end) { setPanelState({ skuName: sku.name, monthLabel: month.label, totalText: '-', status: '没有识别到 publishId 或月份,请确认在市场热图页。', html: '
无法读取当前比赛参数。
', }); return; } const requestKey = JSON.stringify({ publishId, skuId: sku.id, start: month.start, end: month.end }); if (!force && requestKey === lastRequestKey) return; lastRequestKey = requestKey; setPanelState({ skuName: sku.name, monthLabel: month.label, totalText: '-', status: '正在读取热图订单数据...', }); try { const [rows, customerLookup] = await Promise.all([ postJson(`${getRaceContextPath()}/execute/loadCityTender`, { publishId: Number(publishId), startMonthDate: month.start, endMonthDate: month.end, skuId: sku.id, }), loadCustomerLookup(), ]); const total = rows.reduce((sum, row) => sum + Number(row.quantity || 0), 0); setPanelState({ skuName: sku.name, monthLabel: month.label, totalText: `${formatNumber(total)} 箱 / ${rows.length} 城市`, status: `已按当前热图选择更新。`, html: renderRows(rows, customerLookup), }); } catch (error) { setPanelState({ skuName: sku.name, monthLabel: month.label, totalText: '-', status: '读取失败', html: `
${escapeHtml(error.message || error)}
`, }); } } function scheduleRefresh(force) { window.clearTimeout(debounceTimer); debounceTimer = window.setTimeout(() => refreshNow(force), 250); } function bindEvents() { document.addEventListener('change', (event) => { if (event.target.matches(`${SKU_SELECTOR}, ${DISPLAY_TYPE_SELECTOR}, input[name="showSupplier"]`)) { scheduleRefresh(true); } }, true); document.addEventListener('click', (event) => { if (event.target.closest(MONTH_ITEM_SELECTOR)) { scheduleRefresh(true); } }, true); const observer = new MutationObserver(() => scheduleRefresh(false)); const monthGroup = document.querySelector(MONTH_GROUP_SELECTOR); if (monthGroup) { observer.observe(monthGroup, { attributes: true, subtree: true, attributeFilter: ['class'] }); } } function startWhenReady() { addStyle(); createPanel(); bindEvents(); scheduleRefresh(true); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', startWhenReady); } else { startWhenReady(); } })();