// ==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 ``;
}
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();
}
})();