回复
5
查看
218
收藏
9

68

赠楼

3%

赠楼率

592

蒸汽

110

主题

1623

帖子

2633

积分
发表于 昨天 22:27 · 河北 | 显示全部楼层 |阅读模式
本文为 其乐用户(UID:1574722) 发布的原创文章,转摘前请联系该用户获得许可
本帖最后由 1574722 于 2026-8-7 09:09 编辑

实在是受不了新版市场详情页这一大串的上架物品列表了,
特别是上架胶囊这种大量商品的时候,要看个价格还得往下拉好久
于是用AI跑了一个油猴脚本,顺便加了一个快速下架的操作
界面清爽太多了有木有




PS. 下架顺序是优先下架最晚上架的商品
功能蛮简单的也就不做过多介绍了,脚本已经上架greasyfork 链接如下
https://greasyfork.org/zh-CN/scripts/590179-steam-market-listings-group

有需求自取,谢谢大家!

有些朋友可能无法访问greasyfork,直接贴下代码吧



  1. // ==UserScript==
  2. // @name         Steam Market Listings Group
  3. // @namespace    https://steamcommunity.com/
  4. // @version      2.1.0
  5. // @description  在Steam市场饰品详情页聚合显示已上架物品,按上架日期与价格分组
  6. // @author       RayRoad
  7. // @match        *://steamcommunity.com/market/listings/*
  8. // @grant        GM_addStyle
  9. // @grant        unsafeWindow
  10. // @run-at       document-idle
  11. // @license      MIT
  12. // ==/UserScript==


  13. (() => {
  14.   'use strict';


  15.   // ── Styles ──────────────────────────────────────────────


  16.   GM_addStyle(`
  17.     #smg-panel {
  18.       box-sizing: border-box;
  19.       background: linear-gradient(180deg, #1d1f23 0%, #23262e 100%);
  20.       border: 1px solid #3d4450; border-radius: 4px;
  21.       padding: 16px 20px; margin: 12px 0 16px; width: 100%;
  22.       font-family: "Motiva Sans", Arial, Helvetica, sans-serif; color: #c7d5e0;
  23.     }
  24.     .smg-header {
  25.       display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px;
  26.     }
  27.     .smg-title { color: #66c0f4; font-size: 15px; font-weight: 500; margin: 0; }
  28.     .smg-toggle-btn {
  29.       background: none; border: 1px solid #3d4450; color: #8f98a0; cursor: pointer;
  30.       padding: 4px 12px; border-radius: 3px; font-size: 12px; transition: all .15s;
  31.     }
  32.     .smg-toggle-btn:hover { border-color: #66c0f4; color: #66c0f4; }


  33.     .smg-table { width: 100%; border-collapse: collapse; }
  34.     .smg-table th {
  35.       color: #8f98a0; text-align: left; padding: 6px 8px; font-size: 12px;
  36.       border-bottom: 1px solid #3d4450; font-weight: 500;
  37.     }
  38.     .smg-table td {
  39.       color: #c7d5e0; padding: 7px 8px; font-size: 13px;
  40.       border-bottom: 1px solid rgba(61,68,80,.4);
  41.     }
  42.     .smg-table tr:last-child td { border-bottom: none; }
  43.     .smg-table tr:hover td { background: rgba(102,192,244,.04); }
  44.     .smg-date-cell { white-space: nowrap; color: #66c0f4; font-weight: 500; width: 110px; }
  45.     .smg-prices-cell { color: #acb2b8; line-height: 1.8; }
  46.     .smg-price-tag {
  47.       display: inline-block; background: rgba(102,192,244,.08); border: 1px solid rgba(102,192,244,.15);
  48.       border-radius: 3px; padding: 1px 7px; margin: 2px 3px 2px 0; font-size: 12px; color: #c7d5e0;
  49.     }
  50.     .smg-price-tag .smg-ct { color: #8f98a0; margin-left: 3px; }
  51.     .smg-price-tag .smg-sp { color: #7a8a96; font-size: 11px; margin-left: 2px; }
  52.     .smg-count-cell { text-align: right; white-space: nowrap; color: #acb2b8; width: 60px; }
  53.     .smg-total-row td {
  54.       border-top: 1px solid #3d4450; font-weight: 500; color: #66c0f4; padding-top: 10px;
  55.     }
  56.     #smg-panel.smg-collapsed .smg-table-wrap { display: none; }


  57.     .smg-delist-section {
  58.       margin-top: 14px; padding-top: 12px; border-top: 1px solid #3d4450;
  59.     }
  60.     .smg-delist-row {
  61.       display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  62.     }
  63.     .smg-delist-label { color: #8f98a0; font-size: 13px; white-space: nowrap; }
  64.     .smg-delist-select {
  65.       background: #31404d; border: 1px solid #3d4450; color: #c7d5e0;
  66.       padding: 5px 8px; border-radius: 3px; font-size: 13px; min-width: 140px;
  67.     }
  68.     .smg-delist-input {
  69.       background: #31404d; border: 1px solid #3d4450; color: #c7d5e0;
  70.       padding: 5px 8px; border-radius: 3px; font-size: 13px; width: 70px; text-align: center;
  71.     }
  72.     .smg-delist-btn {
  73.       background: linear-gradient(180deg, #c44 0%, #a33 100%); border: none;
  74.       color: #fff; padding: 5px 16px; border-radius: 3px; cursor: pointer;
  75.       font-size: 13px; transition: opacity .15s;
  76.     }
  77.     .smg-delist-btn:hover { opacity: .85; }
  78.     .smg-delist-btn:disabled { opacity: .5; cursor: default; }
  79.     .smg-delist-status { color: #8f98a0; font-size: 12px; margin-top: 8px; }
  80.     .smg-delist-status.smg-error { color: #e44; }
  81.     .smg-delist-status.smg-ok { color: #5c7; }
  82.   `);


  83.   // ── Utility ─────────────────────────────────────────────


  84.   function escapeHtml(str) {
  85.     const d = document.createElement('div');
  86.     d.textContent = str;
  87.     return d.innerHTML;
  88.   }


  89.   function tsToDateKey(ts) {
  90.     const d = new Date(ts * 1000);
  91.     const y = d.getFullYear();
  92.     const m = String(d.getMonth() + 1).padStart(2, '0');
  93.     const day = String(d.getDate()).padStart(2, '0');
  94.     return `${y}-${m}-${day}`;
  95.   }


  96.   function tsToDateTime(ts) {
  97.     const d = new Date(ts * 1000);
  98.     const y = d.getFullYear();
  99.     const m = String(d.getMonth() + 1).padStart(2, '0');
  100.     const day = String(d.getDate()).padStart(2, '0');
  101.     const h = String(d.getHours()).padStart(2, '0');
  102.     const min = String(d.getMinutes()).padStart(2, '0');
  103.     return `${y}-${m}-${day} ${h}:${min}`;
  104.   }


  105.   function getSessionId() {
  106.     if (unsafeWindow.g_sessionID) return unsafeWindow.g_sessionID;
  107.     const meta = document.querySelector('meta[name="sessionid"]');
  108.     if (meta) return meta.content;
  109.     const match = document.cookie.match(/sessionid=([^;]+)/);
  110.     return match ? decodeURIComponent(match[1]) : '';
  111.   }


  112.   // ── Data Extraction ─────────────────────────────────────


  113.   function extractOrders() {
  114.     console.log('[SMG] Extracting orders from SSR data...');
  115.     const ssr = unsafeWindow.SSR?.loaderData;
  116.     if (!Array.isArray(ssr)) {
  117.       console.warn('[SMG] SSR data not available');
  118.       return null;
  119.     }


  120.     for (const entry of ssr) {
  121.       try {
  122.         const data = typeof entry === 'string' ? JSON.parse(entry) : entry;
  123.         const orders = data?.myOrders?.rgSellOrders;
  124.         if (Array.isArray(orders) && orders.length > 0) {
  125.           console.log(`[SMG] Found ${orders.length} sell orders`);
  126.           return orders.map(o => ({
  127.             listingid: o.listingid,
  128.             assetid: o.assetid,
  129.             rtListed: o.rtListed,
  130.             dateKey: tsToDateKey(o.rtListed),
  131.             dateTime: tsToDateTime(o.rtListed),
  132.             buyerPrice: (o.strBuyerPrice || '').trim(),
  133.             sellerPrice: (o.strSellerPrice || '').trim(),
  134.           }));
  135.         }
  136.       } catch (e) { /* skip */ }
  137.     }


  138.     console.warn('[SMG] No sell orders found in SSR data');
  139.     return null;
  140.   }


  141.   // ── Grouping ────────────────────────────────────────────


  142.   function groupOrders(orders) {
  143.     const dateMap = new Map();


  144.     for (const o of orders) {
  145.       if (!dateMap.has(o.dateKey)) {
  146.         dateMap.set(o.dateKey, { dateKey: o.dateKey, prices: new Map(), items: [] });
  147.       }
  148.       const dg = dateMap.get(o.dateKey);
  149.       if (!dg.prices.has(o.buyerPrice)) {
  150.         dg.prices.set(o.buyerPrice, []);
  151.       }
  152.       dg.prices.get(o.buyerPrice).push(o);
  153.       dg.items.push(o);
  154.     }


  155.     for (const g of dateMap.values()) {
  156.       for (const items of g.prices.values()) {
  157.         items.sort((a, b) => b.rtListed - a.rtListed);
  158.       }
  159.     }


  160.     return [...dateMap.values()]
  161.       .sort((a, b) => b.dateKey.localeCompare(a.dateKey))
  162.       .map(g => ({
  163.         dateKey: g.dateKey,
  164.         prices: g.prices,
  165.         sortedPrices: [...g.prices.keys()].sort(),
  166.         totalCount: g.items.length,
  167.       }));
  168.   }


  169.   // ── Rendering ───────────────────────────────────────────


  170.   function renderSummary(grouped, allOrders) {
  171.     const total = grouped.reduce((s, g) => s + g.totalCount, 0);


  172.     let rows = '';
  173.     for (const g of grouped) {
  174.       const tags = g.sortedPrices.map(p => {
  175.         const items = g.prices.get(p);
  176.         const cnt = items.length;
  177.         const sp = items[0].sellerPrice;
  178.         const sellerPart = sp ? `<span class="smg-sp">(${escapeHtml(sp)})</span>` : '';
  179.         return `<span class="smg-price-tag">${escapeHtml(p)}${sellerPart}<span class="smg-ct">x${cnt}</span></span>`;
  180.       }).join('');


  181.       rows += `<tr>
  182.         <td class="smg-date-cell">${escapeHtml(g.dateKey)}</td>
  183.         <td class="smg-prices-cell">${tags}</td>
  184.         <td class="smg-count-cell">${g.totalCount}</td>
  185.       </tr>`;
  186.     }


  187.     return `<div id="smg-panel">
  188.       <div class="smg-header">
  189.         <h3 class="smg-title">上架汇总 - 共 ${total} 件 / ${grouped.length} 个日期</h3>
  190.         <button class="smg-toggle-btn" id="smg-toggle">收起</button>
  191.       </div>
  192.       <div class="smg-table-wrap">
  193.         <table class="smg-table">
  194.           <thead><tr><th>日期</th><th>价格分布</th><th style="text-align:right">数量</th></tr></thead>
  195.           <tbody>${rows}
  196.             <tr class="smg-total-row">
  197.               <td>合计</td><td></td><td class="smg-count-cell">${total}</td>
  198.             </tr>
  199.           </tbody>
  200.         </table>
  201.       </div>
  202.       ${renderDelistSection(allOrders)}
  203.     </div>`;
  204.   }


  205.   function renderDelistSection(allOrders) {
  206.     const priceMap = new Map();
  207.     for (const o of allOrders) {
  208.       if (!priceMap.has(o.buyerPrice)) priceMap.set(o.buyerPrice, 0);
  209.       priceMap.set(o.buyerPrice, priceMap.get(o.buyerPrice) + 1);
  210.     }


  211.     const options = [...priceMap.entries()]
  212.       .sort((a, b) => a[0].localeCompare(b[0]))
  213.       .map(([p, c]) => `<option value="${escapeHtml(p)}">${escapeHtml(p)} (${c}件)</option>`)
  214.       .join('');


  215.     return `<div class="smg-delist-section">
  216.       <div class="smg-delist-row">
  217.         <span class="smg-delist-label">下架价格:</span>
  218.         <select class="smg-delist-select" id="smg-delist-price">
  219.           <option value="">选择价格</option>
  220.           ${options}
  221.         </select>
  222.         <span class="smg-delist-label">数量:</span>
  223.         <input type="number" class="smg-delist-input" id="smg-delist-qty" value="1" min="1" placeholder="数量">
  224.         <button class="smg-delist-btn" id="smg-delist-btn">下架</button>
  225.       </div>
  226.       <div class="smg-delist-status" id="smg-delist-status"></div>
  227.     </div>`;
  228.   }


  229.   function refreshPanel(allOrders, grouped) {
  230.     const panel = document.getElementById('smg-panel');
  231.     if (!panel) return;
  232.     const wrapper = document.getElementById('smg-root');
  233.     if (wrapper) wrapper.innerHTML = renderSummary(grouped, allOrders);
  234.     bindPanelEvents(allOrders, grouped);
  235.   }


  236.   function bindPanelEvents(allOrders, grouped) {
  237.     document.getElementById('smg-toggle')?.addEventListener('click', function () {
  238.       const p = document.getElementById('smg-panel');
  239.       p.classList.toggle('smg-collapsed');
  240.       this.textContent = p.classList.contains('smg-collapsed') ? '展开' : '收起';
  241.     });


  242.     document.getElementById('smg-delist-btn')?.addEventListener('click', () => {
  243.       handleDelist(allOrders, grouped);
  244.     });
  245.   }


  246.   async function handleDelist(allOrders, grouped) {
  247.     const priceSelect = document.getElementById('smg-delist-price');
  248.     const qtyInput = document.getElementById('smg-delist-qty');
  249.     const statusEl = document.getElementById('smg-delist-status');
  250.     const btn = document.getElementById('smg-delist-btn');


  251.     const price = priceSelect.value;
  252.     const qty = parseInt(qtyInput.value, 10);


  253.     if (!price) {
  254.       statusEl.textContent = '请选择价格';
  255.       statusEl.className = 'smg-delist-status smg-error';
  256.       return;
  257.     }
  258.     if (!qty || qty < 1) {
  259.       statusEl.textContent = '请输入有效数量';
  260.       statusEl.className = 'smg-delist-status smg-error';
  261.       return;
  262.     }


  263.     const candidates = allOrders
  264.       .filter(o => o.buyerPrice === price)
  265.       .sort((a, b) => b.rtListed - a.rtListed);


  266.     if (candidates.length === 0) {
  267.       statusEl.textContent = '该价格没有可下架的商品';
  268.       statusEl.className = 'smg-delist-status smg-error';
  269.       return;
  270.     }


  271.     const toRemove = candidates.slice(0, Math.min(qty, candidates.length));
  272.     const sessionid = getSessionId();
  273.     if (!sessionid) {
  274.       statusEl.textContent = '无法获取会话ID';
  275.       statusEl.className = 'smg-delist-status smg-error';
  276.       return;
  277.     }


  278.     btn.disabled = true;
  279.     let success = 0;
  280.     let failed = 0;


  281.     for (let i = 0; i < toRemove.length; i++) {
  282.       const item = toRemove[i];
  283.       statusEl.textContent = `正在下架 ${i + 1}/${toRemove.length}...`;
  284.       statusEl.className = 'smg-delist-status';


  285.       try {
  286.         const resp = await fetch(`https://steamcommunity.com/market/removelisting/${item.listingid}`, {
  287.           method: 'POST',
  288.           credentials: 'include',
  289.           headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  290.           body: `sessionid=${encodeURIComponent(sessionid)}`,
  291.         });
  292.         if (resp.ok) {
  293.           success++;
  294.           const idx = allOrders.indexOf(item);
  295.           if (idx !== -1) allOrders.splice(idx, 1);
  296.         } else {
  297.           failed++;
  298.           console.warn(`[SMG] Failed to delist ${item.listingid}: HTTP ${resp.status}`);
  299.         }
  300.       } catch (e) {
  301.         failed++;
  302.         console.warn(`[SMG] Failed to delist ${item.listingid}:`, e);
  303.       }
  304.     }


  305.     statusEl.textContent = `下架完成: 成功 ${success} 件` + (failed > 0 ? `, 失败 ${failed} 件` : '');
  306.     statusEl.className = 'smg-delist-status' + (failed > 0 ? ' smg-error' : ' smg-ok');
  307.     btn.disabled = false;


  308.     const regrouped = groupOrders(allOrders);
  309.     refreshPanel(allOrders, regrouped);
  310.   }


  311.   // ── Main ────────────────────────────────────────────────


  312.   function findTargetElement() {
  313.     return document.querySelector(
  314.       '[style="--background: var(--color-dull-5); --border: 2px solid; ' +
  315.       '--border-color: var(--color-accent-8); --direction: column; --gap: var(--spacing-2);"]'
  316.     );
  317.   }


  318.   function ensurePanel(allOrders, grouped, targetEl) {
  319.     if (document.getElementById('smg-panel')) return;


  320.     const wrapper = document.createElement('div');
  321.     wrapper.id = 'smg-root';
  322.     wrapper.innerHTML = renderSummary(grouped, allOrders);


  323.     targetEl.style.display = 'none';
  324.     targetEl.parentNode.insertBefore(wrapper, targetEl.nextSibling);


  325.     bindPanelEvents(allOrders, grouped);


  326.     console.log('[SMG] Panel injected');
  327.   }


  328.   let allOrders = [];


  329.   function main() {
  330.     console.log('[SMG] Script started on', location.href);


  331.     let targetEl = findTargetElement();


  332.     if (!targetEl) {
  333.       console.log('[SMG] Target element not found, waiting...');
  334.       const waitObs = new MutationObserver(() => {
  335.         targetEl = findTargetElement();
  336.         if (targetEl) {
  337.           waitObs.disconnect();
  338.           console.log('[SMG] Target element found');
  339.           proceed(targetEl);
  340.         }
  341.       });
  342.       waitObs.observe(document.body, { childList: true, subtree: true });
  343.       setTimeout(() => {
  344.         if (!targetEl) {
  345.           waitObs.disconnect();
  346.           console.warn('[SMG] Target element not found after timeout');
  347.         }
  348.       }, 15000);
  349.       return;
  350.     }


  351.     proceed(targetEl);


  352.     function proceed(targetEl) {
  353.       allOrders = extractOrders();
  354.       if (!allOrders || allOrders.length === 0) {
  355.         console.log('[SMG] No orders to display');
  356.         return;
  357.       }


  358.       const grouped = groupOrders(allOrders);
  359.       console.log(`[SMG] Grouped into ${grouped.length} date groups`);


  360.       ensurePanel(allOrders, grouped, targetEl);


  361.       new MutationObserver(() => {
  362.         const panel = document.getElementById('smg-panel');
  363.         if (!panel) {
  364.           console.log('[SMG] Panel removed by page, re-injecting...');
  365.           if (targetEl) targetEl.style.display = '';
  366.           ensurePanel(allOrders, groupOrders(allOrders), targetEl);
  367.         }
  368.       }).observe(document.body, { childList: true, subtree: true });
  369.     }
  370.   }


  371.   if (document.readyState === 'loading') {
  372.     document.addEventListener('DOMContentLoaded', main);
  373.   } else {
  374.     main();
  375.   }
  376. })();
