let priceCheckLoading = false;
// ==================== 面板创建 ====================
function createPanel() {
if (panelEl) return panelEl;
panelEl = h('div', { class: 'sme-panel', id: 'sme-panel' }, [
h('div', { class: 'sme-header' }, [
h('div', { class: 'sme-header-title' }, [
h('span', { html: ICONS.bag }),
h('span', { text: '社区市场增强' }),
]),
h('div', { class: 'sme-tabs' }, [
h('button', { class: 'sme-tab active', id: 'sme-tab-summary', html: ICONS.list + '上架汇总', onClick: () => switchTab('summary') }),
h('button', { class: 'sme-tab', id: 'sme-tab-history', html: ICONS.chart + '价格历史', onClick: () => switchTab('history') }),
h('button', { class: 'sme-tab', id: 'sme-tab-orderbook', html: ICONS.layers + '订单深度', onClick: () => switchTab('orderbook') }),
h('button', { class: 'sme-tab', id: 'sme-tab-settings', html: ICONS.settings + '设置', onClick: () => switchTab('settings') }),
]),
]),
h('div', { class: 'sme-content', id: 'sme-content' }),
]);
return panelEl;
}
function switchTab(tab) {
currentTab = tab;
panelEl.querySelectorAll('.sme-tab').forEach(b => b.classList.remove('active'));
const btn = document.getElementById('sme-tab-' + tab);
if (btn) btn.classList.add('active');
const content = document.getElementById('sme-content');
if (!content) return;
content.innerHTML = '';
if (tab === 'summary') renderSummary(content);
else if (tab === 'history') renderPriceHistory(content);
else if (tab === 'orderbook') renderOrderBook(content);
else if (tab === 'settings') renderSettings(content);
}
// ==================== 渲染:上架汇总 ====================
function renderSummary(container) {
if (!allSellOrders || allSellOrders.length === 0) {
container.appendChild(h('div', { class: 'sme-empty' }, [
h('span', { html: ICONS.package }),
h('span', { text: '您没有在售的此物品' }),
]));
return;
}
const grouped = groupOrders(allSellOrders);
const total = grouped.reduce((s, g) => s + g.totalCount, 0);
const priceSet = new Set(allSellOrders.map(o => o.buyerPrice));
// 计算买家/卖家总价
let totalBuyerCents = 0, totalSellerCents = 0;
for (const o of allSellOrders) {
const buyerCents = parsePriceToCents(o.buyerPrice);
totalBuyerCents += buyerCents;
totalSellerCents += priceBeforeFees(buyerCents);
}
// 统计卡片(含买家/卖家总价)
const statGrid = h('div', { class: 'sme-stats-grid' }, [
makeStatCard(total, '在售总数'),
makeStatCard(formatCents(totalBuyerCents), '买家总价'),
makeStatCard(formatCents(totalSellerCents), '卖家到手'),
makeStatCard(priceSet.size, '价格种类'),
]);
container.appendChild(statGrid);
// 价格检测信息条 + 汇总表格
const tableContainer = h('div');
container.appendChild(tableContainer);
if (settings.get('autoCheckPrices') && listingInfo) {
// 自动检测价格
renderSummaryWithPriceCheck(tableContainer, grouped, total);
} else {
renderSummaryTable(tableContainer, grouped, total, null);
}
// 下架区域
container.appendChild(renderDelistSection(allSellOrders));
// 重新上架区域(仅当有价格检测结果时显示)
if (priceCheckResults) {
container.appendChild(renderRelistSection(allSellOrders, priceCheckResults));
}
}
/**
* 渲染带价格检测的汇总(异步获取订单簿 + 价格历史后标注)
*/
function renderSummaryWithPriceCheck(container, grouped, total) {
container.appendChild(h('div', { class: 'sme-price-check-info', id: 'sme-price-check-info' }, [
h('span', { html: ICONS.refreshCw }),
h('span', { text: '正在检测价格...' }),
]));
renderSummaryTable(container, grouped, total, null);
priceCheckLoading = true;
const tasks = [];
// 立即获取价格历史(不依赖 item_nameid)
tasks.push(
fetchPriceHistory(listingInfo.appid, listingInfo.marketHashName)
.then(data => { priceHistoryData = data; })
.catch(err => W('Price check: price history failed:', err.message))
);
// 获取订单簿(SSR 优先,降级到 item_nameid+API)
tasks.push(
fetchOrderBook()
.then(data => { orderBookData = data; })
.catch(err => W('Price check: order book failed:', err.message))
);
Promise.allSettled(tasks).then(() => {
priceCheckLoading = false;
// 计算每个价格的检测状态
priceCheckResults = new Map();
for (const order of allSellOrders) {
if (!priceCheckResults.has(order.buyerPrice)) {
const result = checkListingPrice(order, priceHistoryData, orderBookData);
priceCheckResults.set(order.buyerPrice, result);
}
}
// 重新渲染表格
const infoBar = document.getElementById('sme-price-check-info');
if (infoBar) {
const overpricedCount = [...priceCheckResults.values()].filter(r => r.status === 'overpriced').length;
const underpricedCount = [...priceCheckResults.values()].filter(r => r.status === 'underpriced').length;
const fairCount = [...priceCheckResults.values()].filter(r => r.status === 'fair').length;
const uncheckedCount = [...priceCheckResults.values()].filter(r => r.status === 'unchecked').length;
const algoName = ALGORITHM_NAMES[settings.get('priceAlgorithm')] || '';
infoBar.innerHTML = '';
infoBar.appendChild(h('span', { html: ICONS.check }));
infoBar.appendChild(h('span', { text: `价格检测完成 · 算法: ` }));
infoBar.appendChild(h('span', { class: 'sme-algo-name', text: algoName }));
if (overpricedCount > 0) infoBar.appendChild(h('span', { style: { color: 'var(--sme-red)', marginLeft: '8px' }, text: `超价 ${overpricedCount}种` }));
if (underpricedCount > 0) infoBar.appendChild(h('span', { style: { color: 'var(--sme-amber)', marginLeft: '8px' }, text: `低价 ${underpricedCount}种` }));
if (fairCount > 0) infoBar.appendChild(h('span', { style: { color: 'var(--sme-green)', marginLeft: '8px' }, text: `合理 ${fairCount}种` }));
if (uncheckedCount > 0) infoBar.appendChild(h('span', { style: { color: 'var(--sme-text-dim)', marginLeft: '8px' }, text: `未检 ${uncheckedCount}种` }));
}
// 刷新表格和重新上架区域
container.innerHTML = '';
container.appendChild(infoBar || h('div'));
renderSummaryTable(container, grouped, total, priceCheckResults);
// 刷新重新上架区域
const oldRelist = document.getElementById('sme-relist-section');
if (oldRelist) oldRelist.remove();
const content = document.getElementById('sme-content');
if (content && priceCheckResults) {
content.appendChild(renderRelistSection(allSellOrders, priceCheckResults));
}
});
}
/**
* 渲染汇总表格(可带价格检测结果)
*/
function renderSummaryTable(container, grouped, total, checkResults) {
let rows = '';
for (const g of grouped) {
const tags = g.sortedPrices.map(p => {
const cnt = g.prices.get(p).length;
const check = checkResults?.get(p);
let statusClass = '';
let suggestedHtml = '';
if (check) {
statusClass = ' ' + check.status;
if (check.suggestedCents > 0 && check.status !== 'fair') {
const suggestedWithFees = priceIncludingFees(check.suggestedCents);
suggestedHtml = `→ ${escapeHtml(formatCents(suggestedWithFees))}`;
}
}
const dot = check ? `` : '';
return `${dot}${escapeHtml(p)}×${cnt}${suggestedHtml}`;
}).join('');
rows += `| ${escapeHtml(g.dateKey)} | ${tags} | ${g.totalCount} |
`;
}
const tableWrap = h('div', { style: { overflowX: 'auto' } }, []);
tableWrap.innerHTML = `| 日期 | 价格分布 | 数量 |
${rows}| 合计 | | ${total} |
`;
container.appendChild(tableWrap);
}
function makeStatCard(val, lbl) {
return h('div', { class: 'sme-stat-card' }, [
h('div', { class: 'sme-stat-val', text: String(val) }),
h('div', { class: 'sme-stat-lbl', text: lbl }),
]);
}
function renderDelistSection(orders) {
const priceMap = new Map();
for (const o of orders) {
priceMap.set(o.buyerPrice, (priceMap.get(o.buyerPrice) || 0) + 1);
}
const options = [...priceMap.entries()]
.sort((a, b) => a[0].localeCompare(b[0]))
.map(([p, c]) => ``)
.join('');
const section = h('div', { class: 'sme-delist-section' }, [
h('div', { class: 'sme-delist-row' }, [
h('span', { class: 'sme-delist-label', text: '下架价格:' }),
h('select', { class: 'sme-delist-select', id: 'sme-delist-price', html: `${options}` }),
h('span', { class: 'sme-delist-label', text: '数量:' }),
h('input', { type: 'number', class: 'sme-delist-input', id: 'sme-delist-qty', value: '1', min: '1', placeholder: '数量' }),
h('button', { class: 'sme-delist-btn', id: 'sme-delist-btn', text: '下架', onClick: () => handleDelist(orders, false) }),
h('button', { class: 'sme-delist-all-btn', id: 'sme-delist-all-btn', text: '全部下架', onClick: () => handleDelist(orders, true) }),
]),
h('div', { class: 'sme-progress-wrap', id: 'sme-delist-progress', style: { display: 'none' } }, [
h('div', { class: 'sme-progress-bar', id: 'sme-delist-progress-bar' }),
]),
h('div', { class: 'sme-delist-status', id: 'sme-delist-status' }),
]);
return section;
}
async function handleDelist(orders, delistAll) {
const priceSelect = document.getElementById('sme-delist-price');
const qtyInput = document.getElementById('sme-delist-qty');
const statusEl = document.getElementById('sme-delist-status');
const btn = document.getElementById('sme-delist-btn');
const allBtn = document.getElementById('sme-delist-all-btn');
const progressWrap = document.getElementById('sme-delist-progress');
const progressBar = document.getElementById('sme-delist-progress-bar');
let candidates;
if (delistAll) {
candidates = [...orders].sort((a, b) => b.rtListed - a.rtListed);
if (!confirm(`确定要下架全部 ${candidates.length} 件物品吗?`)) return;
} else {
const price = priceSelect.value;
const qty = parseInt(qtyInput.value, 10);
if (!price) { statusEl.textContent = '请选择价格'; statusEl.className = 'sme-delist-status sme-error'; return; }
if (!qty || qty < 1) { statusEl.textContent = '请输入有效数量'; statusEl.className = 'sme-delist-status sme-error'; return; }
candidates = orders.filter(o => o.buyerPrice === price).sort((a, b) => b.rtListed - a.rtListed).slice(0, qty);
}
if (candidates.length === 0) { statusEl.textContent = '没有可下架的商品'; statusEl.className = 'sme-delist-status sme-error'; return; }
const sessionid = getSessionId();
if (!sessionid) { statusEl.textContent = '无法获取会话ID'; statusEl.className = 'sme-delist-status sme-error'; return; }
btn.disabled = true;
allBtn.disabled = true;
progressWrap.style.display = '';
progressBar.style.width = '0%';
let success = 0, failed = 0;
for (let i = 0; i < candidates.length; i++) {
const item = candidates[i];
statusEl.textContent = `正在下架 ${i + 1}/${candidates.length}...`;
statusEl.className = 'sme-delist-status';
progressBar.style.width = Math.round((i / candidates.length) * 100) + '%';
try {
const resp = await fetch(`https://steamcommunity.com/market/removelisting/${item.listingid}`, {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: `sessionid=${encodeURIComponent(sessionid)}`,
});
if (resp.ok) {
success++;
const idx = allSellOrders.indexOf(item);
if (idx !== -1) allSellOrders.splice(idx, 1);
} else {
failed++;
W('Delist failed:', item.listingid, 'HTTP', resp.status);
}
} catch (e) {
failed++;
W('Delist error:', item.listingid, e);
}
if (i < candidates.length - 1) await sleep(300);
}
progressBar.style.width = '100%';
statusEl.textContent = `下架完成: 成功 ${success} 件` + (failed > 0 ? `, 失败 ${failed} 件` : '');
statusEl.className = 'sme-delist-status' + (failed > 0 ? ' sme-error' : ' sme-ok');
btn.disabled = false;
allBtn.disabled = false;
toast(`下架完成: 成功 ${success} 件` + (failed > 0 ? `, 失败 ${failed} 件` : ''), failed > 0 ? 'warning' : 'success');
setTimeout(() => { progressWrap.style.display = 'none'; }, 2000);
// 刷新汇总
switchTab('summary');
}
// ==================== 重新上架(超价物品) ====================
/**
* 渲染重新上架区域
*/
function renderRelistSection(orders, checkResults) {
// 找出所有超价物品
const overpricedOrders = orders.filter(o => {
const check = checkResults.get(o.buyerPrice);
return check && check.status === 'overpriced' && check.suggestedCents > 0;
});
if (overpricedOrders.length === 0) return h('div'); // 无超价物品时不显示
// 按价格分组统计
const priceMap = new Map();
for (const o of overpricedOrders) {
const check = checkResults.get(o.buyerPrice);
if (!priceMap.has(o.buyerPrice)) {
priceMap.set(o.buyerPrice, { count: 0, suggestedCents: check.suggestedCents, source: check.source });
}
priceMap.get(o.buyerPrice).count++;
}
const section = h('div', { class: 'sme-relist-section', id: 'sme-relist-section' }, [
h('div', { class: 'sme-relist-row' }, [
h('span', { html: ICONS.zap, style: { color: 'var(--sme-purple)' } }),
h('span', { style: { fontWeight: '700', color: 'var(--sme-text-bright)' }, text: '重新上架超价物品' }),
h('button', {
class: 'sme-relist-btn',
id: 'sme-relist-btn',
html: ICONS.refreshCw + '重新上架全部',
onClick: () => handleRelist(overpricedOrders, checkResults),
}),
]),
h('div', { class: 'sme-relist-summary', text: `检测到 ${overpricedOrders.length} 件超价物品,将以下架后按建议价格重新上架` }),
]);
// 列出超价价格和建议价格
const listEl = h('div', { style: { marginTop: '8px' } });
for (const [price, info] of priceMap) {
const suggestedWithFees = priceIncludingFees(info.suggestedCents);
listEl.appendChild(h('div', { class: 'sme-relist-item' }, [
h('span', { class: 'sme-relist-from', text: `${price} ×${info.count}` }),
h('span', { class: 'sme-relist-arrow', text: '→' }),
h('span', { class: 'sme-relist-to', text: formatCents(suggestedWithFees) }),
h('span', { style: { color: 'var(--sme-text-dim)', fontSize: '10px' }, text: `(${info.source})` }),
]));
}
section.appendChild(listEl);
// 进度条和状态
section.appendChild(h('div', { class: 'sme-progress-wrap', id: 'sme-relist-progress', style: { display: 'none' } }, [
h('div', { class: 'sme-progress-bar', id: 'sme-relist-progress-bar' }),
]));
section.appendChild(h('div', { class: 'sme-delist-status', id: 'sme-relist-status' }));
return section;
}
/**
* 处理重新上架:下架 → 等待 → 重新上架
* 参考 Steam Economy Enhancer 的 marketOverpricedQueueWorker 三步流程
*/
async function handleRelist(overpricedOrders, checkResults) {
const btn = document.getElementById('sme-relist-btn');
const statusEl = document.getElementById('sme-relist-status');
const progressWrap = document.getElementById('sme-relist-progress');
const progressBar = document.getElementById('sme-relist-progress-bar');
const sessionid = getSessionId();
if (!sessionid) {
statusEl.textContent = '无法获取会话ID';
statusEl.className = 'sme-delist-status sme-error';
return;
}
if (!confirm(`确定要重新上架 ${overpricedOrders.length} 件超价物品吗?\n这将先下架再按建议价格重新上架。`)) return;
btn.disabled = true;
progressWrap.style.display = '';
progressBar.style.width = '0%';
let success = 0, failed = 0;
const total = overpricedOrders.length;
for (let i = 0; i < overpricedOrders.length; i++) {
const order = overpricedOrders[i];
const check = checkResults.get(order.buyerPrice);
const suggestedCents = check.suggestedCents;
statusEl.textContent = `重新上架 ${i + 1}/${total}: 下架中...`;
statusEl.className = 'sme-delist-status';
progressBar.style.width = Math.round((i / total) * 100) + '%';
try {
// 步骤1: 下架
const removeResp = await fetch(`https://steamcommunity.com/market/removelisting/${order.listingid}`, {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: `sessionid=${encodeURIComponent(sessionid)}`,
});
if (!removeResp.ok) {
failed++;
W('Relist: remove failed:', order.listingid, 'HTTP', removeResp.status);
continue;
}
// 从内存列表中移除
const idx = allSellOrders.indexOf(order);
if (idx !== -1) allSellOrders.splice(idx, 1);
// 步骤2: 等待物品返回库存(1.5-2.5秒)
statusEl.textContent = `重新上架 ${i + 1}/${total}: 等待物品返回库存...`;
await sleep(1500 + Math.random() * 1000);
// 步骤3: 以建议价格重新上架
statusEl.textContent = `重新上架 ${i + 1}/${total}: 上架中 (${formatCents(priceIncludingFees(suggestedCents))})...`;
const assetInfo = getAssetInfoFromOrder(order);
if (!assetInfo.assetid) {
W('Relist: no assetid for order', order.listingid);
failed++;
continue;
}
try {
await sellItem({
assetid: assetInfo.assetid,
appid: assetInfo.appid,
contextid: assetInfo.contextid,
priceCents: suggestedCents,
});
success++;
} catch (sellErr) {
W('Relist: sell failed:', sellErr.message);
failed++;
}
} catch (err) {
W('Relist error:', order.listingid, err);
failed++;
}
// 请求间隔
if (i < overpricedOrders.length - 1) await sleep(800);
}
progressBar.style.width = '100%';
statusEl.textContent = `重新上架完成: 成功 ${success} 件` + (failed > 0 ? `, 失败 ${failed} 件` : '');
statusEl.className = 'sme-delist-status' + (failed > 0 ? ' sme-error' : ' sme-ok');
toast(`重新上架完成: 成功 ${success} 件` + (failed > 0 ? `, 失败 ${failed} 件` : ''), failed > 0 ? 'warning' : 'success');
setTimeout(() => { progressWrap.style.display = 'none'; }, 3000);
// 刷新汇总
priceCheckResults = null;
setTimeout(() => switchTab('summary'), 500);
}
// ==================== 渲染:价格历史 ====================
function renderPriceHistory(container) {
if (!listingInfo) {
container.appendChild(makeEmpty('无法获取物品信息'));
return;
}
container.appendChild(makeLoading('正在加载价格历史...'));
const range = settings.get('historyRange');
fetchPriceHistory(listingInfo.appid, listingInfo.marketHashName)
.then(data => {
priceHistoryData = data;
container.innerHTML = '';
renderPriceHistoryContent(container, data, range);
})
.catch(err => {
container.innerHTML = '';
container.appendChild(makeError('价格历史加载失败: ' + err.message));
});
}
function renderPriceHistoryContent(container, data, range) {
const now = Date.now();
const rangeMs = { '7d': 7 * 864e5, '30d': 30 * 864e5, '90d': 90 * 864e5, all: Infinity };
const cutoff = now - (rangeMs[range] || rangeMs['30d']);
let prices = data.prices.filter(p => p.t >= cutoff);
if (prices.length === 0) {
container.appendChild(makeEmpty('所选时间范围内无价格数据'));
return;
}
// 时间范围选择器
const rangeBar = h('div', { class: 'sme-range-bar' });
for (const [label, val] of [['7天', '7d'], ['30天', '30d'], ['90天', '90d'], ['全部', 'all']]) {
rangeBar.appendChild(h('button', {
class: 'sme-range-btn' + (val === range ? ' active' : ''),
text: label,
onClick: () => {
settings.set('historyRange', val);
container.innerHTML = '';
renderPriceHistoryContent(container, data, val);
},
}));
}
container.appendChild(rangeBar);
// 统计卡片
const pricesOnly = prices.map(p => p.p);
const current = pricesOnly[pricesOnly.length - 1];
const min = Math.min(...pricesOnly);
const max = Math.max(...pricesOnly);
const avg = pricesOnly.reduce((s, v) => s + v, 0) / pricesOnly.length;
const totalVol = prices.reduce((s, p) => s + p.v, 0);
const prefix = data.prefix || '';
const trendIcon = current >= avg ? ICONS.trendingUp : ICONS.trendingDown;
const trendColor = current >= avg ? 'var(--sme-green)' : 'var(--sme-red)';
container.appendChild(h('div', { class: 'sme-stats-grid' }, [
makeStatCard(prefix + current.toFixed(2), '当前价格'),
makeStatCard(prefix + min.toFixed(2), '最低价格'),
makeStatCard(prefix + max.toFixed(2), '最高价格'),
makeStatCard(prefix + avg.toFixed(2), '平均价格'),
makeStatCard(totalVol.toLocaleString(), '总成交量'),
]));
// 图表
const chartWrap = h('div', { class: 'sme-chart-wrap' });
chartWrap.innerHTML = buildPriceChartSVG(prices, prefix);
container.appendChild(chartWrap);
}
// ==================== 渲染:订单深度 ====================
function renderOrderBook(container) {
container.appendChild(makeLoading('正在获取订单深度...'));
fetchOrderBook()
.then(data => {
orderBookData = data;
container.innerHTML = '';
renderOrderBookContent(container, data);
})
.catch(err => {
container.innerHTML = '';
container.appendChild(makeError('订单深度加载失败: ' + err.message));
});
}
function renderOrderBookContent(container, data) {
const prefix = data.prefix || '';
const suffix = data.suffix || '';
// fmtPrice: 将分(cents)转换为货币单位显示
// data.lowestSell/highestBuy 是分字符串(如 "207000"),需 / 100 转为货币单位
const fmtPrice = (val) => {
const n = parseFloat(val);
if (isNaN(n)) return val;
return prefix + (n / 100).toFixed(2) + suffix;
};
// 价差统计(lowestSell/highestBuy 均为分)
const highestBuy = parseInt(data.highestBuy, 10) || 0;
const lowestSell = parseInt(data.lowestSell, 10) || 0;
const spread = lowestSell - highestBuy;
const spreadPct = lowestSell > 0 ? (spread / lowestSell * 100).toFixed(2) : '0.00';
container.appendChild(h('div', { class: 'sme-stats-grid' }, [
makeStatCard(fmtPrice(data.lowestSell), '最低卖单'),
makeStatCard(fmtPrice(data.highestBuy), '最高买单'),
makeStatCard(fmtPrice(spread), '价差'),
makeStatCard(spreadPct + '%', '价差比'),
]));
// 卖单列表
const sellOrders = (data.sellOrders || []).slice(0, 15);
const buyOrders = (data.buyOrders || []).slice(0, 15);
const maxQty = Math.max(...sellOrders.map(o => o.qty), ...buyOrders.map(o => o.qty), 1);
if (sellOrders.length > 0) {
const sellSection = h('div', { class: 'sme-order-section' }, [
h('div', { class: 'sme-order-title sell', html: ICONS.trendingDown + '卖单 (Sell Orders)' }),
]);
for (const o of sellOrders) {
const widthPct = (o.qty / maxQty * 100).toFixed(1);
sellSection.appendChild(h('div', { class: 'sme-order-row sell' }, [
h('div', { class: 'sme-order-bar', style: { width: widthPct + '%' } }),
h('span', { class: 'sme-order-price', text: o.price }),
h('span', { class: 'sme-order-qty', text: o.qty.toLocaleString() }),
]));
}
container.appendChild(sellSection);
}
// 价差指示器
if (spread > 0) {
container.appendChild(h('div', { class: 'sme-spread-box' }, [
h('span', { class: 'sme-spread-lbl', text: '价差' }),
h('span', { class: 'sme-spread-val', text: fmtPrice(spread) + ' (' + spreadPct + '%)' }),
]));
}
// 买单列表
if (buyOrders.length > 0) {
const buySection = h('div', { class: 'sme-order-section' }, [
h('div', { class: 'sme-order-title buy', html: ICONS.trendingUp + '买单 (Buy Orders)' }),
]);
for (const o of buyOrders) {
const widthPct = (o.qty / maxQty * 100).toFixed(1);
buySection.appendChild(h('div', { class: 'sme-order-row buy' }, [
h('div', { class: 'sme-order-bar', style: { width: widthPct + '%' } }),
h('span', { class: 'sme-order-price', text: o.price }),
h('span', { class: 'sme-order-qty', text: o.qty.toLocaleString() }),
]));
}
container.appendChild(buySection);
}
if (sellOrders.length === 0 && buyOrders.length === 0) {
container.appendChild(makeEmpty('暂无订单数据'));
}
}
// ==================== 渲染:设置 ====================
function renderSettings(container) {
// --- 基础设置 ---
container.appendChild(h('div', { class: 'sme-setting-group-title', text: '基础设置' }));
container.appendChild(h('div', { class: 'sme-setting-row' }, [
h('div', { class: 'sme-setting-info' }, [
h('div', { class: 'sme-setting-name', text: '已拥有物品标识' }),
h('div', { class: 'sme-setting-desc', text: '在市场浏览页和详情页显示库存中已拥有的物品数量' }),
]),
makeToggle('ownedBadges', settings.get('ownedBadges'), val => settings.set('ownedBadges', val)),
]));
container.appendChild(h('div', { class: 'sme-setting-row' }, [
h('div', { class: 'sme-setting-info' }, [
h('div', { class: 'sme-setting-name', text: '自动加载价格历史' }),
h('div', { class: 'sme-setting-desc', text: '进入物品详情页时自动加载价格趋势图' }),
]),
makeToggle('autoLoadHistory', settings.get('autoLoadHistory'), val => settings.set('autoLoadHistory', val)),
]));
container.appendChild(h('div', { class: 'sme-setting-row' }, [
h('div', { class: 'sme-setting-info' }, [
h('div', { class: 'sme-setting-name', text: '默认价格历史范围' }),
h('div', { class: 'sme-setting-desc', text: '价格趋势图默认显示的时间范围' }),
]),
h('select', {
class: 'sme-setting-select',
html: [['7天', '7d'], ['30天', '30d'], ['90天', '90d'], ['全部', 'all']]
.map(([l, v]) => ``).join(''),
onChange: e => settings.set('historyRange', e.target.value),
}),
]));
// --- 价格检测设置 ---
container.appendChild(h('div', { class: 'sme-setting-group-title', html: ICONS.dollarSign + '价格检测设置' }));
container.appendChild(h('div', { class: 'sme-setting-row' }, [
h('div', { class: 'sme-setting-info' }, [
h('div', { class: 'sme-setting-name', text: '自动价格检测' }),
h('div', { class: 'sme-setting-desc', text: '进入上架汇总页时自动检测超价/低价' }),
]),
makeToggle('autoCheckPrices', settings.get('autoCheckPrices'), val => settings.set('autoCheckPrices', val)),
]));
container.appendChild(h('div', { class: 'sme-setting-row' }, [
h('div', { class: 'sme-setting-info' }, [
h('div', { class: 'sme-setting-name', text: '定价算法' }),
h('div', { class: 'sme-setting-desc', text: '计算建议价格时使用的数据源策略' }),
]),
h('select', {
class: 'sme-setting-select',
html: Object.entries(ALGORITHM_NAMES)
.map(([v, l]) => ``).join(''),
onChange: e => settings.set('priceAlgorithm', parseInt(e.target.value, 10)),
}),
]));
container.appendChild(h('div', { class: 'sme-setting-row' }, [
h('div', { class: 'sme-setting-info' }, [
h('div', { class: 'sme-setting-name', text: '价格偏移' }),
h('div', { class: 'sme-setting-desc', text: '在计算价格上加减偏移量(可为负,如 -0.01 表示低于建议价1分上架)' }),
]),
h('div', { class: 'sme-setting-inline' }, [
h('input', {
type: 'number',
class: 'sme-number-input',
id: 'sme-setting-offset',
value: String(settings.get('priceOffset')),
step: '0.01',
onChange: e => settings.set('priceOffset', parseFloat(e.target.value) || 0),
}),
h('span', { style: { color: 'var(--sme-text-dim)', fontSize: '12px' }, text: '元' }),
]),
]));
container.appendChild(h('div', { class: 'sme-setting-row' }, [
h('div', { class: 'sme-setting-info' }, [
h('div', { class: 'sme-setting-name', text: '历史均价时间窗口' }),
h('div', { class: 'sme-setting-desc', text: '计算历史加权均价时回溯的小时数' }),
]),
h('div', { class: 'sme-setting-inline' }, [
h('input', {
type: 'number',
class: 'sme-number-input',
id: 'sme-setting-hours',
value: String(settings.get('historyHours')),
step: '1',
min: '1',
onChange: e => settings.set('historyHours', parseInt(e.target.value, 10) || 12),
}),
h('span', { style: { color: 'var(--sme-text-dim)', fontSize: '12px' }, text: '小时' }),
]),
]));
container.appendChild(h('div', { class: 'sme-setting-row' }, [
h('div', { class: 'sme-setting-info' }, [
h('div', { class: 'sme-setting-name', text: '智能过滤异常低价挂单' }),
h('div', { class: 'sme-setting-desc', text: '最低价挂单数量极少时使用第二低价作为参考' }),
]),
makeToggle('ignoreLowQuantity', settings.get('ignoreLowQuantity'), val => settings.set('ignoreLowQuantity', val)),
]));
container.appendChild(h('div', { class: 'sme-setting-row' }, [
h('div', { class: 'sme-setting-info' }, [
h('div', { class: 'sme-setting-name', text: '价格范围限制' }),
h('div', { class: 'sme-setting-desc', text: '建议价格将被钳制在此范围内' }),
]),
h('div', { class: 'sme-setting-inline' }, [
h('input', {
type: 'number',
class: 'sme-number-input',
id: 'sme-setting-min',
value: String(settings.get('minPrice')),
step: '0.01',
min: '0.01',
style: { width: '60px' },
onChange: e => settings.set('minPrice', parseFloat(e.target.value) || 0.03),
}),
h('span', { style: { color: 'var(--sme-text-dim)', fontSize: '12px' }, text: '~' }),
h('input', {
type: 'number',
class: 'sme-number-input',
id: 'sme-setting-max',
value: String(settings.get('maxPrice')),
step: '1',
style: { width: '60px' },
onChange: e => settings.set('maxPrice', parseFloat(e.target.value) || 999),
}),
h('span', { style: { color: 'var(--sme-text-dim)', fontSize: '12px' }, text: '元' }),
]),
]));
// --- 物品类型差异化定价 ---
container.appendChild(h('div', { class: 'sme-setting-group-title', html: ICONS.layers + '物品类型差异化定价' }));
container.appendChild(h('div', { class: 'sme-setting-row' }, [
h('div', { class: 'sme-setting-info' }, [
h('div', { class: 'sme-setting-name', text: '普通卡牌价格范围' }),
h('div', { class: 'sme-setting-desc', text: '普通交易卡牌的最低/最高上架价格' }),
]),
h('div', { class: 'sme-setting-inline' }, [
h('input', { type: 'number', class: 'sme-number-input', value: String(settings.get('minNormalCardPrice')), step: '0.01', style: { width: '60px' }, onChange: e => settings.set('minNormalCardPrice', parseFloat(e.target.value) || 0.05) }),
h('span', { style: { color: 'var(--sme-text-dim)', fontSize: '12px' }, text: '~' }),
h('input', { type: 'number', class: 'sme-number-input', value: String(settings.get('maxNormalCardPrice')), step: '0.5', style: { width: '60px' }, onChange: e => settings.set('maxNormalCardPrice', parseFloat(e.target.value) || 2.5) }),
h('span', { style: { color: 'var(--sme-text-dim)', fontSize: '12px' }, text: '元' }),
]),
]));
container.appendChild(h('div', { class: 'sme-setting-row' }, [
h('div', { class: 'sme-setting-info' }, [
h('div', { class: 'sme-setting-name', text: '闪箔卡牌价格范围' }),
h('div', { class: 'sme-setting-desc', text: '闪亮(Foil)交易卡牌的最低/最高上架价格' }),
]),
h('div', { class: 'sme-setting-inline' }, [
h('input', { type: 'number', class: 'sme-number-input', value: String(settings.get('minFoilCardPrice')), step: '0.01', style: { width: '60px' }, onChange: e => settings.set('minFoilCardPrice', parseFloat(e.target.value) || 0.15) }),
h('span', { style: { color: 'var(--sme-text-dim)', fontSize: '12px' }, text: '~' }),
h('input', { type: 'number', class: 'sme-number-input', value: String(settings.get('maxFoilCardPrice')), step: '1', style: { width: '60px' }, onChange: e => settings.set('maxFoilCardPrice', parseFloat(e.target.value) || 10) }),
h('span', { style: { color: 'var(--sme-text-dim)', fontSize: '12px' }, text: '元' }),
]),
]));
container.appendChild(h('div', { class: 'sme-setting-row' }, [
h('div', { class: 'sme-setting-info' }, [
h('div', { class: 'sme-setting-name', text: '杂项物品价格范围' }),
h('div', { class: 'sme-setting-desc', text: '非卡牌类物品的最低/最高上架价格' }),
]),
h('div', { class: 'sme-setting-inline' }, [
h('input', { type: 'number', class: 'sme-number-input', value: String(settings.get('minMiscPrice')), step: '0.01', style: { width: '60px' }, onChange: e => settings.set('minMiscPrice', parseFloat(e.target.value) || 0.05) }),
h('span', { style: { color: 'var(--sme-text-dim)', fontSize: '12px' }, text: '~' }),
h('input', { type: 'number', class: 'sme-number-input', value: String(settings.get('maxMiscPrice')), step: '1', style: { width: '60px' }, onChange: e => settings.set('maxMiscPrice', parseFloat(e.target.value) || 10) }),
h('span', { style: { color: 'var(--sme-text-dim)', fontSize: '12px' }, text: '元' }),
]),
]));
// --- 库存页设置 ---
container.appendChild(h('div', { class: 'sme-setting-group-title', html: ICONS.tag + '库存页设置' }));
container.appendChild(h('div', { class: 'sme-setting-row' }, [
h('div', { class: 'sme-setting-info' }, [
h('div', { class: 'sme-setting-name', text: '库存价格标签' }),
h('div', { class: 'sme-setting-desc', text: '在库存页物品上显示市场参考价格' }),
]),
makeToggle('inventoryPriceLabels', settings.get('inventoryPriceLabels'), val => settings.set('inventoryPriceLabels', val)),
]));
container.appendChild(h('div', { class: 'sme-setting-row' }, [
h('div', { class: 'sme-setting-info' }, [
h('div', { class: 'sme-setting-name', text: '快速出售按钮' }),
h('div', { class: 'sme-setting-desc', text: '选中物品时显示快速出售面板(最高买单/最低卖单等)' }),
]),
makeToggle('quickSellButtons', settings.get('quickSellButtons'), val => settings.set('quickSellButtons', val)),
]));
// --- 市场挂单页设置 ---
container.appendChild(h('div', { class: 'sme-setting-group-title', html: ICONS.list + '市场挂单页设置' }));
container.appendChild(h('div', { class: 'sme-setting-row' }, [
h('div', { class: 'sme-setting-info' }, [
h('div', { class: 'sme-setting-name', text: '批量操作按钮' }),
h('div', { class: 'sme-setting-desc', text: '在"我的挂单"页显示批量选择/下架/重新上架按钮' }),
]),
makeToggle('marketBatchButtons', settings.get('marketBatchButtons'), val => settings.set('marketBatchButtons', val)),
]));
container.appendChild(h('div', { class: 'sme-setting-row' }, [
h('div', { class: 'sme-setting-info' }, [
h('div', { class: 'sme-setting-name', text: '排序功能' }),
h('div', { class: 'sme-setting-desc', text: '点击表头按名称/价格/日期排序' }),
]),
makeToggle('marketSorting', settings.get('marketSorting'), val => settings.set('marketSorting', val)),
]));
container.appendChild(h('div', { class: 'sme-setting-row' }, [
h('div', { class: 'sme-setting-info' }, [
h('div', { class: 'sme-setting-name', text: '搜索过滤' }),
h('div', { class: 'sme-setting-desc', text: '在挂单列表中添加搜索框实时过滤' }),
]),
makeToggle('marketSearch', settings.get('marketSearch'), val => settings.set('marketSearch', val)),
]));
// --- 交易报价设置 ---
container.appendChild(h('div', { class: 'sme-setting-group-title', html: ICONS.exchange + '交易报价设置' }));
container.appendChild(h('div', { class: 'sme-setting-row' }, [
h('div', { class: 'sme-setting-info' }, [
h('div', { class: 'sme-setting-name', text: '交易报价价值摘要' }),
h('div', { class: 'sme-setting-desc', text: '显示双方物品清单、总价值和差额对比' }),
]),
makeToggle('tradeOfferSummary', settings.get('tradeOfferSummary'), val => settings.set('tradeOfferSummary', val)),
]));
// --- 数据管理 ---
container.appendChild(h('div', { class: 'sme-setting-group-title', text: '数据管理' }));
container.appendChild(h('div', { style: { marginTop: '12px' } }, [
h('button', { class: 'sme-danger-btn', text: '清除缓存', onClick: () => {
cache.clear();
Object.keys(invCache).forEach(k => delete invCache[k]);
Object.keys(jsonFallbackDone).forEach(k => delete jsonFallbackDone[k]);
const sid = getSteamId();
if (sid) {
clearInvCache(sid);
try { sessionStorage.removeItem(SESSION_KEY); } catch (e) {}
}
toast('缓存已清除', 'success');
}}),
]));
container.appendChild(h('div', { class: 'sme-info-box' }, [
h('div', { text: `Steam 社区市场增强 v${VERSION}` }),
h('div', { text: '功能:上架汇总分组 · 超价/低价检测 · 一键重新上架 · 批量下架 · 价格历史趋势 · 订单深度 · 已拥有标识 · 我的挂单排序搜索批量操作 · 库存页价格标签与快速出售 · 交易报价价值摘要 · 物品类型差异化定价 · 货币特定取整(2025年12月Steam变更)' }),
h('div', { text: '参考:Steam Market Listings Group + Steam Economy Enhancer + steam-friend-manager 设计语言' }),
]));
}
function makeToggle(key, checked, onChange) {
const input = h('input', { type: 'checkbox' });
if (checked) input.setAttribute('checked', '');
input.addEventListener('change', () => onChange(input.checked));
return h('label', { class: 'sme-toggle' }, [
input,
h('span', { class: 'sme-toggle-slider' }),
]);
}
// ==================== 辅助:加载/空/错误状态 ====================
function makeLoading(msg) {
return h('div', { class: 'sme-loading' }, [
h('div', { class: 'sme-spinner' }),
h('div', { class: 'sme-loading-text', text: msg || '加载中...' }),
]);
}
function makeEmpty(msg) {
return h('div', { class: 'sme-empty' }, [
h('span', { html: ICONS.package }),
h('span', { text: msg || '暂无数据' }),
]);
}
function makeError(msg) {
return h('div', { class: 'sme-error-box' }, [
h('span', { html: ICONS.alert }),
h('span', { text: msg || '加载失败' }),
]);
}
// ==================== 面板注入(详情页) ====================
function findListingContainer() {
const priceEls = [];
const walker = document.createTreeWalker(document.body, 4, null);
let count = 0;
while (walker.nextNode() && count < 5) {
if (/¥\s*\d/.test(walker.currentNode.nodeValue)) {
priceEls.push(walker.currentNode.parentElement);
count++;
}
}
if (priceEls.length === 0) return null;
let common = priceEls[0].parentElement;
while (common && common !== document.body) {
if (priceEls.every(el => common.contains(el)) && common.children.length > 2) return common;
common = common.parentElement;
}
return priceEls[0].parentElement?.parentElement?.parentElement || null;
}
function injectPanel() {
if (document.getElementById('sme-panel')) return;
const panel = createPanel();
const listingContainer = findListingContainer();
if (listingContainer && listingContainer.parentNode) {
listingContainer.parentNode.insertBefore(panel, listingContainer);
} else {
const target = document.querySelector('#mainContents') || document.querySelector('[class*="pagecontent"]') || document.body;
target.insertBefore(panel, target.firstChild);
}
switchTab('summary');
L('Panel injected on listing page');
}
// ==================== 详情页:已拥有物品标识 ====================
/** 在详情页物品名称前显示已拥有数量徽章 */
function processDetailPageBadge() {
if (!settings.get('ownedBadges')) return;
const info = getListingInfo();
if (!info) return;
// 尝试多个可能的物品名称位置
const nameEl = document.querySelector('#largeiteminfo .market_listing_nav .market_listing_item_name')
|| document.querySelector('.market_listing_nav .market_listing_item_name')
|| document.querySelector('.market_listing_item_name:first-of-type');
if (!nameEl || nameEl.querySelector('.sme-owned-badge')) return;
getOwnedCount(info.appid, info.marketHashName).then(count => {
if (count > 0 && nameEl.parentNode && !nameEl.querySelector('.sme-owned-badge')) {
nameEl.parentNode.insertBefore(
h('span', { class: 'sme-owned-badge', title: `您库存中有 ${count} 件此物品`, text: '×' + count }),
nameEl
);
}
}).catch(() => {});
}
// ==================== 浏览页:已拥有物品标识 ====================
const browseSeen = new WeakSet();
let browseObserver = null;
function processBrowseRow(row) {
if (browseSeen.has(row)) return;
browseSeen.add(row);
if (row.querySelector('.sme-owned-badge')) return;
const href = row.href || row.getAttribute('href');
if (!href) return;
const m = href.match(/\/market\/listings\/(\d+)\/(.+?)(?:\?.*)?$/);
if (!m) return;
let appid, hashName;
try { appid = m[1]; hashName = decodeURIComponent(m[2]); } catch (e) { return; }
const nameBlock = row.querySelector('.market_listing_item_name_block');
if (!nameBlock) return;
const nameSpan = nameBlock.querySelector('.market_listing_item_name');
if (!nameSpan) return;
getOwnedCount(appid, hashName).then(count => {
if (count > 0) {
nameBlock.insertBefore(
h('span', { class: 'sme-owned-badge', title: `您库存中有 ${count} 件此物品`, text: '×' + count }),
nameSpan
);
}
}).catch(() => {});
}
function processAllBrowseRows() {
const rows = document.querySelectorAll('a.market_listing_row_link');
rows.forEach(r => processBrowseRow(r));
}
function setupBrowseObserver() {
if (browseObserver) browseObserver.disconnect();
const target = document.getElementById('searchResults') || document.body;
browseObserver = new MutationObserver(mutations => {
const rows = [];
mutations.forEach(mut => {
if (mut.type !== 'childList') return;
mut.addedNodes.forEach(node => {
if (node.nodeType !== 1) return;
if (node.classList && node.classList.contains('market_listing_row_link')) rows.push(node);
if (node.querySelectorAll) {
node.querySelectorAll('a.market_listing_row_link').forEach(r => rows.push(r));
}
});
});
if (rows.length > 0) rows.forEach(r => processBrowseRow(r));
});
browseObserver.observe(target, { childList: true, subtree: true });
}
// ==================== 浏览页:浮动设置按钮 ====================
let fabEl = null, fabPopup = null;
function createFab() {
if (fabEl) return;
fabEl = h('button', { class: 'sme-fab', title: '市场增强设置', html: ICONS.settings, onClick: toggleFabPopup });
document.body.appendChild(fabEl);
fabPopup = h('div', { class: 'sme-fab-popup' }, [
h('h4', { html: ICONS.settings + '市场增强设置' }),
h('div', { class: 'sme-setting-row' }, [
h('div', { class: 'sme-setting-info' }, [
h('div', { class: 'sme-setting-name', text: '已拥有物品标识' }),
h('div', { class: 'sme-setting-desc', text: '浏览页和详情页显示库存中已拥有的物品数量' }),
]),
makeToggle('ownedBadges', settings.get('ownedBadges'), val => {
settings.set('ownedBadges', val);
if (val) {
processAllBrowseRows();
processDetailPageBadge();
} else {
document.querySelectorAll('.sme-owned-badge').forEach(el => el.remove());
}
}),
]),
h('div', { class: 'sme-setting-row' }, [
h('div', { class: 'sme-setting-info' }, [
h('div', { class: 'sme-setting-name', text: '自动加载价格历史' }),
h('div', { class: 'sme-setting-desc', text: '详情页自动加载价格趋势图' }),
]),
makeToggle('autoLoadHistory', settings.get('autoLoadHistory'), val => settings.set('autoLoadHistory', val)),
]),
h('div', { style: { marginTop: '12px', fontSize: '11px', color: 'var(--sme-text-dim)', textAlign: 'center' }, text: `Steam 社区市场增强 v${VERSION}` }),
]);
document.body.appendChild(fabPopup);
document.addEventListener('mousedown', e => {
if (fabPopup.classList.contains('sme-show') && !fabPopup.contains(e.target) && !fabEl.contains(e.target)) {
fabPopup.classList.remove('sme-show');
}
}, true);
}
function toggleFabPopup() {
fabPopup.classList.toggle('sme-show');
}
// ==================== 我的挂单增强(批量选择 / 排序 / 搜索) ====================
// 为市场主页"我的挂单"区域提供:批量选择与下架、超价检测与重新上架、列排序、名称搜索。
// 依赖现有工具函数:h, settings, toast, getSessionId, parsePriceToCents, priceBeforeFees,
// priceIncludingFees, calcSellPrice, fetchPriceHistory, fetchOrderHistogram, sellItem,
// isRetryMessage, randInt, sleep, ICONS, GM_addStyle, unsafeWindow, APP_CONTEXT,
// INV_REQUEST_DELAY, RETRY_BACKOFF_MIN, RETRY_BACKOFF_MAX, L, W。
let myListingsModuleActive = false; // 模块是否已激活(防止重复初始化)
let myListingsStyleAdded = false; // CSS 是否已注入
let myListingsObserver = null; // 挂单区域变动观察器
let myListingsToolbarEl = null; // 工具栏元素
let myListingsSortKey = null; // 当前排序键: 'name' | 'date' | 'price' | null
let myListingsSortAsc = true; // 升序/降序
let myListingsSearchQ = ''; // 搜索关键词(小写)
let myListingsBusy = false; // 是否有批量操作进行中
const myListingsRowData = new WeakMap(); // 行元素 → 解析数据
const myListingsPriceCheck = new Map(); // listingid → { overpriced, suggestedCents, source, buyerCents, unknown? }
const myListItemNameIdCache = new Map(); // "appid|hash" → item_nameid
const MY_LISTINGS_CSS = `
.sme-ml-toolbar{
display:flex;align-items:center;gap:6px;flex-wrap:wrap;padding:8px 10px;margin:6px 0;
background:linear-gradient(135deg,rgba(102,192,244,0.06) 0%,rgba(23,26,33,0.6) 100%);
border:1px solid var(--sme-border);border-radius:var(--sme-radius-sm);
font-family:"Motiva Sans",system-ui,-apple-system,sans-serif;
}
.sme-ml-toolbar .sme-market-btn:disabled{opacity:0.4;cursor:not-allowed}
.sme-ml-sep{width:1px;height:18px;background:var(--sme-border);margin:0 2px;display:inline-block}
.sme-ml-count{font-size:12px;color:var(--sme-text-dim);white-space:nowrap;margin-left:auto}
.sme-ml-count b{color:var(--sme-blue)}
.sme-ml-status{width:100%;font-size:12px;color:var(--sme-text-dim);margin-top:6px;min-height:14px}
.sme-ml-status.sme-err{color:var(--sme-red)}
.sme-ml-status.sme-ok{color:var(--sme-green)}
.sme-ml-progress{width:100%;height:3px;background:rgba(255,255,255,0.06);border-radius:2px;margin-top:6px;overflow:hidden;display:none}
.sme-ml-progress-bar{height:100%;width:0;background:linear-gradient(90deg,var(--sme-blue),var(--sme-green));border-radius:2px;transition:width .3s ease}
.sme-ml-check{margin:0 6px 0 2px;cursor:pointer;accent-color:var(--sme-blue);vertical-align:middle}
.market_listing_row.sme-ml-overpriced{box-shadow:inset 3px 0 0 var(--sme-red)}
.market_listing_row.sme-ml-overpriced .market_listing_my_price{background:rgba(226,74,74,0.08)}
.sme-ml-sortable{cursor:pointer;user-select:none}
.sme-ml-sortable:hover{color:var(--sme-blue)}
.sme-ml-sortarrow{font-size:10px;color:var(--sme-blue);margin-left:3px;opacity:0}
.sme-ml-sortable.sme-active .sme-ml-sortarrow{opacity:1}
`;
/** 入口:在市场主页"我的挂单"区域启用增强功能 */
function initMyListingsEnhancement() {
if (myListingsModuleActive) return;
const enableBatch = settings.get('marketBatchButtons');
const enableSort = settings.get('marketSorting');
const enableSearch = settings.get('marketSearch');
if (!enableBatch && !enableSort && !enableSearch) return;
if (!myListingsStyleAdded) {
GM_addStyle(MY_LISTINGS_CSS);
myListingsStyleAdded = true;
}
// "我的挂单"区域为 AJAX 异步加载,轮询等待其出现
let attempts = 0;
const tryStart = () => {
const container = document.getElementById('my_market_selllistings');
if (container && container.querySelector('.market_listing_row')) {
startMyListingsModule(container);
} else if (attempts++ < 30) {
setTimeout(tryStart, 500);
}
};
tryStart();
}
function startMyListingsModule(container) {
if (myListingsModuleActive) return;
myListingsModuleActive = true;
L('My Listings enhancement module active');
injectMyListingsToolbar(container);
setupMyListingsSortHeaders(container);
processMyListingsRows(container);
setupMyListingsObserver(container);
}
/** 清理模块状态(URL 变化时调用) */
function cleanupMyListingsEnhancement() {
if (myListingsObserver) { myListingsObserver.disconnect(); myListingsObserver = null; }
if (myListingsToolbarEl) { myListingsToolbarEl.remove(); myListingsToolbarEl = null; }
myListingsModuleActive = false;
myListingsSortKey = null;
myListingsSortAsc = true;
myListingsSearchQ = '';
myListingsBusy = false;
myListingsPriceCheck.clear();
myListItemNameIdCache.clear();
}
// ---------- 工具栏 ----------
function injectMyListingsToolbar(container) {
if (myListingsToolbarEl && document.body.contains(myListingsToolbarEl)) {
updateMyListingsCount();
return;
}
const enableBatch = settings.get('marketBatchButtons');
const enableSearch = settings.get('marketSearch');
const controls = [];
if (enableBatch) {
controls.push(
h('button', { class: 'sme-market-btn', html: ICONS.selectAll + '全选', onClick: onSelectAllMyListings }),
h('button', { class: 'sme-market-btn', text: '选5个', onClick: () => selectNMyListings(5) }),
h('button', { class: 'sme-market-btn', text: '选25个', onClick: () => selectNMyListings(25) }),
h('button', { class: 'sme-market-btn', html: ICONS.alert + '选超价', onClick: onSelectOverpricedMyListings }),
h('span', { class: 'sme-ml-sep' }),
h('button', { class: 'sme-market-btn sme-danger', html: ICONS.trash + '批量下架', onClick: onRemoveSelectedMyListings }),
h('button', { class: 'sme-market-btn', html: ICONS.refreshCw + '超价重挂', onClick: onRelistOverpricedMyListings }),
);
}
if (enableSearch) {
controls.push(
h('span', { html: ICONS.search, style: { color: 'var(--sme-text-dim)', display: 'inline-flex', alignItems: 'center' } }),
h('input', {
class: 'sme-market-search', type: 'text', placeholder: '搜索物品名称…', value: myListingsSearchQ,
oninput: (e) => {
myListingsSearchQ = e.target.value.trim().toLowerCase();
const c = document.getElementById('my_market_selllistings');
if (c) applyMyListingsSortAndFilter(c);
},
}),
);
}
controls.push(h('span', { class: 'sme-ml-count', id: 'sme-ml-count' }));
myListingsToolbarEl = h('div', { class: 'sme-ml-toolbar' }, [
h('div', { style: { display: 'flex', alignItems: 'center', gap: '6px', flexWrap: 'wrap', width: '100%' } }, controls),
h('div', { class: 'sme-ml-status', id: 'sme-ml-status' }),
h('div', { class: 'sme-ml-progress', id: 'sme-ml-progress' }, [
h('div', { class: 'sme-ml-progress-bar', id: 'sme-ml-progress-bar' }),
]),
]);
container.parentNode.insertBefore(myListingsToolbarEl, container);
updateMyListingsCount();
}
function getMyListingsStatusEls() {
return {
statusEl: document.getElementById('sme-ml-status'),
progressEl: document.getElementById('sme-ml-progress'),
progressBarEl: document.getElementById('sme-ml-progress-bar'),
};
}
function setMyListingsStatus(el, msg, cls) {
if (!el) return;
el.textContent = msg || '';
el.className = 'sme-ml-status' + (cls === 'err' ? ' sme-err' : (cls === 'ok' ? ' sme-ok' : ''));
}
function showMyListingsProgress(el, show) { if (el) el.style.display = show ? '' : 'none'; }
function setMyListingsProgress(el, ratio) { if (el) el.style.width = Math.max(0, Math.min(1, ratio)) * 100 + '%'; }
function setMyListingsButtonsDisabled(disabled) {
if (!myListingsToolbarEl) return;
myListingsToolbarEl.querySelectorAll('button.sme-market-btn').forEach(b => { b.disabled = disabled; });
}
function updateMyListingsCount() {
const el = document.getElementById('sme-ml-count');
if (!el) return;
const c = document.getElementById('my_market_selllistings');
if (!c) return;
let total = 0, selected = 0, visible = 0;
c.querySelectorAll('.market_listing_row').forEach(r => {
const d = myListingsRowData.get(r);
if (!d) return;
total++;
if (r.style.display !== 'none') visible++;
if (d.checked) selected++;
});
el.innerHTML = `已选 ${selected} / 显示 ${visible} / 共 ${total}`;
}
// ---------- 行解析与复选框 ----------
function parseMyListingRow(row) {
const m = (row.id || '').match(/^mylisting_(\d+)$/);
if (!m) return null;
const listingid = m[1];
const nameEl = row.querySelector('.market_listing_item_name');
const name = nameEl ? nameEl.textContent.trim() : '';
const dateEl = row.querySelector('.market_listing_listed_date');
const dateText = dateEl ? dateEl.textContent.trim() : '';
const { buyerCents, sellerCents } = extractMyListingPrices(row);
return { row, listingid, name, dateText, dateTs: parseMyListingDate(dateText), buyerCents, sellerCents, checked: false };
}
function extractMyListingPrices(row) {
let buyerCents = 0, sellerCents = 0;
const sellerRe = /你将收到|You will receive|Sie erhalten|Вы получите|receberá|会收到/i;
row.querySelectorAll('.market_listing_price').forEach(el => {
const txt = el.textContent || '';
const cents = parsePriceToCents(txt);
if (sellerRe.test(txt)) { if (!sellerCents) sellerCents = cents; }
else { if (!buyerCents) buyerCents = cents; }
});
if (buyerCents && !sellerCents) sellerCents = priceBeforeFees(buyerCents);
if (sellerCents && !buyerCents) buyerCents = priceIncludingFees(sellerCents);
return { buyerCents, sellerCents };
}
function parseMyListingDate(text) {
if (!text) return 0;
const t = text.trim();
let d = new Date(t);
if (isNaN(d.getTime())) d = new Date(t + ' ' + new Date().getFullYear());
if (isNaN(d.getTime())) return 0;
// 仅含月日时,若解析到未来超过 7 天,视为去年
if (d.getTime() - Date.now() > 7 * 24 * 3600 * 1000) d.setFullYear(d.getFullYear() - 1);
return d.getTime();
}
function injectMyListingCheckbox(row, data) {
if (row.querySelector('.sme-ml-check')) return;
const cb = h('input', { type: 'checkbox', class: 'sme-ml-check', title: '选择此挂单' });
cb.addEventListener('change', () => { data.checked = cb.checked; updateMyListingsCount(); });
const area = row.querySelector('.market_listing_cancel_button') || row.querySelector('.market_listing_cancel');
if (area) area.insertBefore(cb, area.firstChild);
else row.insertBefore(cb, row.firstChild);
}
function processMyListingsRows(container) {
let fresh = false;
container.querySelectorAll('.market_listing_row').forEach(row => {
if (myListingsRowData.has(row)) return;
const data = parseMyListingRow(row);
if (!data) return;
myListingsRowData.set(row, data);
if (settings.get('marketBatchButtons')) injectMyListingCheckbox(row, data);
fresh = true;
});
if (fresh) {
applyMyListingsSortAndFilter(container);
updateMyListingsCount();
}
}
// ---------- 排序与搜索 ----------
function applyMyListingsSortAndFilter(container) {
const rows = Array.from(container.querySelectorAll('.market_listing_row'));
if (rows.length === 0) return;
if (myListingsSortKey) {
rows.sort((a, b) => {
const da = myListingsRowData.get(a), db = myListingsRowData.get(b);
if (!da || !db) return 0;
let cmp = 0;
if (myListingsSortKey === 'name') cmp = da.name.localeCompare(db.name, 'zh');
else if (myListingsSortKey === 'date') cmp = (da.dateTs || 0) - (db.dateTs || 0);
else if (myListingsSortKey === 'price') cmp = (da.buyerCents || 0) - (db.buyerCents || 0);
return myListingsSortAsc ? cmp : -cmp;
});
reorderMyListingRows(rows);
}
const q = myListingsSearchQ;
rows.forEach(row => {
const data = myListingsRowData.get(row);
const match = !q || (data && data.name.toLowerCase().indexOf(q) !== -1);
row.style.display = match ? '' : 'none';
});
updateMyListingsCount();
}
function reorderMyListingRows(sortedRows) {
if (sortedRows.length === 0) return;
const parent = sortedRows[0].parentNode;
const placeholder = document.createComment('sme-ml-sort');
parent.insertBefore(placeholder, sortedRows[0]);
// 依次把已排序行插入占位符之前,最终顺序即为排序顺序;表头/分页等非行元素保持原位
for (const row of sortedRows) parent.insertBefore(row, placeholder);
placeholder.remove();
}
function setupMyListingsSortHeaders(container) {
if (!settings.get('marketSorting')) return;
const header = container.querySelector('.market_listing_table_header');
if (!header || header.dataset.smeMlSort) return;
header.dataset.smeMlSort = '1';
const nameRe = /(物品|名称|商品|item|name)/i;
const priceRe = /(价格|价钱|price)/i;
const dateRe = /(时间|日期|挂单|listed|date)/i;
Array.from(header.children).forEach(cell => {
const txt = cell.textContent || '';
let key = null;
if (nameRe.test(txt)) key = 'name';
else if (priceRe.test(txt)) key = 'price';
else if (dateRe.test(txt)) key = 'date';
if (!key) return;
cell.classList.add('sme-ml-sortable');
cell.dataset.smeMlKey = key;
cell.appendChild(h('span', { class: 'sme-ml-sortarrow', text: '▼' }));
cell.addEventListener('click', () => {
if (myListingsSortKey === key) myListingsSortAsc = !myListingsSortAsc;
else { myListingsSortKey = key; myListingsSortAsc = true; }
refreshMyListingsSortHeaders(header);
const c = document.getElementById('my_market_selllistings');
if (c) applyMyListingsSortAndFilter(c);
});
});
refreshMyListingsSortHeaders(header);
}
function refreshMyListingsSortHeaders(header) {
header.querySelectorAll('.sme-ml-sortable').forEach(cell => {
const key = cell.dataset.smeMlKey;
const active = key && myListingsSortKey === key;
cell.classList.toggle('sme-active', active);
const arrow = cell.querySelector('.sme-ml-sortarrow');
if (arrow) arrow.textContent = active ? (myListingsSortAsc ? '▲' : '▼') : '▼';
});
}
function setupMyListingsObserver(container) {
if (myListingsObserver) myListingsObserver.disconnect();
let debounce = null;
myListingsObserver = new MutationObserver(() => {
clearTimeout(debounce);
debounce = setTimeout(() => processMyListingsRows(container), 200);
});
myListingsObserver.observe(container, { childList: true, subtree: true });
}
// ---------- 选择操作 ----------
function getVisibleMyListingRows() {
const c = document.getElementById('my_market_selllistings');
if (!c) return [];
return Array.from(c.querySelectorAll('.market_listing_row')).filter(r => {
const d = myListingsRowData.get(r);
return d && r.style.display !== 'none';
});
}
function setMyListingChecked(row, data, checked) {
if (!data) return;
data.checked = checked;
const cb = row.querySelector('.sme-ml-check');
if (cb) cb.checked = checked;
}
function onSelectAllMyListings() {
const rows = getVisibleMyListingRows();
const allChecked = rows.length > 0 && rows.every(r => myListingsRowData.get(r).checked);
rows.forEach(r => setMyListingChecked(r, myListingsRowData.get(r), !allChecked));
updateMyListingsCount();
}
function selectNMyListings(n) {
const c = document.getElementById('my_market_selllistings');
if (c) c.querySelectorAll('.market_listing_row').forEach(r => {
const d = myListingsRowData.get(r);
if (d) setMyListingChecked(r, d, false);
});
getVisibleMyListingRows().slice(0, n).forEach(r => setMyListingChecked(r, myListingsRowData.get(r), true));
updateMyListingsCount();
}
// ---------- 资产信息提取 ----------
function getMyListingAssetInfo(listingid) {
let info = null;
try {
const ml = unsafeWindow.g_oMyListings;
if (ml) info = (ml.m_rgListingInfo && ml.m_rgListingInfo[listingid]) || (ml.m_rgListings && ml.m_rgListings[listingid]) || null;
} catch (e) {}
if (!info) return null;
const asset = info.asset || info.rgAsset || info;
const appid = String(asset.appid || info.appid || '');
const contextid = String(asset.contextid || info.contextid || APP_CONTEXT[appid] || '');
const assetid = String(asset.assetid || asset.id || info.assetid || '');
let marketHashName = '';
if (appid && contextid && assetid) {
try {
const ctx = unsafeWindow.g_rgAssets && unsafeWindow.g_rgAssets[appid] && unsafeWindow.g_rgAssets[appid][contextid];
const a = ctx && ctx[assetid];
if (a) {
const desc = a.description || a;
marketHashName = desc.market_hash_name || desc.market_name || desc.name || '';
}
} catch (e) {}
}
if (!marketHashName) {
const desc = asset.description || info.description;
if (desc) marketHashName = desc.market_hash_name || desc.market_name || desc.name || '';
}
if (!marketHashName) marketHashName = asset.market_hash_name || info.market_hash_name || '';
return { appid, contextid, assetid, marketHashName };
}
// ---------- 带限流重试的请求包装 ----------
async function withMarketRetry(fn, label, maxAttempts) {
if (maxAttempts === undefined) maxAttempts = 3;
let lastErr;
for (let attempt = 0; attempt < maxAttempts; attempt++) {
try { return await fn(); }
catch (err) {
lastErr = err;
const msg = err && err.message ? err.message : String(err);
const isRate = msg === 'RateLimited' || /429/i.test(msg);
if ((isRate || isRetryMessage(msg)) && attempt < maxAttempts - 1) {
const wait = randInt(RETRY_BACKOFF_MIN, RETRY_BACKOFF_MAX);
toast(`${label || '操作'}: 请求受限,${Math.round(wait / 1000)} 秒后重试…`, 'warning');
await sleep(wait);
continue;
}
throw err;
}
}
throw lastErr;
}
async function fetchMyListingItemNameId(appid, hash) {
const key = appid + '|' + hash;
if (myListItemNameIdCache.has(key)) return myListItemNameIdCache.get(key);
let result = null;
try {
result = await withMarketRetry(async () => {
const url = `https://steamcommunity.com/market/listings/${appid}/${encodeURIComponent(hash)}`;
const resp = await fetch(url, { credentials: 'include' });
if (resp.status === 429) throw new Error('RateLimited');
if (!resp.ok) return null;
const html = await resp.text();
let m = html.match(/Market_LoadOrderSpread\(\s*['"]?(\d+)/);
if (!m) m = html.match(/g_item_nameid\s*=\s*["']?(\d+)/);
if (!m) m = html.match(/"item_nameid"\s*:\s*"?(\d+)/);
return m ? m[1] : null;
}, 'item_nameid');
} catch (e) { /* 限流重试耗尽,返回 null */ }
myListItemNameIdCache.set(key, result);
return result;
}
async function fetchMyListingOrderBook(appid, hash) {
const itemNameId = await fetchMyListingItemNameId(appid, hash);
if (!itemNameId) return null;
await sleep(randInt(INV_REQUEST_DELAY, INV_REQUEST_DELAY + 500));
try {
return await withMarketRetry(() => fetchOrderHistogram(itemNameId), '订单簿');
} catch (e) { return null; }
}
async function computeMyListingSuggestedPrice(appid, hash) {
let priceHistory = null;
try { priceHistory = await withMarketRetry(() => fetchPriceHistory(appid, hash), '价格历史'); }
catch (e) {}
await sleep(randInt(INV_REQUEST_DELAY, INV_REQUEST_DELAY + 500));
const orderBook = await fetchMyListingOrderBook(appid, hash);
// calcSellPrice 在缺少订单簿时会退化为仅按历史均价定价
return calcSellPrice(priceHistory, orderBook, true);
}
async function runMyListingsPriceCheck(statusEl, progressEl, progressBarEl) {
const c = document.getElementById('my_market_selllistings');
if (!c) return;
const rows = Array.from(c.querySelectorAll('.market_listing_row')).filter(r => {
const d = myListingsRowData.get(r);
return d && r.style.display !== 'none';
});
// 按唯一物品分组,避免重复请求
const itemMap = new Map();
for (const row of rows) {
const d = myListingsRowData.get(row);
const ai = getMyListingAssetInfo(d.listingid);
if (!ai || !ai.appid || !ai.marketHashName) {
myListingsPriceCheck.set(d.listingid, { overpriced: false, suggestedCents: 0, source: '无物品信息', buyerCents: d.buyerCents, unknown: true });
continue;
}
d._asset = ai;
const key = ai.appid + '|' + ai.marketHashName;
if (!itemMap.has(key)) itemMap.set(key, { appid: ai.appid, hash: ai.marketHashName, listingids: [] });
itemMap.get(key).listingids.push(d.listingid);
}
const items = Array.from(itemMap.values());
const total = items.length;
if (total === 0) {
setMyListingsStatus(statusEl, '无可检测的物品(缺少物品信息)', 'err');
return;
}
showMyListingsProgress(progressEl, true);
setMyListingsProgress(progressBarEl, 0);
let done = 0;
for (const item of items) {
setMyListingsStatus(statusEl, `正在检测价格 ${done}/${total}:${item.hash.slice(0, 30)}…`, '');
try {
const suggested = await computeMyListingSuggestedPrice(item.appid, item.hash);
const suggestedBuyerCents = priceIncludingFees(suggested.priceCents);
for (const lid of item.listingids) {
const row = document.getElementById('mylisting_' + lid);
const d = row ? myListingsRowData.get(row) : null;
const buyerCents = d ? d.buyerCents : 0;
const overpriced = suggestedBuyerCents > 0 && buyerCents > suggestedBuyerCents;
myListingsPriceCheck.set(lid, { overpriced, suggestedCents: suggested.priceCents, source: suggested.source, buyerCents });
if (row) {
row.classList.toggle('sme-ml-overpriced', overpriced);
if (d) d._suggested = suggested;
}
}
} catch (err) {
W('Price check failed for', item.appid, item.hash, err && err.message);
for (const lid of item.listingids) {
myListingsPriceCheck.set(lid, { overpriced: false, suggestedCents: 0, source: '检测失败', buyerCents: 0, unknown: true });
}
}
done++;
setMyListingsProgress(progressBarEl, done / total);
setMyListingsStatus(statusEl, `正在检测价格 ${done}/${total}…`, '');
if (done < total) await sleep(randInt(INV_REQUEST_DELAY, INV_REQUEST_DELAY + 500));
}
setMyListingsStatus(statusEl, `价格检测完成,共 ${total} 种物品`, 'ok');
setTimeout(() => showMyListingsProgress(progressEl, false), 1500);
}
async function onSelectOverpricedMyListings() {
if (myListingsBusy) { toast('已有操作进行中,请稍候', 'warning'); return; }
myListingsBusy = true;
setMyListingsButtonsDisabled(true);
const { statusEl, progressEl, progressBarEl } = getMyListingsStatusEls();
try {
const c = document.getElementById('my_market_selllistings');
if (c) c.querySelectorAll('.sme-ml-overpriced').forEach(r => r.classList.remove('sme-ml-overpriced'));
myListingsPriceCheck.clear();
await runMyListingsPriceCheck(statusEl, progressEl, progressBarEl);
let count = 0;
if (c) c.querySelectorAll('.market_listing_row').forEach(r => {
const d = myListingsRowData.get(r);
if (!d) return;
const chk = myListingsPriceCheck.get(d.listingid);
const over = chk && chk.overpriced;
setMyListingChecked(r, d, over);
if (over) count++;
});
updateMyListingsCount();
toast(`检测完成,选中 ${count} 件超价物品`, count > 0 ? 'success' : 'info');
} catch (err) {
setMyListingsStatus(statusEl, '价格检测出错: ' + (err && err.message), 'err');
toast('价格检测出错: ' + (err && err.message), 'error');
} finally {
myListingsBusy = false;
setMyListingsButtonsDisabled(false);
}
}
// ---------- 下架 / 重新上架 ----------
async function removeMyListing(listingid) {
const sessionid = getSessionId();
if (!sessionid) throw new Error('无法获取会话ID');
const resp = await fetch(`https://steamcommunity.com/market/removelisting/${listingid}`, {
method: 'POST', credentials: 'include',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: `sessionid=${encodeURIComponent(sessionid)}`,
});
if (resp.status === 429) throw new Error('RateLimited');
if (!resp.ok) throw new Error('HTTP ' + resp.status);
let data = null;
try { data = await resp.json(); } catch (e) {}
if (data && data.success === false) throw new Error(data.message || '下架失败');
return true;
}
async function onRemoveSelectedMyListings() {
if (myListingsBusy) { toast('已有操作进行中,请稍候', 'warning'); return; }
const c = document.getElementById('my_market_selllistings');
if (!c) return;
const selected = Array.from(c.querySelectorAll('.market_listing_row')).filter(r => {
const d = myListingsRowData.get(r);
return d && d.checked;
});
if (selected.length === 0) { toast('未选中任何挂单', 'warning'); return; }
if (!confirm(`确定要下架选中的 ${selected.length} 件物品吗?`)) return;
myListingsBusy = true;
setMyListingsButtonsDisabled(true);
const { statusEl, progressEl, progressBarEl } = getMyListingsStatusEls();
showMyListingsProgress(progressEl, true);
let success = 0, failed = 0;
for (let i = 0; i < selected.length; i++) {
const row = selected[i];
const d = myListingsRowData.get(row);
setMyListingsStatus(statusEl, `正在下架 ${i + 1}/${selected.length}…`, '');
setMyListingsProgress(progressBarEl, i / selected.length);
try {
await withMarketRetry(() => removeMyListing(d.listingid), '下架');
success++;
row.remove();
myListingsRowData.delete(row);
myListingsPriceCheck.delete(d.listingid);
} catch (err) {
failed++;
W('下架失败:', d.listingid, err && err.message);
setMyListingChecked(row, d, false);
}
if (i < selected.length - 1) await sleep(randInt(INV_REQUEST_DELAY, INV_REQUEST_DELAY + 500));
}
setMyListingsProgress(progressBarEl, 1);
setMyListingsStatus(statusEl, `下架完成: 成功 ${success} 件` + (failed > 0 ? `,失败 ${failed} 件` : ''), failed > 0 ? 'err' : 'ok');
toast(`下架完成: 成功 ${success} 件` + (failed > 0 ? `,失败 ${failed} 件` : ''), failed > 0 ? 'warning' : 'success');
setTimeout(() => showMyListingsProgress(progressEl, false), 2000);
updateMyListingsCount();
myListingsBusy = false;
setMyListingsButtonsDisabled(false);
}
async function onRelistOverpricedMyListings() {
if (myListingsBusy) { toast('已有操作进行中,请稍候', 'warning'); return; }
const c = document.getElementById('my_market_selllistings');
if (!c) return;
const checkedRows = Array.from(c.querySelectorAll('.market_listing_row')).filter(r => {
const d = myListingsRowData.get(r);
return d && d.checked;
});
// 若尚无价格检测结果或未选中任何行,则先对全部可见挂单进行检测
const needCheck = myListingsPriceCheck.size === 0 || checkedRows.length === 0;
const relistAll = needCheck;
myListingsBusy = true;
setMyListingsButtonsDisabled(true);
const { statusEl, progressEl, progressBarEl } = getMyListingsStatusEls();
try {
if (needCheck) {
if (c) c.querySelectorAll('.sme-ml-overpriced').forEach(r => r.classList.remove('sme-ml-overpriced'));
myListingsPriceCheck.clear();
await runMyListingsPriceCheck(statusEl, progressEl, progressBarEl);
}
const rows = Array.from(c.querySelectorAll('.market_listing_row')).filter(r => {
const d = myListingsRowData.get(r);
if (!d) return false;
const chk = myListingsPriceCheck.get(d.listingid);
if (!chk || !chk.overpriced || chk.suggestedCents <= 0) return false;
return relistAll ? true : d.checked;
});
if (rows.length === 0) {
setMyListingsStatus(statusEl, '未检测到超价物品', '');
toast('未检测到超价物品', 'info');
return;
}
if (!confirm(`确定要重新上架 ${rows.length} 件超价物品吗?\n将先下架再按建议价格重新上架。`)) return;
const total = rows.length;
showMyListingsProgress(progressEl, true);
let success = 0, failed = 0;
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
const d = myListingsRowData.get(row);
const chk = myListingsPriceCheck.get(d.listingid);
const suggestedCents = chk.suggestedCents;
setMyListingsStatus(statusEl, `重新上架 ${i + 1}/${total}: 下架中…`, '');
setMyListingsProgress(progressBarEl, i / total);
try {
// 步骤1: 下架
await withMarketRetry(() => removeMyListing(d.listingid), '下架');
// 步骤2: 等待物品返回库存(1.5-2.5 秒)
setMyListingsStatus(statusEl, `重新上架 ${i + 1}/${total}: 等待物品返回库存…`, '');
await sleep(randInt(1500, 2500));
// 步骤3: 以建议价格重新上架(assetid 在下架后保持不变)
const ai = d._asset || getMyListingAssetInfo(d.listingid);
if (!ai || !ai.assetid || !ai.appid) {
W('重挂缺少资产信息:', d.listingid);
failed++;
row.remove();
myListingsRowData.delete(row);
continue;
}
setMyListingsStatus(statusEl, `重新上架 ${i + 1}/${total}: 上架中 (${formatCents(priceIncludingFees(suggestedCents))})…`, '');
await withMarketRetry(() => sellItem({
assetid: ai.assetid, appid: ai.appid, contextid: ai.contextid, priceCents: suggestedCents,
}), '上架');
success++;
row.remove();
myListingsRowData.delete(row);
myListingsPriceCheck.delete(d.listingid);
} catch (err) {
failed++;
W('重挂失败:', d.listingid, err && err.message);
setMyListingChecked(row, d, false);
}
if (i < rows.length - 1) await sleep(randInt(INV_REQUEST_DELAY, INV_REQUEST_DELAY + 500));
}
setMyListingsProgress(progressBarEl, 1);
setMyListingsStatus(statusEl, `重新上架完成: 成功 ${success} 件` + (failed > 0 ? `,失败 ${failed} 件` : ''), failed > 0 ? 'err' : 'ok');
toast(`重新上架完成: 成功 ${success} 件` + (failed > 0 ? `,失败 ${failed} 件` : ''), failed > 0 ? 'warning' : 'success');
setTimeout(() => showMyListingsProgress(progressEl, false), 3000);
updateMyListingsCount();
} catch (err) {
setMyListingsStatus(statusEl, '重新上架出错: ' + (err && err.message), 'err');
toast('重新上架出错: ' + (err && err.message), 'error');
} finally {
myListingsBusy = false;
setMyListingsButtonsDisabled(false);
}
}
// ==================== 库存页 ====================
// 库存页增强:价格标签、快速出售面板、批量操作工具栏
let invObserver = null; // MutationObserver 实例
let invToolbarEl = null; // 批量操作工具栏 DOM
let invPriceLabels = new Map(); // 价格缓存:key=`appid|hash` → {cents,text,done,error}
let invQuickSellEl = null; // 当前快速出售面板 DOM
let invSelectedItem = null; // 当前选中物品数据(供"转宝石"使用)
let invBatchRunning = false; // 批量操作进行中标记
const invPricePending = new Map(); // 正在请求的价格 key → Promise(去重)
let invPriceQueueTail = Promise.resolve(); // 价格请求串行队列尾指针(限流)
/** 库存页入口 */
function initInventoryPage() {
L('Initializing inventory page features');
let attempt = 0;
const tryInit = () => {
const ready = unsafeWindow.g_ActiveInventory || document.querySelector('.inventory_ctn .item');
if (ready || attempt >= 30) {
if (settings.get('inventoryPriceLabels')) {
processInventoryItems();
setupInventoryObserver();
}
if (settings.get('quickSellButtons')) {
addInventoryToolbar();
hookItemSelection();
}
} else {
attempt++;
setTimeout(tryInit, 500);
}
};
tryInit();
}
/** 注入批量操作工具栏 */
function addInventoryToolbar() {
if (invToolbarEl) return;
const toolbar = h('div', { class: 'sme-inv-toolbar' }, [
h('div', { class: 'sme-inv-toolbar-title', html: ICONS.bag + '批量操作' }),
h('button', { class: 'sme-inv-btn', html: ICONS.sell + '出售所有物品', onclick: onBatchSellAll }),
h('button', { class: 'sme-inv-btn', html: ICONS.layers + '出售重复物品', onclick: onBatchSellDuplicates }),
h('button', { class: 'sme-inv-btn', html: ICONS.tag + '出售所有卡牌', onclick: onBatchSellCards }),
h('button', { class: 'sme-inv-btn sme-danger', html: ICONS.gem + '选中转宝石', onclick: turnSelectedIntoGems }),
h('div', { class: 'sme-inv-progress' }, [h('div', { class: 'sme-inv-progress-bar', style: { width: '0%' } })]),
h('span', { class: 'sme-inv-status', text: '就绪' }),
]);
invToolbarEl = toolbar;
const anchor = document.querySelector('.inventory_ctn') || document.querySelector('.filter_ctn') || document.querySelector('.games_list_tab_ctn');
if (anchor && anchor.parentNode) {
anchor.parentNode.insertBefore(toolbar, anchor);
} else {
const slot = document.querySelector('.responsive_page_template_content') || document.body;
slot.insertBefore(toolbar, slot.firstChild);
}
}
/** 更新进度条与状态文本 */
function updateInvProgress(done, total, status) {
if (!invToolbarEl) return;
const bar = invToolbarEl.querySelector('.sme-inv-progress-bar');
const txt = invToolbarEl.querySelector('.sme-inv-status');
if (bar) bar.style.width = (total > 0 ? Math.round(done / total * 100) : 0) + '%';
if (txt && status != null) txt.textContent = status;
}
/** 禁用/启用工具栏按钮 */
function setBatchButtonsDisabled(disabled) {
if (!invToolbarEl) return;
invToolbarEl.querySelectorAll('.sme-inv-btn').forEach(b => { b.disabled = !!disabled; });
}
/** 获取当前游戏库存中可见物品的 DOM 元素(id 形如 appid_contextid_assetid)
* 仅收集当前激活游戏(g_ActiveInventory)且未被品类筛选隐藏的物品 */
function getInventoryItemElements() {
// 确定当前激活的游戏 appid
let activeAppid = null;
try {
const inv = unsafeWindow.g_ActiveInventory;
if (inv) activeAppid = String(inv.m_appid || inv.appid || '');
} catch (e) {}
const els = document.querySelectorAll('.inventory_ctn .item');
const out = [];
const seenIds = new Set(); // 去重:同一 assetid 只保留一个
for (const el of els) {
if (!el.id || !/^\d+_\d+_\d+$/.test(el.id)) continue;
if (seenIds.has(el.id)) continue;
if (activeAppid) {
// 优先用 appid 过滤:只收集当前游戏库存的物品
const parts = el.id.split('_');
if (parts[0] !== activeAppid) continue;
} else {
// 回退:跳过隐藏库存容器中的物品(其他游戏的库存容器 display:none)
const ctn = el.closest('.inventory_ctn');
if (ctn && ctn.style.display === 'none') continue;
}
// 跳过被品类筛选隐藏的物品(Steam 在 .itemHolder 上设置 display:none)
if (el.style.display === 'none') continue;
const holder = el.parentElement;
if (holder && holder.style.display === 'none') continue;
seenIds.add(el.id);
out.push(el);
}
return out;
}
/** 标准化物品数据(合并 asset 与 description) */
function normalizeInvItem(raw, appid, contextid, assetid) {
if (!raw) return null;
const desc = raw.description || raw.rgDescription || raw;
return {
appid: String(raw.appid || appid),
contextid: String(raw.contextid || contextid),
assetid: String(raw.assetid || raw.id || assetid),
market_hash_name: desc.market_hash_name || raw.market_hash_name || desc.market_name || raw.name || '',
name: desc.name || raw.name || desc.market_hash_name || '',
type: desc.type || raw.type || '',
marketable: (desc.marketable != null ? desc.marketable : (raw.marketable != null ? raw.marketable : 0)),
tradable: (desc.tradable != null ? desc.tradable : (raw.tradable != null ? raw.tradable : 0)),
tags: desc.tags || raw.tags || {},
owner_actions: desc.owner_actions || raw.owner_actions || [],
amount: parseInt(raw.amount || raw.original_amount, 10) || 1,
original_amount: parseInt(raw.original_amount || raw.amount, 10) || 1,
element: raw.element || null,
};
}
function isInvMarketable(data) {
return !!(data && (data.marketable === 1 || data.marketable === true));
}
/** 通过多种途径获取物品数据(jQuery data → 直接属性 → g_ActiveInventory) */
function getRgItemFromElement(itemEl) {
if (!itemEl || !itemEl.id) return null;
const parts = itemEl.id.split('_');
if (parts.length < 3) return null;
const appid = parts[0], contextid = parts[1], assetid = parts[2];
// 1. jQuery data rgItem(Steam 在元素上挂载 CItem)
try {
const jq = unsafeWindow.$J || unsafeWindow.jQuery || unsafeWindow.$;
if (jq) {
const data = jq(itemEl).data('rgItem');
if (data) return normalizeInvItem(data, appid, contextid, assetid);
}
} catch (e) {}
// 2. 直接属性
try {
if (itemEl.rgItem) return normalizeInvItem(itemEl.rgItem, appid, contextid, assetid);
} catch (e) {}
// 3. g_ActiveInventory 查找
try {
const inv = unsafeWindow.g_ActiveInventory;
if (inv) {
if (inv.rgChildInventories) {
const child = inv.rgChildInventories[contextid];
if (child) {
if (child.rgItems && child.rgItems[assetid]) {
return normalizeInvItem(child.rgItems[assetid], appid, contextid, assetid);
}
if (child.m_rgAssets && child.m_rgAssets[assetid]) {
const asset = child.m_rgAssets[assetid];
const descKey = asset.classid + '_' + (asset.instanceid || '0');
const desc = (child.m_rgDescriptions && child.m_rgDescriptions[descKey]) ||
(inv.m_rgDescriptions && inv.m_rgDescriptions[descKey]) || null;
return normalizeInvItem(desc ? Object.assign({}, asset, desc) : asset, appid, contextid, assetid);
}
}
}
if (inv.m_rgAssets && inv.m_rgAssets[assetid]) {
return normalizeInvItem(inv.m_rgAssets[assetid], appid, contextid, assetid);
}
}
} catch (e) {}
return null;
}
/** 遍历可见物品,添加价格标签 */
function processInventoryItems() {
const els = getInventoryItemElements();
for (const el of els) {
if (el.querySelector('.sme-inv-price')) continue; // 已处理
const data = getRgItemFromElement(el);
if (!data || !isInvMarketable(data) || !data.market_hash_name) continue;
addPriceLabelToItem(el, data);
}
}
/** 为单个物品添加价格标签并异步获取价格 */
function addPriceLabelToItem(itemEl, data) {
const label = h('span', { class: 'sme-inv-price sme-inf', text: '···' });
itemEl.appendChild(label);
fetchInvPriceOverview(data.appid, data.market_hash_name).then(entry => {
label.textContent = entry.text;
label.classList.remove('sme-inf');
if (entry.error) label.classList.add('sme-na');
});
}
/** 价格概览请求(串行限流 1s/次,按 appid|hash 缓存去重,429 自动重试,不缓存错误) */
function fetchInvPriceOverview(appid, hashName) {
const key = `${appid}|${hashName}`;
if (invPriceLabels.has(key)) return Promise.resolve(invPriceLabels.get(key));
if (invPricePending.has(key)) return invPricePending.get(key);
const maxRetries = 2;
const p = invPriceQueueTail.then(async () => {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
await sleep(attempt === 0 ? INV_REQUEST_DELAY : 5000 * (attempt + 1));
try {
const wi = getWalletInfo();
const url = `https://steamcommunity.com/market/priceoverview/?appid=${appid}&market_hash_name=${encodeURIComponent(hashName)}¤cy=${wi.wallet_currency}`;
const po = await doFetch(url);
let cents = 0, text = '—';
if (po && po.success) {
// 优先使用最低卖单价,回退到中位数价
const priceStr = po.lowest_price || po.median_price;
if (priceStr) {
cents = parsePriceToCents(priceStr);
if (cents > 0) text = formatCents(cents);
}
}
const entry = { cents, text, done: true };
// 仅缓存成功结果(cents > 0),不缓存零值/错误,允许后续重试
if (cents > 0) invPriceLabels.set(key, entry);
return entry;
} catch (e) {
if (e.message === 'RateLimited' && attempt < maxRetries) {
continue; // 429 限流,等待更长延迟后重试
}
// 非 429 错误或重试耗尽:返回错误但不缓存,允许未来重试
return { cents: 0, text: '×', done: true, error: true };
}
}
return { cents: 0, text: '×', done: true, error: true };
});
invPricePending.set(key, p);
invPriceQueueTail = p.then(() => {}, () => {});
p.then(() => invPricePending.delete(key), () => invPricePending.delete(key));
return p;
}
/** 通过市场详情页 HTML 提取 item_nameid(库存页无该值,需抓取列表页) */
async function fetchItemNameIdByListing(appid, hashName) {
const cacheKey = `nid_${appid}_${hashName}`;
const cached = cache.get(cacheKey, CACHE_TTL.ORDER_BOOK);
if (cached) return cached;
const url = `https://steamcommunity.com/market/listings/${appid}/${encodeURIComponent(hashName)}`;
const resp = await fetch(url, { credentials: 'include' });
if (!resp.ok) throw new Error('HTTP ' + resp.status);
const html = await resp.text();
let nameId = null, m;
if ((m = html.match(/Market_LoadOrderSpread\(\s*(\d+)/))) nameId = m[1];
else if ((m = html.match(/"item_nameid"\s*:\s*"?(\d+)/))) nameId = m[1];
else if ((m = html.match(/item_nameid["'\s:=]+(\d{5,})/))) nameId = m[1];
if (nameId) cache.set(cacheKey, nameId);
return nameId;
}
/** 获取库存物品的订单簿(最低卖单 + 最高买单) */
async function fetchInventoryOrderBook(appid, hashName) {
const result = { lowestSell: 0, highestBuy: 0, volume: null, lowestText: '—' };
// 价格概览(轻量,提供最低卖单与成交量)
try {
const entry = await fetchInvPriceOverview(appid, hashName);
if (entry && entry.cents > 0) {
result.lowestSell = entry.cents;
result.lowestText = entry.text;
}
} catch (e) {}
// 订单直方图(提供最高买单,best-effort)
try {
const nameId = await fetchItemNameIdByListing(appid, hashName);
if (nameId) {
const hist = await fetchOrderHistogram(nameId);
if (hist.highestBuy) result.highestBuy = parseInt(hist.highestBuy, 10) || 0;
if (hist.lowestSell && !result.lowestSell) result.lowestSell = parseInt(hist.lowestSell, 10) || 0;
}
} catch (e) {
W('Inventory order histogram fetch failed:', e.message);
}
return result;
}
/** 监听库存 DOM 变化,处理新加载的物品(防抖 400ms) */
function setupInventoryObserver() {
const ctn = document.querySelector('.inventory_ctn');
if (!ctn) return;
if (invObserver) invObserver.disconnect();
let timer = null;
invObserver = new MutationObserver(() => {
if (timer) return;
timer = setTimeout(() => {
timer = null;
if (settings.get('inventoryPriceLabels')) processInventoryItems();
}, 400);
});
invObserver.observe(ctn, { childList: true, subtree: true });
}
/** 挂载物品选择钩子(事件委托),选中后显示快速出售面板 */
function hookItemSelection() {
const ctn = document.querySelector('.inventory_ctn') || document.body;
const handler = (e) => {
const itemEl = e.target && e.target.closest ? e.target.closest('.item') : null;
if (!itemEl || !/^\d+_\d+_\d+$/.test(itemEl.id)) return;
setTimeout(() => showQuickSellForElement(itemEl), 300);
};
ctn.addEventListener('click', handler, true);
}
function showQuickSellForElement(itemEl) {
const data = getRgItemFromElement(itemEl);
invSelectedItem = data; // 记录当前选中(供"转宝石"使用)
removeQuickSellPanel();
if (!data || !isInvMarketable(data) || !data.market_hash_name) return;
showQuickSell(data);
}
function getActiveItemInfoContent() {
return document.getElementById('iteminfo0_content') || document.getElementById('iteminfo1_content');
}
function removeQuickSellPanel() {
if (invQuickSellEl && invQuickSellEl.parentNode) {
invQuickSellEl.parentNode.removeChild(invQuickSellEl);
}
invQuickSellEl = null;
}
/** 显示快速出售面板 */
function showQuickSell(data) {
const content = getActiveItemInfoContent();
if (!content) return;
const panel = h('div', { class: 'sme-quick-sell' }, [
h('div', { class: 'sme-quick-sell-title', html: ICONS.sell + '快速出售 — ' + escapeHtml(data.name || data.market_hash_name) + '' }),
h('div', { class: 'sme-qs-book', text: '加载价格中...' }),
h('div', { class: 'sme-quick-sell-btns' }, []),
]);
invQuickSellEl = panel;
content.appendChild(panel);
fetchInventoryOrderBook(data.appid, data.market_hash_name).then(book => {
renderQuickSellPanel(panel, data, book);
}).catch(() => {
const bookEl = panel.querySelector('.sme-qs-book');
if (bookEl) bookEl.textContent = '价格加载失败';
});
}
/** 渲染快速出售按钮与订单簿摘要 */
function renderQuickSellPanel(panel, data, book) {
const bookEl = panel.querySelector('.sme-qs-book');
const btnsEl = panel.querySelector('.sme-quick-sell-btns');
if (!bookEl || !btnsEl) return;
bookEl.innerHTML = '';
const lowestSellCents = book.lowestSell || 0;
const highestBuyCents = book.highestBuy || 0;
bookEl.appendChild(h('div', { class: 'sme-qs-book-row sme-sell', text: '最低挂单: ' + (lowestSellCents ? formatCents(lowestSellCents) : '—') }));
bookEl.appendChild(h('div', { class: 'sme-qs-book-row sme-buy', text: '最高买单: ' + (highestBuyCents ? formatCents(highestBuyCents) : '—') }));
if (book.volume) bookEl.appendChild(h('div', { class: 'sme-qs-book-row', text: '24h成交量: ' + book.volume }));
btnsEl.innerHTML = '';
// 最高买单(即时成交)
if (highestBuyCents > 0) {
const buyReceived = priceBeforeFees(highestBuyCents);
if (buyReceived > 0) {
btnsEl.appendChild(h('button', { class: 'sme-qs-btn sme-start', html: ICONS.dollarSign + '最高买单 ' + formatCents(buyReceived) + '', onclick: () => doQuickSell(data, buyReceived, panel) }));
}
}
// 最低卖单 -1 / 最低卖单(到手价)
if (lowestSellCents > 0) {
const sellReceived = priceBeforeFees(lowestSellCents);
const minus1 = Math.max(sellReceived - 1, 1);
btnsEl.appendChild(h('button', { class: 'sme-qs-btn', html: ICONS.tag + '最低卖-1 ' + formatCents(minus1) + '', onclick: () => doQuickSell(data, minus1, panel) }));
btnsEl.appendChild(h('button', { class: 'sme-qs-btn', html: ICONS.tag + '最低卖 ' + formatCents(sellReceived) + '', onclick: () => doQuickSell(data, sellReceived, panel) }));
}
// 自定义价格(售价 = 买家支付价,自动换算为到手价)
const input = h('input', { class: 'sme-qs-input', type: 'number', step: '0.01', min: '0', placeholder: '售价' });
btnsEl.appendChild(input);
btnsEl.appendChild(h('button', { class: 'sme-qs-btn', html: ICONS.sell + '出售', onclick: () => {
const val = parseFloat(input.value);
if (!val || val <= 0) { toast('请输入有效价格', 'warning'); return; }
const buyerPaid = Math.round(val * 100);
const received = Math.max(priceBeforeFees(buyerPaid), 1);
doQuickSell(data, received, panel);
}}));
}
/** 执行快速出售 */
async function doQuickSell(data, priceCents, panel) {
const btns = panel.querySelectorAll('.sme-qs-btn');
btns.forEach(b => b.disabled = true);
const statusEl = panel.querySelector('.sme-qs-book');
const prevHtml = statusEl ? statusEl.innerHTML : '';
if (statusEl) statusEl.innerHTML = '出售中...
';
try {
await sellItem({ appid: data.appid, contextid: data.contextid, assetid: data.assetid, priceCents });
toast('已上架: ' + (data.name || data.market_hash_name), 'success');
if (statusEl) statusEl.innerHTML = '上架成功,到手 ' + formatCents(priceCents) + '
';
if (data.element) data.element.style.opacity = '0.4';
} catch (e) {
toast('上架失败: ' + e.message, 'error');
if (statusEl) statusEl.innerHTML = prevHtml + '失败: ' + escapeHtml(e.message) + '
';
btns.forEach(b => b.disabled = false);
}
}
/** 收集所有可出售物品 */
function collectSellableItems(filterFn) {
const els = getInventoryItemElements();
const items = [];
for (const el of els) {
const data = getRgItemFromElement(el);
if (!data || !isInvMarketable(data) || !data.market_hash_name) continue;
if (filterFn && !filterFn(data)) continue;
items.push({ element: el, data });
}
return items;
}
/** 收集重复物品(每种保留一件,出售其余) */
function collectDuplicateItems() {
const all = collectSellableItems();
const seen = new Map();
const result = [];
for (const it of all) {
const k = it.data.market_hash_name;
const n = seen.get(k) || 0;
if (n > 0) result.push(it); // 跳过首个,出售重复
seen.set(k, n + 1);
}
return result;
}
/** 计算批量出售价格:优先用最低卖单到手价,回退到订单直方图最高买单,最终回退到物品类型最低价 */
async function computeBatchSellPrice(data) {
// 1. 优先使用价格概览 API(轻量,有缓存)
try {
const entry = await fetchInvPriceOverview(data.appid, data.market_hash_name);
if (entry && entry.cents > 0) return Math.max(priceBeforeFees(entry.cents), 1);
} catch (e) {}
// 2. 回退到订单直方图(最高买单价 / 最低卖单价)
try {
const nameId = await fetchItemNameIdByListing(data.appid, data.market_hash_name);
if (nameId) {
const hist = await fetchOrderHistogram(nameId);
// highestBuy/lowestSell 均为分(cents,含手续费),需 priceBeforeFees 转为卖家到手价
if (hist.highestBuy) {
const buyCents = parseInt(hist.highestBuy, 10) || 0;
if (buyCents > 0) return Math.max(priceBeforeFees(buyCents), 1);
}
if (hist.lowestSell) {
const sellCents = parseInt(hist.lowestSell, 10) || 0;
if (sellCents > 0) return Math.max(priceBeforeFees(sellCents), 1);
}
}
} catch (e) {
W('Batch sell price fallback (order histogram) failed:', e.message);
}
// 3. 最终回退到物品类型最低价
const info = getItemPriceInfo(data);
return Math.max(Math.round(parseFloat(info.min) * 100), 1);
}
/** 批量出售(带进度与限流重试) */
async function batchSell(items, opLabel) {
if (invBatchRunning) { toast('已有批量操作进行中', 'warning'); return; }
if (!items || items.length === 0) { toast('没有符合条件的可出售物品', 'info'); return; }
invBatchRunning = true;
setBatchButtonsDisabled(true);
let success = 0, fail = 0;
updateInvProgress(0, items.length, opLabel + ': 准备出售 ' + items.length + ' 件...');
for (let i = 0; i < items.length; i++) {
const { data } = items[i];
updateInvProgress(i, items.length, opLabel + ' (' + (i + 1) + '/' + items.length + '): ' + (data.name || data.market_hash_name));
try {
const price = await computeBatchSellPrice(data);
await sellItem({ appid: data.appid, contextid: data.contextid, assetid: data.assetid, priceCents: price, amount: data.amount || 1 });
success++;
if (data.element) data.element.style.opacity = '0.4';
} catch (e) {
fail++;
if (isRetryMessage(e.message)) {
updateInvProgress(i, items.length, opLabel + ': 遇到限流,等待重试...');
await sleep(randInt(RETRY_BACKOFF_MIN, RETRY_BACKOFF_MAX));
try {
const price = await computeBatchSellPrice(data);
await sellItem({ appid: data.appid, contextid: data.contextid, assetid: data.assetid, priceCents: price, amount: data.amount || 1 });
success++; fail--;
if (data.element) data.element.style.opacity = '0.4';
} catch (e2) {
W('Batch sell retry failed:', e2.message);
}
} else {
W('Batch sell failed for', data.market_hash_name, e.message);
}
}
await sleep(randInt(SELL_DELAY_MIN, SELL_DELAY_MAX));
}
updateInvProgress(items.length, items.length, opLabel + ' 完成: 成功 ' + success + ', 失败 ' + fail);
toast(opLabel + '完成: 成功 ' + success + ', 失败 ' + fail, success > 0 ? 'success' : 'error');
invBatchRunning = false;
setBatchButtonsDisabled(false);
}
function invConfirm(msg) {
try { if (unsafeWindow.confirm) return unsafeWindow.confirm(msg); } catch (e) {}
return confirm(msg);
}
function onBatchSellAll() {
if (invBatchRunning) { toast('已有批量操作进行中', 'warning'); return; }
const items = collectSellableItems();
if (items.length === 0) { toast('没有可出售的市场物品', 'info'); return; }
if (!invConfirm('确定出售所有 ' + items.length + ' 件可市场物品吗?')) return;
batchSell(items, '出售所有物品');
}
function onBatchSellDuplicates() {
if (invBatchRunning) { toast('已有批量操作进行中', 'warning'); return; }
const items = collectDuplicateItems();
if (items.length === 0) { toast('没有重复物品可出售', 'info'); return; }
if (!invConfirm('确定出售 ' + items.length + ' 件重复物品吗(每种保留一件)?')) return;
batchSell(items, '出售重复物品');
}
function onBatchSellCards() {
if (invBatchRunning) { toast('已有批量操作进行中', 'warning'); return; }
const items = collectSellableItems(isTradingCard);
if (items.length === 0) { toast('没有可出售的卡牌', 'info'); return; }
if (!invConfirm('确定出售所有 ' + items.length + ' 张可市场卡牌吗?')) return;
batchSell(items, '出售所有卡牌');
}
/** 获取物品可转换的宝石数量(GET /ajaxgetgoovalue/) */
async function getGooValue(data) {
const url = `https://steamcommunity.com/ajaxgetgoovalue/?appid=${data.appid}&assetid=${data.assetid}&contextid=${data.contextid}`;
const json = await doFetch(url);
if (!json || !json.success) throw new Error((json && json.message) || '无法获取宝石价值');
return parseInt(json.goo_value, 10) || 0;
}
/** 转换为宝石(POST /ajaxgrindintogoo/) */
async function turnIntoGems(data, gooValue) {
const sessionid = getSessionId();
if (!sessionid) throw new Error('无法获取 sessionid');
const body = new URLSearchParams({
sessionid,
appid: String(data.appid),
assetid: String(data.assetid),
contextid: String(data.contextid),
goo_value_expected: String(gooValue),
});
const resp = await fetch('https://steamcommunity.com/ajaxgrindintogoo/', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
body: body.toString(),
});
if (!resp.ok) throw new Error('HTTP ' + resp.status);
const json = await resp.json();
if (!json || !json.success) throw new Error((json && json.message) || '转换宝石失败');
return json;
}
/** 将当前选中物品转换为宝石 */
async function turnSelectedIntoGems() {
if (invBatchRunning) { toast('已有操作进行中', 'warning'); return; }
if (!invSelectedItem) { toast('请先选择一个物品', 'warning'); return; }
const data = invSelectedItem;
if (isGems(data)) { toast('该物品已经是宝石', 'info'); return; }
invBatchRunning = true;
setBatchButtonsDisabled(true);
try {
updateInvProgress(0, 1, '转换宝石中: ' + (data.name || data.market_hash_name));
const goo = await getGooValue(data);
await turnIntoGems(data, goo);
updateInvProgress(1, 1, '转换成功: 获得 ' + goo + ' 颗宝石');
toast('已转换为 ' + goo + ' 颗宝石', 'success');
if (data.element) data.element.style.opacity = '0.4';
} catch (e) {
updateInvProgress(1, 1, '转换失败: ' + e.message);
toast('转换为宝石失败: ' + e.message, 'error');
W('Turn into gems failed:', e.message);
} finally {
invBatchRunning = false;
setBatchButtonsDisabled(false);
}
}
// ==================== 交易报价页 ====================
let tradeObserver = null;
let tradeLastSum = 0;
/** 初始化交易报价页 */
function initTradeOfferPage() {
L('Initializing trade offer page');
if (!settings.get('tradeOfferSummary')) return;
// 等待交易报价 DOM 加载
const tryInit = (attempt) => {
const tradeBox = document.querySelector('.trade_item_box');
if (tradeBox) {
setupTradeObserver();
injectSelectAllButton();
setTimeout(updateTradeOfferSummary, 1000);
} else if (attempt < 30) {
setTimeout(() => tryInit(attempt + 1), 500);
}
};
tryInit(0);
}
/** 设置 MutationObserver 监听交易框变化 */
function setupTradeObserver() {
if (tradeObserver) tradeObserver.disconnect();
tradeObserver = new MutationObserver(() => {
setTimeout(updateTradeOfferSummary, 300);
});
// 监听双方交易框
const boxes = document.querySelectorAll('.trade_item_box');
boxes.forEach(box => {
tradeObserver.observe(box, { childList: true, subtree: true });
});
L('Trade observer set up on', boxes.length, 'boxes');
}
/** 检查所有交易物品是否已加载 */
function hasLoadedAllTradeOfferItems() {
try {
const status = unsafeWindow.g_rgCurrentTradeStatus;
if (!status) return false;
const checkSide = (user, assets) => {
if (!assets || !user) return true;
for (const a of assets) {
const item = user.findAsset(a.appid, a.contextid, a.assetid);
if (!item) return false;
}
return true;
};
return checkSide(unsafeWindow.UserYou, status.me?.assets)
&& checkSide(unsafeWindow.UserThem, status.them?.assets);
} catch (e) {
return false;
}
}
/**
* 汇总交易一方的物品
* @param {Array} assets - 资产数组
* @param {Object} user - UserYou 或 UserThem
* @returns {{text: string, totalPrice: number, uniqueCount: number, totalCount: number}}
*/
function sumTradeOfferAssets(assets, user) {
const grouped = {};
let totalPrice = 0;
let totalCount = 0;
if (!assets || !user) return { text: '', totalPrice: 0, uniqueCount: 0, totalCount: 0 };
for (const asset of assets) {
let text = '';
try {
const rgItem = user.findAsset(asset.appid, asset.contextid, asset.assetid);
if (rgItem) {
// 从 DOM 读取价格标签(库存页设置的 price_ class)
if (rgItem.element) {
const priceEl = rgItem.element.querySelector('[class*="price_"]');
if (priceEl) {
const cls = priceEl.className || '';
const m = cls.match(/price_(\d+)/);
if (m) totalPrice += parseInt(m[1], 10);
}
}
// 处理堆叠物品的已用数量
if (rgItem.original_amount != null && rgItem.amount != null) {
const used = parseInt(rgItem.original_amount) - parseInt(rgItem.amount);
if (used > 0) text += `${used}x `;
}
text += rgItem.name || rgItem.market_hash_name || '未知物品';
if (rgItem.type && rgItem.type.length > 0) {
text += ` (${rgItem.type})`;
}
} else {
text = '未知物品';
}
} catch (e) {
text = '未知物品';
}
grouped[text] = (grouped[text] || 0) + 1;
totalCount++;
}
// 按数量降序排序
const sorted = Object.entries(grouped).sort((a, b) => b[1] - a[1]);
let text = `唯一物品:${sorted.length} · 总价值:${formatCents(totalPrice)} · 物品总数:${totalCount}
`;
for (const [name, count] of sorted) {
text += `${count}x ${name}
`;
}
return { text, totalPrice, uniqueCount: sorted.length, totalCount };
}
/** 更新交易报价双方摘要 */
function updateTradeOfferSummary() {
try {
if (!hasLoadedAllTradeOfferItems()) return;
const status = unsafeWindow.g_rgCurrentTradeStatus;
if (!status) return;
const currentSum = (status.me?.assets?.length || 0) + (status.them?.assets?.length || 0);
if (currentSum === tradeLastSum) return; // 无变化
tradeLastSum = currentSum;
// 你的物品摘要
const yourSum = sumTradeOfferAssets(status.me?.assets, unsafeWindow.UserYou);
const yourEl = document.querySelector('#trade_offer_your_sum');
if (yourEl) yourEl.remove();
if (yourSum.text) {
const yourHeader = document.querySelector('.offerheader.left') || document.querySelector('#trade_offer_your');
if (yourHeader) {
const el = h('div', { id: 'trade_offer_your_sum', class: 'sme-trade-summary', html: yourSum.text });
yourHeader.appendChild(el);
}
}
// 对方物品摘要
const theirSum = sumTradeOfferAssets(status.them?.assets, unsafeWindow.UserThem);
const theirEl = document.querySelector('#trade_offer_their_sum');
if (theirEl) theirEl.remove();
if (theirSum.text) {
const theirHeader = document.querySelector('.offerheader.right') || document.querySelector('#trade_offer_their');
if (theirHeader) {
const el = h('div', { id: 'trade_offer_their_sum', class: 'sme-trade-summary', html: theirSum.text });
theirHeader.appendChild(el);
}
}
// 价值对比提示
if (yourSum.totalPrice > 0 || theirSum.totalPrice > 0) {
const diff = yourSum.totalPrice - theirSum.totalPrice;
let diffText = '';
if (diff > 0) {
diffText = `你多付 ${formatCents(diff)}`;
} else if (diff < 0) {
diffText = `对方多付 ${formatCents(-diff)}`;
} else {
diffText = '双方价值相等';
}
const diffEl = document.querySelector('#trade_offer_diff');
if (diffEl) diffEl.remove();
const yourSumEl = document.querySelector('#trade_offer_your_sum');
if (yourSumEl) {
yourSumEl.appendChild(h('div', {
id: 'trade_offer_diff',
style: { marginTop: '6px', fontSize: '13px', fontWeight: '700', color: diff > 0 ? '#e24a4a' : (diff < 0 ? '#4caf50' : '#66c0f4') },
text: diffText,
}));
}
}
} catch (e) {
W('Trade offer summary error:', e.message);
}
}
/** 注入"选中页面全部物品"按钮 */
function injectSelectAllButton() {
if (document.querySelector('#sme_select_all_page')) return;
// 查找库存区域底部
const inventoryPage = document.querySelector('.inventory_page_tabs') || document.querySelector('.inventory_ctn');
if (!inventoryPage) return;
// 检查是否为还价场景(需先点击"修改报价")
const modifyBtn = document.querySelector('.modify_offer_btn');
if (modifyBtn && modifyBtn.offsetParent !== null) {
// 还价场景,暂不注入
return;
}
const btn = h('a', {
id: 'sme_select_all_page',
class: 'sme-trade-btn',
html: ICONS.selectAll + ' 选中页面全部物品',
onClick: selectAllItemsOnPage,
});
// 插入到库存页控制区域
const pageControl = document.querySelector('#inventory_pagecontrols');
if (pageControl) {
pageControl.parentElement.insertBefore(btn, pageControl.nextSibling);
} else {
inventoryPage.parentElement.insertBefore(btn, inventoryPage);
}
}
/** 选中当前可见库存页的所有可交易物品并移入交易 */
function selectAllItemsOnPage() {
try {
const holders = document.querySelectorAll('.inventory_ctn:visible > .inventory_page:visible > .itemHolder:visible, .inventory_ctn > .inventory_page:not([style*="none"]) > .itemHolder:not([style*="none"])');
let count = 0;
const items = [];
holders.forEach(holder => {
const itemEl = holder.querySelector('.item');
if (!itemEl) return;
const rgItem = itemEl.rgItem || getRgItemFromElement(itemEl);
if (!rgItem) return;
if (rgItem.is_stackable) return;
if (!rgItem.tradable) return;
items.push(holder);
});
if (items.length === 0) {
toast('当前页面无可交易物品', 'info');
return;
}
toast(`正在移入 ${items.length} 个物品...`, 'info');
// 逐个移入(250ms 间隔)
let idx = 0;
const moveNext = () => {
if (idx >= items.length) {
toast(`已移入 ${count} 个物品`, 'success');
return;
}
const holder = items[idx++];
try {
if (unsafeWindow.MoveItemToTrade) {
unsafeWindow.MoveItemToTrade(holder);
count++;
}
} catch (e) {}
setTimeout(moveNext, 250);
};
moveNext();
} catch (e) {
toast('选中全部物品失败: ' + e.message, 'error');
}
}
// ==================== 初始化 ====================
let initialized = false;
let lastURL = '';
let urlCheckTimer = null;
function init() {
if (initialized) return;
initialized = true;
L('Initializing on', location.href);
const sid = getSteamId();
if (sid) loadCacheForSession(sid);
else W('Steam ID not detected, some features may not work');
const pageType = detectPageType();
L('Page type:', pageType);
if (pageType === 'listing') {
initListingPage();
} else if (pageType === 'browse') {
initBrowsePage();
} else if (pageType === 'inventory') {
initInventoryPage();
} else if (pageType === 'tradeoffer') {
initTradeOfferPage();
}
// URL 变化检测(Steam 市场使用 AJAX 导航)
lastURL = location.href;
urlCheckTimer = setInterval(() => {
if (location.href !== lastURL) {
lastURL = location.href;
handleURLChange();
}
}, 800);
}
function initListingPage() {
listingInfo = getListingInfo();
L('Listing info:', listingInfo);
// 详情页已拥有物品标识
if (settings.get('ownedBadges')) {
setTimeout(processDetailPageBadge, 600);
}
// 等待 SSR 数据可用
const tryExtract = (attempt) => {
const ssrData = extractSSRData();
if (ssrData.sellOrders !== null) {
allSellOrders = ssrData.sellOrders;
L(`Found ${allSellOrders.length} sell orders`);
injectPanel();
if (settings.get('autoLoadHistory')) {
// 预加载价格历史
if (listingInfo) {
fetchPriceHistory(listingInfo.appid, listingInfo.marketHashName)
.then(data => { priceHistoryData = data; })
.catch(() => {});
}
}
} else if (attempt < 20) {
setTimeout(() => tryExtract(attempt + 1), 500);
} else {
L('No sell orders found after timeout, injecting panel anyway');
injectPanel();
}
};
tryExtract(0);
}
function initBrowsePage() {
if (settings.get('ownedBadges')) {
setTimeout(processAllBrowseRows, 400);
setupBrowseObserver();
}
createFab();
initMyListingsEnhancement();
}
function handleURLChange() {
L('URL changed to', location.href);
const pageType = detectPageType();
// 清理旧面板
if (panelEl) { panelEl.remove(); panelEl = null; }
if (fabEl) { fabEl.remove(); fabEl = null; }
if (fabPopup) { fabPopup.remove(); fabPopup = null; }
if (browseObserver) { browseObserver.disconnect(); browseObserver = null; }
if (invObserver) { invObserver.disconnect(); invObserver = null; }
if (invToolbarEl) { invToolbarEl.remove(); invToolbarEl = null; }
if (invPriceLabels) { invPriceLabels.clear(); }
if (tradeObserver) { tradeObserver.disconnect(); tradeObserver = null; }
cleanupMyListingsEnhancement();
allSellOrders = [];
priceHistoryData = null;
orderBookData = null;
currentTab = 'summary';
priceCheckResults = null;
priceCheckLoading = false;
if (pageType === 'listing') {
initListingPage();
} else if (pageType === 'browse') {
initBrowsePage();
} else if (pageType === 'inventory') {
initInventoryPage();
} else if (pageType === 'tradeoffer') {
initTradeOfferPage();
}
}
// ==================== 启动(等待 URL 稳定) ====================
let stableSince = 0;
let lastCheck = location.href;
let domReady = false;
function isOnMarket() {
return /^\/market(?:\/|$)/.test(location.pathname)
|| /^\/(?:id|profiles)\/[^/]+\/inventory/.test(location.pathname)
|| /^\/tradeoffer/.test(location.pathname);
}
function tryStart() {
if (!domReady) return;
const now = Date.now();
const curURL = location.href;
if (curURL !== lastCheck) {
lastCheck = curURL;
stableSince = now;
return;
}
if (now - stableSince < 800) return;
if (!isOnMarket()) return;
clearInterval(stableTimer);
init();
}
let stableTimer;
function onDOMReady() {
domReady = true;
stableSince = Date.now();
lastCheck = location.href;
stableTimer = setInterval(tryStart, 250);
setTimeout(() => {
if (domReady && isOnMarket() && !initialized) {
clearInterval(stableTimer);
init();
}
}, 2000);
}
if (document.readyState === 'interactive' || document.readyState === 'complete') {
onDOMReady();
} else {
document.addEventListener('DOMContentLoaded', onDOMReady);
}
})();