|
|
|
本文为 其乐用户(UID:1574722) 发布的原创文章,转摘前请联系该用户获得许可
本帖最后由 1574722 于 2026-8-7 09:09 编辑
实在是受不了新版市场详情页这一大串的上架物品列表了,
特别是上架胶囊这种大量商品的时候,要看个价格还得往下拉好久
于是用AI跑了一个油猴脚本,顺便加了一个快速下架的操作
界面清爽太多了有木有
PS. 下架顺序是优先下架最晚上架的商品
功能蛮简单的也就不做过多介绍了,脚本已经上架greasyfork 链接如下
https://greasyfork.org/zh-CN/scripts/590179-steam-market-listings-group
有需求自取,谢谢大家!
有些朋友可能无法访问greasyfork,直接贴下代码吧
- // ==UserScript==
- // @name Steam Market Listings Group
- // @namespace https://steamcommunity.com/
- // @version 2.1.0
- // @description 在Steam市场饰品详情页聚合显示已上架物品,按上架日期与价格分组
- // @author RayRoad
- // @match *://steamcommunity.com/market/listings/*
- // @grant GM_addStyle
- // @grant unsafeWindow
- // @run-at document-idle
- // @license MIT
- // ==/UserScript==
- (() => {
- 'use strict';
- // ── Styles ──────────────────────────────────────────────
- GM_addStyle(`
- #smg-panel {
- box-sizing: border-box;
- background: linear-gradient(180deg, #1d1f23 0%, #23262e 100%);
- border: 1px solid #3d4450; border-radius: 4px;
- padding: 16px 20px; margin: 12px 0 16px; width: 100%;
- font-family: "Motiva Sans", Arial, Helvetica, sans-serif; color: #c7d5e0;
- }
- .smg-header {
- display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px;
- }
- .smg-title { color: #66c0f4; font-size: 15px; font-weight: 500; margin: 0; }
- .smg-toggle-btn {
- background: none; border: 1px solid #3d4450; color: #8f98a0; cursor: pointer;
- padding: 4px 12px; border-radius: 3px; font-size: 12px; transition: all .15s;
- }
- .smg-toggle-btn:hover { border-color: #66c0f4; color: #66c0f4; }
- .smg-table { width: 100%; border-collapse: collapse; }
- .smg-table th {
- color: #8f98a0; text-align: left; padding: 6px 8px; font-size: 12px;
- border-bottom: 1px solid #3d4450; font-weight: 500;
- }
- .smg-table td {
- color: #c7d5e0; padding: 7px 8px; font-size: 13px;
- border-bottom: 1px solid rgba(61,68,80,.4);
- }
- .smg-table tr:last-child td { border-bottom: none; }
- .smg-table tr:hover td { background: rgba(102,192,244,.04); }
- .smg-date-cell { white-space: nowrap; color: #66c0f4; font-weight: 500; width: 110px; }
- .smg-prices-cell { color: #acb2b8; line-height: 1.8; }
- .smg-price-tag {
- display: inline-block; background: rgba(102,192,244,.08); border: 1px solid rgba(102,192,244,.15);
- border-radius: 3px; padding: 1px 7px; margin: 2px 3px 2px 0; font-size: 12px; color: #c7d5e0;
- }
- .smg-price-tag .smg-ct { color: #8f98a0; margin-left: 3px; }
- .smg-price-tag .smg-sp { color: #7a8a96; font-size: 11px; margin-left: 2px; }
- .smg-count-cell { text-align: right; white-space: nowrap; color: #acb2b8; width: 60px; }
- .smg-total-row td {
- border-top: 1px solid #3d4450; font-weight: 500; color: #66c0f4; padding-top: 10px;
- }
- #smg-panel.smg-collapsed .smg-table-wrap { display: none; }
- .smg-delist-section {
- margin-top: 14px; padding-top: 12px; border-top: 1px solid #3d4450;
- }
- .smg-delist-row {
- display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
- }
- .smg-delist-label { color: #8f98a0; font-size: 13px; white-space: nowrap; }
- .smg-delist-select {
- background: #31404d; border: 1px solid #3d4450; color: #c7d5e0;
- padding: 5px 8px; border-radius: 3px; font-size: 13px; min-width: 140px;
- }
- .smg-delist-input {
- background: #31404d; border: 1px solid #3d4450; color: #c7d5e0;
- padding: 5px 8px; border-radius: 3px; font-size: 13px; width: 70px; text-align: center;
- }
- .smg-delist-btn {
- background: linear-gradient(180deg, #c44 0%, #a33 100%); border: none;
- color: #fff; padding: 5px 16px; border-radius: 3px; cursor: pointer;
- font-size: 13px; transition: opacity .15s;
- }
- .smg-delist-btn:hover { opacity: .85; }
- .smg-delist-btn:disabled { opacity: .5; cursor: default; }
- .smg-delist-status { color: #8f98a0; font-size: 12px; margin-top: 8px; }
- .smg-delist-status.smg-error { color: #e44; }
- .smg-delist-status.smg-ok { color: #5c7; }
- `);
- // ── Utility ─────────────────────────────────────────────
- function escapeHtml(str) {
- const d = document.createElement('div');
- d.textContent = str;
- return d.innerHTML;
- }
- function tsToDateKey(ts) {
- const d = new Date(ts * 1000);
- const y = d.getFullYear();
- const m = String(d.getMonth() + 1).padStart(2, '0');
- const day = String(d.getDate()).padStart(2, '0');
- return `${y}-${m}-${day}`;
- }
- function tsToDateTime(ts) {
- const d = new Date(ts * 1000);
- const y = d.getFullYear();
- const m = String(d.getMonth() + 1).padStart(2, '0');
- const day = String(d.getDate()).padStart(2, '0');
- const h = String(d.getHours()).padStart(2, '0');
- const min = String(d.getMinutes()).padStart(2, '0');
- return `${y}-${m}-${day} ${h}:${min}`;
- }
- function getSessionId() {
- if (unsafeWindow.g_sessionID) return unsafeWindow.g_sessionID;
- const meta = document.querySelector('meta[name="sessionid"]');
- if (meta) return meta.content;
- const match = document.cookie.match(/sessionid=([^;]+)/);
- return match ? decodeURIComponent(match[1]) : '';
- }
- // ── Data Extraction ─────────────────────────────────────
- function extractOrders() {
- console.log('[SMG] Extracting orders from SSR data...');
- const ssr = unsafeWindow.SSR?.loaderData;
- if (!Array.isArray(ssr)) {
- console.warn('[SMG] SSR data not available');
- return null;
- }
- for (const entry of ssr) {
- try {
- const data = typeof entry === 'string' ? JSON.parse(entry) : entry;
- const orders = data?.myOrders?.rgSellOrders;
- if (Array.isArray(orders) && orders.length > 0) {
- console.log(`[SMG] Found ${orders.length} sell orders`);
- return orders.map(o => ({
- listingid: o.listingid,
- assetid: o.assetid,
- rtListed: o.rtListed,
- dateKey: tsToDateKey(o.rtListed),
- dateTime: tsToDateTime(o.rtListed),
- buyerPrice: (o.strBuyerPrice || '').trim(),
- sellerPrice: (o.strSellerPrice || '').trim(),
- }));
- }
- } catch (e) { /* skip */ }
- }
- console.warn('[SMG] No sell orders found in SSR data');
- return null;
- }
- // ── Grouping ────────────────────────────────────────────
- function groupOrders(orders) {
- const dateMap = new Map();
- for (const o of orders) {
- if (!dateMap.has(o.dateKey)) {
- dateMap.set(o.dateKey, { dateKey: o.dateKey, prices: new Map(), items: [] });
- }
- const dg = dateMap.get(o.dateKey);
- if (!dg.prices.has(o.buyerPrice)) {
- dg.prices.set(o.buyerPrice, []);
- }
- dg.prices.get(o.buyerPrice).push(o);
- dg.items.push(o);
- }
- for (const g of dateMap.values()) {
- for (const items of g.prices.values()) {
- items.sort((a, b) => b.rtListed - a.rtListed);
- }
- }
- return [...dateMap.values()]
- .sort((a, b) => b.dateKey.localeCompare(a.dateKey))
- .map(g => ({
- dateKey: g.dateKey,
- prices: g.prices,
- sortedPrices: [...g.prices.keys()].sort(),
- totalCount: g.items.length,
- }));
- }
- // ── Rendering ───────────────────────────────────────────
- function renderSummary(grouped, allOrders) {
- const total = grouped.reduce((s, g) => s + g.totalCount, 0);
- let rows = '';
- for (const g of grouped) {
- const tags = g.sortedPrices.map(p => {
- const items = g.prices.get(p);
- const cnt = items.length;
- const sp = items[0].sellerPrice;
- const sellerPart = sp ? `<span class="smg-sp">(${escapeHtml(sp)})</span>` : '';
- return `<span class="smg-price-tag">${escapeHtml(p)}${sellerPart}<span class="smg-ct">x${cnt}</span></span>`;
- }).join('');
- rows += `<tr>
- <td class="smg-date-cell">${escapeHtml(g.dateKey)}</td>
- <td class="smg-prices-cell">${tags}</td>
- <td class="smg-count-cell">${g.totalCount}</td>
- </tr>`;
- }
- return `<div id="smg-panel">
- <div class="smg-header">
- <h3 class="smg-title">上架汇总 - 共 ${total} 件 / ${grouped.length} 个日期</h3>
- <button class="smg-toggle-btn" id="smg-toggle">收起</button>
- </div>
- <div class="smg-table-wrap">
- <table class="smg-table">
- <thead><tr><th>日期</th><th>价格分布</th><th style="text-align:right">数量</th></tr></thead>
- <tbody>${rows}
- <tr class="smg-total-row">
- <td>合计</td><td></td><td class="smg-count-cell">${total}</td>
- </tr>
- </tbody>
- </table>
- </div>
- ${renderDelistSection(allOrders)}
- </div>`;
- }
- function renderDelistSection(allOrders) {
- const priceMap = new Map();
- for (const o of allOrders) {
- if (!priceMap.has(o.buyerPrice)) priceMap.set(o.buyerPrice, 0);
- priceMap.set(o.buyerPrice, priceMap.get(o.buyerPrice) + 1);
- }
- const options = [...priceMap.entries()]
- .sort((a, b) => a[0].localeCompare(b[0]))
- .map(([p, c]) => `<option value="${escapeHtml(p)}">${escapeHtml(p)} (${c}件)</option>`)
- .join('');
- return `<div class="smg-delist-section">
- <div class="smg-delist-row">
- <span class="smg-delist-label">下架价格:</span>
- <select class="smg-delist-select" id="smg-delist-price">
- <option value="">选择价格</option>
- ${options}
- </select>
- <span class="smg-delist-label">数量:</span>
- <input type="number" class="smg-delist-input" id="smg-delist-qty" value="1" min="1" placeholder="数量">
- <button class="smg-delist-btn" id="smg-delist-btn">下架</button>
- </div>
- <div class="smg-delist-status" id="smg-delist-status"></div>
- </div>`;
- }
- function refreshPanel(allOrders, grouped) {
- const panel = document.getElementById('smg-panel');
- if (!panel) return;
- const wrapper = document.getElementById('smg-root');
- if (wrapper) wrapper.innerHTML = renderSummary(grouped, allOrders);
- bindPanelEvents(allOrders, grouped);
- }
- function bindPanelEvents(allOrders, grouped) {
- document.getElementById('smg-toggle')?.addEventListener('click', function () {
- const p = document.getElementById('smg-panel');
- p.classList.toggle('smg-collapsed');
- this.textContent = p.classList.contains('smg-collapsed') ? '展开' : '收起';
- });
- document.getElementById('smg-delist-btn')?.addEventListener('click', () => {
- handleDelist(allOrders, grouped);
- });
- }
- async function handleDelist(allOrders, grouped) {
- const priceSelect = document.getElementById('smg-delist-price');
- const qtyInput = document.getElementById('smg-delist-qty');
- const statusEl = document.getElementById('smg-delist-status');
- const btn = document.getElementById('smg-delist-btn');
- const price = priceSelect.value;
- const qty = parseInt(qtyInput.value, 10);
- if (!price) {
- statusEl.textContent = '请选择价格';
- statusEl.className = 'smg-delist-status smg-error';
- return;
- }
- if (!qty || qty < 1) {
- statusEl.textContent = '请输入有效数量';
- statusEl.className = 'smg-delist-status smg-error';
- return;
- }
- const candidates = allOrders
- .filter(o => o.buyerPrice === price)
- .sort((a, b) => b.rtListed - a.rtListed);
- if (candidates.length === 0) {
- statusEl.textContent = '该价格没有可下架的商品';
- statusEl.className = 'smg-delist-status smg-error';
- return;
- }
- const toRemove = candidates.slice(0, Math.min(qty, candidates.length));
- const sessionid = getSessionId();
- if (!sessionid) {
- statusEl.textContent = '无法获取会话ID';
- statusEl.className = 'smg-delist-status smg-error';
- return;
- }
- btn.disabled = true;
- let success = 0;
- let failed = 0;
- for (let i = 0; i < toRemove.length; i++) {
- const item = toRemove[i];
- statusEl.textContent = `正在下架 ${i + 1}/${toRemove.length}...`;
- statusEl.className = 'smg-delist-status';
- 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 = allOrders.indexOf(item);
- if (idx !== -1) allOrders.splice(idx, 1);
- } else {
- failed++;
- console.warn(`[SMG] Failed to delist ${item.listingid}: HTTP ${resp.status}`);
- }
- } catch (e) {
- failed++;
- console.warn(`[SMG] Failed to delist ${item.listingid}:`, e);
- }
- }
- statusEl.textContent = `下架完成: 成功 ${success} 件` + (failed > 0 ? `, 失败 ${failed} 件` : '');
- statusEl.className = 'smg-delist-status' + (failed > 0 ? ' smg-error' : ' smg-ok');
- btn.disabled = false;
- const regrouped = groupOrders(allOrders);
- refreshPanel(allOrders, regrouped);
- }
- // ── Main ────────────────────────────────────────────────
- function findTargetElement() {
- return document.querySelector(
- '[style="--background: var(--color-dull-5); --border: 2px solid; ' +
- '--border-color: var(--color-accent-8); --direction: column; --gap: var(--spacing-2);"]'
- );
- }
- function ensurePanel(allOrders, grouped, targetEl) {
- if (document.getElementById('smg-panel')) return;
- const wrapper = document.createElement('div');
- wrapper.id = 'smg-root';
- wrapper.innerHTML = renderSummary(grouped, allOrders);
- targetEl.style.display = 'none';
- targetEl.parentNode.insertBefore(wrapper, targetEl.nextSibling);
- bindPanelEvents(allOrders, grouped);
- console.log('[SMG] Panel injected');
- }
- let allOrders = [];
- function main() {
- console.log('[SMG] Script started on', location.href);
- let targetEl = findTargetElement();
- if (!targetEl) {
- console.log('[SMG] Target element not found, waiting...');
- const waitObs = new MutationObserver(() => {
- targetEl = findTargetElement();
- if (targetEl) {
- waitObs.disconnect();
- console.log('[SMG] Target element found');
- proceed(targetEl);
- }
- });
- waitObs.observe(document.body, { childList: true, subtree: true });
- setTimeout(() => {
- if (!targetEl) {
- waitObs.disconnect();
- console.warn('[SMG] Target element not found after timeout');
- }
- }, 15000);
- return;
- }
- proceed(targetEl);
- function proceed(targetEl) {
- allOrders = extractOrders();
- if (!allOrders || allOrders.length === 0) {
- console.log('[SMG] No orders to display');
- return;
- }
- const grouped = groupOrders(allOrders);
- console.log(`[SMG] Grouped into ${grouped.length} date groups`);
- ensurePanel(allOrders, grouped, targetEl);
- new MutationObserver(() => {
- const panel = document.getElementById('smg-panel');
- if (!panel) {
- console.log('[SMG] Panel removed by page, re-injecting...');
- if (targetEl) targetEl.style.display = '';
- ensurePanel(allOrders, groupOrders(allOrders), targetEl);
- }
- }).observe(document.body, { childList: true, subtree: true });
- }
- }
- if (document.readyState === 'loading') {
- document.addEventListener('DOMContentLoaded', main);
- } else {
- main();
- }
- })();
复制代码
|
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有帐号?注册
×
1、转载或引用本网站内容,必须注明本文网址:https://keylol.com/t1045939-1-1。如发文者注明禁止转载,则请勿转载
2、对于不当转载或引用本网站内容而引起的民事纷争、行政处理或其他损失,本网站不承担责任
3、对不遵守本声明或其他违法、恶意使用本网站内容者,本网站保留追究其法律责任的权利
4、所有帖子仅代表作者本人意见,不代表本社区立场
|