复制代码

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?注册

×
回复

使用道具 举报

浏览本版块需要:
1. 初阶会员或更高等级;
2. (点击此处)绑定Steam账号
您需要登录后才可以回帖 登录 | 注册

本版积分规则

欢迎发帖参与讨论 o(*≧▽≦)ツ,请注意
1. 寻求帮助或答案的帖子请发到问题互助版块,悬赏有助于问题解决的速度。发错可能失去在该板块发布主题的权限(了解更多
2. 表达观点可以,也请务必注意语气和用词,以免影响他人浏览,特别是针对其他会员的内容。如觉得违规可使用举报功能 交由管理人员处理,请勿引用对方的内容。
3. 开箱晒物交易中心游戏互鉴福利放送版块请注意额外的置顶版规。
4. 除了提问帖和交易帖以外,不确认发在哪个版块的帖子可以先发在谈天说地

  作为民间站点,自 2004 年起为广大中文 Steam 用户提供技术支持与讨论空间。历经二十余载风雨,如今已发展为国内最大的正版玩家据点。

列表模式 · · 微博 · Bilibili频道 · Steam 群组 · 贴吧 · QQ群 
Keylol 其乐 ©2004-2026 Chinese Steam User Fan Site.
Designed by Lee in Balestier, Powered by Discuz!
推荐使用 ChromeMicrosoft Edge 来浏览本站
广告投放|手机版|广州数趣信息科技有限公司 版权所有|其乐 Keylol ( 粤ICP备17068105号 )
GMT+8, 2026-8-7 10:39
快速回复 返回顶部 返回列表