// ==UserScript== // @name Boss QCOS 超级老板 高级商品搜索辅助 (巅峰版) // @namespace http://tampermonkey.net/ // @version 3.1 // @description XSS防御、Promise并发提速、解决Iframe拖拽卡顿、红粉紫渐变UI、修复首击失效Bug // @author lswlc33 (Refactored) // @match https://boss.qcos.cn/* // @grant GM_addStyle // @grant unsafeWindow // @run-at document-end // ==/UserScript== (function() { 'use strict'; // ================= 配置项 ================= const CONFIG = { ROWS_PER_PAGE: 50, MAX_PAGES: 15, UI: { Z_INDEX: 999999 } }; // ================= 工具函数 ================= // 防御 XSS 注入 const escapeHTML = (str) => { return (str || '').toString() .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); }; // 获取唯一 ID const getUid = (item) => item.stockid || item.spxxno; // 安全解析库存 const getStock = (item) => parseInt(item.kcnum || '0', 10); // ================= CSS 样式注入 ================= GM_addStyle(` #tm-search-ball { position: fixed; top: 25px; right: 25px; width: 48px; height: 48px; background: linear-gradient(135deg, #ff3b30, #ff2d55, #af52de); border-radius: 50%; display: flex; align-items: center; justify-content: center; box-shadow: 0 4px 16px rgba(255, 45, 85, 0.4); cursor: pointer; z-index: ${CONFIG.UI.Z_INDEX}; transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1); user-select: none; color: white; } #tm-search-ball:hover { transform: translateY(-3px) scale(1.05); box-shadow: 0 8px 24px rgba(255, 45, 85, 0.6); } #tm-search-ball svg { width: 22px; height: 22px; fill: currentColor; } #tm-search-panel { position: fixed; top: 85px; right: 25px; width: 450px; background: #ffffff; box-shadow: 0 10px 40px -10px rgba(0,0,0,0.2), 0 0 1px rgba(0,0,0,0.1); border-radius: 12px; z-index: ${CONFIG.UI.Z_INDEX - 1}; display: none; flex-direction: column; overflow: hidden; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif; opacity: 0; transform: translateY(-10px); transition: opacity 0.3s ease, transform 0.3s ease; } #tm-search-panel.tm-show-anim { transform: translateY(0); } #tm-search-header { padding: 16px 20px; background: #fafafa; border-bottom: 1px solid #f0f0f0; display: flex; justify-content: space-between; align-items: center; cursor: move; user-select: none; } .tm-header-title { font-weight: 600; font-size: 16px; color: #1f1f1f; display: flex; align-items: center; gap: 8px;} .tm-header-title::before { content: ''; display: block; width: 4px; height: 16px; background: linear-gradient(to bottom, #ff2d55, #af52de); border-radius: 2px;} #tm-search-close { cursor: pointer; color: #8c8c8c; font-size: 20px; line-height: 1; transition: color 0.2s; padding: 4px; border-radius: 4px; } #tm-search-close:hover { color: #ff4d4f; background: #fff1f0; } #tm-search-body { padding: 20px; display: flex; flex-direction: column; gap: 16px; background: #fff; } .tm-input-wrapper { display: flex; background: #f5f5f5; border-radius: 24px; padding: 4px 4px 4px 16px; border: 1px solid transparent; transition: all 0.3s ease; align-items: center; } .tm-input-wrapper:focus-within { background: #fff; border-color: #ff2d55; box-shadow: 0 0 0 2px rgba(255, 45, 85, 0.2); } #tm-input-keyword { flex: 1; border: none; background: transparent; outline: none; font-size: 14px; color: #333; } #tm-input-keyword::placeholder { color: #bfbfbf; } .tm-btn { padding: 8px 20px; background: linear-gradient(135deg, #ff2d55, #af52de); color: #fff; border: none; border-radius: 20px; cursor: pointer; font-weight: 500; font-size: 14px; transition: all 0.3s; } .tm-btn:hover { filter: brightness(1.1); box-shadow: 0 2px 8px rgba(175, 82, 222, 0.4); } .tm-btn:disabled { background: #d9d9d9; color: #fff; cursor: not-allowed; box-shadow: none; filter: none; } .tm-tools-row { display: flex; align-items: center; gap: 12px; font-size: 13px; color: #595959; } .tm-checkbox-label { display: flex; align-items: center; gap: 6px; cursor: pointer; user-select: none; } .tm-checkbox-label input { cursor: pointer; accent-color: #ff2d55; width: 15px; height: 15px;} #tm-summary-text { color: #8c8c8c; font-size: 12px; margin-left: 4px; } #tm-sort-select { padding: 4px 8px; border: 1px solid #d9d9d9; border-radius: 6px; margin-left: auto; outline: none; background: #fff; color: #595959; cursor: pointer; transition: border-color 0.3s; } #tm-sort-select:hover { border-color: #ff2d55; } #tm-loading-wrapper { display: none; flex-direction: column; align-items: center; justify-content: center; padding: 30px 0; gap: 12px; color: #af52de; font-size: 13px; } .tm-spinner { width: 24px; height: 24px; border: 3px solid rgba(175, 82, 222, 0.2); border-top-color: #af52de; border-radius: 50%; animation: tm-spin 0.8s linear infinite; } @keyframes tm-spin { to { transform: rotate(360deg); } } #tm-results-container { max-height: 420px; overflow-y: auto; border-top: 1px solid #f0f0f0; background: #fafafa; } #tm-results-container::-webkit-scrollbar { width: 6px; } #tm-results-container::-webkit-scrollbar-track { background: transparent; } #tm-results-container::-webkit-scrollbar-thumb { background: #d9d9d9; border-radius: 3px; } #tm-results-container::-webkit-scrollbar-thumb:hover { background: #bfbfbf; } #tm-results-table { width: 100%; border-collapse: collapse; font-size: 13px; background: #fff;} #tm-results-table th, #tm-results-table td { padding: 12px 16px; text-align: left; border-bottom: 1px solid #f0f0f0;} #tm-results-table th { background: #fafafa; position: sticky; top: 0; z-index: 10; color: #8c8c8c; font-weight: 500; font-size: 12px; } #tm-results-table th::after { content: ''; position: absolute; left: 0; bottom: 0; width: 100%; border-bottom: 1px solid #f0f0f0; } #tm-results-table tbody tr { transition: background 0.3s; } #tm-results-table tbody tr:hover { background: #fff0f6; } .tm-badge { display: inline-block; padding: 2px 8px; border-radius: 12px; font-weight: bold; font-size: 12px; text-align: center; min-width: 28px; } .tm-stock-in { background: #f6ffed; color: #52c41a; border: 1px solid #b7eb8f; } .tm-stock-out { background: #fff2f0; color: #ff4d4f; border: 1px solid #ffccc7; } .tm-name-cell { color: #262626; font-weight: 500; line-height: 1.5; } /* 拖拽防干扰层 */ .tm-dragging-overlay { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; z-index: ${CONFIG.UI.Z_INDEX - 2}; cursor: move; } `); // ================= UI 注入 ================= const ball = document.createElement('div'); ball.id = 'tm-search-ball'; ball.innerHTML = ``; document.body.appendChild(ball); const panel = document.createElement('div'); panel.id = 'tm-search-panel'; panel.innerHTML = `
高级库存搜 ×
总 0, 有货 0
翻页拉取中,正在合并计算...
商品名称实际库存
输入关键词后开始搜索
`; document.body.appendChild(panel); const dragOverlay = document.createElement('div'); dragOverlay.className = 'tm-dragging-overlay'; dragOverlay.style.display = 'none'; document.body.appendChild(dragOverlay); // ================= DOM 元素缓存 ================= const dom = { ball: document.getElementById('tm-search-ball'), panel: document.getElementById('tm-search-panel'), header: document.getElementById('tm-search-header'), close: document.getElementById('tm-search-close'), input: document.getElementById('tm-input-keyword'), btnSearch: document.getElementById('tm-btn-search'), filter: document.getElementById('tm-filter-stock'), summary: document.getElementById('tm-summary-text'), sort: document.getElementById('tm-sort-select'), loading: document.getElementById('tm-loading-wrapper'), tbody: document.getElementById('tm-results-tbody'), tableContainer: document.getElementById('tm-results-container') }; // ================= 拖拽逻辑 (防 Iframe 干扰升级版) ================= let isDragging = false, dragStartX = 0, dragStartY = 0, initialLeft = 0, initialTop = 0; dom.header.addEventListener('mousedown', (e) => { if(e.target.id === 'tm-search-close') return; isDragging = true; dragStartX = e.clientX; dragStartY = e.clientY; const rect = dom.panel.getBoundingClientRect(); if(dom.panel.style.right) { dom.panel.style.right = 'auto'; dom.panel.style.bottom = 'auto'; } initialLeft = rect.left; initialTop = rect.top; dom.panel.classList.remove('tm-show-anim'); dom.panel.style.transform = 'none'; dom.panel.style.left = initialLeft + 'px'; dom.panel.style.top = initialTop + 'px'; dragOverlay.style.display = 'block'; e.preventDefault(); }); document.addEventListener('mousemove', (e) => { if (!isDragging) return; let newLeft = initialLeft + (e.clientX - dragStartX); let newTop = initialTop + (e.clientY - dragStartY); const maxX = window.innerWidth - dom.panel.offsetWidth; const maxY = window.innerHeight - dom.header.offsetHeight; dom.panel.style.left = Math.max(0, Math.min(newLeft, maxX)) + 'px'; dom.panel.style.top = Math.max(0, Math.min(newTop, maxY)) + 'px'; }); document.addEventListener('mouseup', () => { if (isDragging) { isDragging = false; dragOverlay.style.display = 'none'; } }); // ================= 面板开关逻辑 ================= const closePanel = () => { dom.panel.style.opacity = '0'; setTimeout(() => dom.panel.style.display = 'none', 300); }; const openPanel = () => { dom.panel.style.display = 'flex'; dom.panel.offsetHeight; // 强制重绘 dom.panel.style.opacity = '1'; if (!dom.panel.style.left) dom.panel.classList.add('tm-show-anim'); dom.input.focus(); }; // 🌟 修复首次点击必须要点两次的 Bug (将 display !== 'none' 修改为 display === 'flex') dom.ball.addEventListener('click', () => dom.panel.style.display === 'flex' ? closePanel() : openPanel()); dom.close.addEventListener('click', closePanel); // ================= 网络请求与数据处理 ================= async function fetchSearchAPI(keyword) { let allData = [], page = 1; while (page <= CONFIG.MAX_PAGES) { try { const data = await new Promise((resolve, reject) => { unsafeWindow.$.ajax({ url: '/b2cSaleorder/queryCommodityInfoList', type: 'GET', data: { lastlevel: '', spxxname: keyword, page: page, rows: CONFIG.ROWS_PER_PAGE }, success: (res) => resolve(typeof res === 'string' ? JSON.parse(res) : res), error: (err) => reject(err) }); }); const currentRows = data.rows || []; allData = allData.concat(currentRows); if (currentRows.length === 0 || allData.length >= (data.total || 0)) break; page++; } catch (error) { console.error(`翻页请求出错 (第${page}页):`, error); break; } } return allData; } async function executeAdvancedSearch(inputText) { const terms = inputText.trim().split(/\s+/).filter(t => t); if (terms.length === 0) return []; let allTermResults = []; for (let term of terms) { const upper = term.toUpperCase(); const lower = term.toLowerCase(); // 并发拉取大写和小写结果 const fetchTasks = [fetchSearchAPI(upper)]; if (upper !== lower) fetchTasks.push(fetchSearchAPI(lower)); const resultsArray = await Promise.all(fetchTasks); // 合并当前词的所有结果并根据 ID 去重 const uniqueMap = new Map(); resultsArray.flat().forEach(item => { const id = getUid(item); if (id) uniqueMap.set(id, item); }); allTermResults.push(Array.from(uniqueMap.values())); } // 求所有词组的交集 let finalIntersection = allTermResults[0]; for (let i = 1; i < allTermResults.length; i++) { const currentSetIds = new Set(allTermResults[i].map(getUid)); finalIntersection = finalIntersection.filter(item => currentSetIds.has(getUid(item))); } return finalIntersection; } function updateSummaryText(dataList) { const total = dataList.length; const inStock = dataList.filter(item => getStock(item) > 0).length; dom.summary.innerText = `总 ${total}, 有货 ${inStock}`; dom.summary.style.color = total === 0 ? '#ff4d4f' : '#af52de'; } function processData(dataList) { let list = dom.filter.checked ? dataList.filter(item => getStock(item) > 0) : [...dataList]; const sortMode = dom.sort.value; if (sortMode === 'stockDesc') list.sort((a, b) => getStock(b) - getStock(a)); else if (sortMode === 'stockAsc') list.sort((a, b) => getStock(a) - getStock(b)); else if (sortMode === 'nameAsc') list.sort((a, b) => (a.spxxname || '').localeCompare(b.spxxname || '')); return list; } function renderTable(dataList) { dom.tbody.innerHTML = ''; if (dataList.length === 0) { dom.tbody.innerHTML = `未找到符合条件的商品`; return; } const fragment = document.createDocumentFragment(); dataList.forEach(item => { const tr = document.createElement('tr'); const stockNum = getStock(item); const badgeClass = stockNum > 0 ? 'tm-stock-in' : 'tm-stock-out'; const safeName = escapeHTML(item.spxxname || '未知商品'); tr.innerHTML = ` ${safeName} ${stockNum} `; fragment.appendChild(tr); }); dom.tbody.appendChild(fragment); } // ================= 主控流程 ================= let rawDataCache = []; async function handleSearch() { if (typeof unsafeWindow.$ === 'undefined') { alert("错误:未检测到原网站 jQuery 依赖,系统可能已升级,脚本暂时失效。"); return; } const val = dom.input.value; if (!val.trim()) return; dom.btnSearch.disabled = true; dom.btnSearch.innerText = '检索中...'; dom.tableContainer.style.display = 'none'; dom.loading.style.display = 'flex'; dom.summary.innerText = '统计中...'; dom.summary.style.color = '#8c8c8c'; try { rawDataCache = await executeAdvancedSearch(val); updateSummaryText(rawDataCache); renderTable(processData(rawDataCache)); } catch (e) { console.error(e); dom.tbody.innerHTML = `网络异常,请查看控制台报错`; dom.summary.innerText = `请求失败`; dom.summary.style.color = '#ff4d4f'; } finally { dom.btnSearch.disabled = false; dom.btnSearch.innerText = '检索数据'; dom.loading.style.display = 'none'; dom.tableContainer.style.display = 'block'; } } // 事件绑定 dom.btnSearch.addEventListener('click', handleSearch); dom.input.addEventListener('keydown', (e) => e.key === 'Enter' && handleSearch()); dom.filter.addEventListener('change', () => rawDataCache.length && renderTable(processData(rawDataCache))); dom.sort.addEventListener('change', () => rawDataCache.length && renderTable(processData(rawDataCache))); })();