// ==UserScript==
// @name Boss QCOS 超级老板 高级商品搜索辅助
// @namespace http://tampermonkey.net/
// @version 3.2
// @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 }
};
// ================= 工具函数 =================
const escapeHTML = (str) => {
return (str || '').toString()
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
};
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: 480px; 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-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-btn-add {
width: 24px; height: 24px; border-radius: 6px; border: none;
background: linear-gradient(135deg, #ff2d55, #af52de);
color: white; cursor: pointer; font-weight: bold; font-size: 16px;
display: flex; align-items: center; justify-content: center;
transition: transform 0.2s;
}
.tm-btn-add:hover { transform: scale(1.15); }
.tm-btn-add:active { transform: scale(0.9); }
.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; }
#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-table { width: 100%; border-collapse: collapse; font-size: 13px; background: #fff;}
#tm-results-table th, #tm-results-table td { padding: 12px 10px; text-align: left; border-bottom: 1px solid #f0f0f0;}
#tm-results-table th { background: #fafafa; position: sticky; top: 0; z-index: 10; color: #8c8c8c; font-size: 12px; }
#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 = `
`;
document.body.appendChild(panel);
const dragOverlay = document.createElement('div');
dragOverlay.className = 'tm-dragging-overlay';
dragOverlay.style.display = 'none';
document.body.appendChild(dragOverlay);
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')
};
// ================= 核心添加逻辑 (对接系统) =================
const addToSalesList = (item) => {
const jq = unsafeWindow.$;
const salesBilling = unsafeWindow.salesBilling;
// 1. 检查是否已存在
if (unsafeWindow.checkSpxxIsExist(item.spxxno)) {
// 如果已存在,直接调用原系统的增加数量逻辑 (参考原代码 addSpxx)
salesBilling.addSpxx(item.barCode || " ");
return;
}
// 2. 找到一个空的行,或者创建新行
let targetTr = null;
jq('.sales-tr').each(function() {
const currentSpxx = jq(this).find('input[name=spxxno]').val();
if (!currentSpxx || currentSpxx === "") {
targetTr = jq(this);
return false;
}
});
// 如果没有空行,点击系统的“添加商品”按钮
if (!targetTr) {
jq('.add-goods').click();
targetTr = jq('.sales-tr:last');
}
// 3. 填充数据 (完全模拟原系统 comfirm 按钮逻辑)
const rownum = targetTr.find('input[type=hidden]:first').attr('rownum');
jq('#totalprice' + rownum).html(item.lsprice);
jq("#spxxname" + rownum).val(item.spxxname);
jq("#spxxno" + rownum).val(item.spxxno);
jq('#spmodel' + rownum).val(item.spmodel || "");
if (item.lsprice != 0) {
jq("#spprice" + rownum).val(item.lsprice);
}
const barCodeVal = (item.barCode === "" || !item.barCode) ? " " : item.barCode;
jq("#barCode" + rownum).val(barCodeVal);
jq("#barCode" + rownum).attr("disabled", true);
jq("#categoryTotalPrice" + rownum).text(item.lsprice);
jq("#spnum" + rownum).val("1");
// 更新总价相关
jq("#ysmoney").val(item.lsprice);
jq("#totalprice").val(item.lsprice);
// 4. 触发系统计算
salesBilling.getTotal();
// 5. 模拟金额计算逻辑 (原系统 inline 代码)
if (jq.trim(jq("#ysmoney").val()) == '') {
jq("#yhmoney").val("");
jq("#pay_money").val("");
}
jq("#hgjCoupon div").removeClass("issued-coupon");
jq("#paylist").val("");
jq("#ysmoney").val(jq("#totalprice").val());
jq("#pay_money").val(jq("#totalprice").val());
jq("#couponmoney").val("");
jq("#couponid").val("");
jq("#yhmoney").text(parseFloat(jq('#total-price').text()) - jq('#ysmoney').val());
// 6. 自动滚动到表格底部方便查看
const scrollDiv = document.getElementById('scrolldIV');
if(scrollDiv) scrollDiv.scrollTop = scrollDiv.scrollHeight;
};
// ================= 搜索与渲染逻辑 =================
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) { 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);
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 renderTable(dataList) {
dom.tbody.innerHTML = '';
if (dataList.length === 0) {
dom.tbody.innerHTML = `| 未找到符合条件的商品 |
`;
return;
}
dataList.forEach(item => {
const tr = document.createElement('tr');
const stockNum = getStock(item);
const badgeClass = stockNum > 0 ? 'tm-stock-in' : 'tm-stock-out';
// 操作列:有货显示+号按钮
const actionHtml = stockNum > 0 ? `` : `缺货`;
tr.innerHTML = `
${actionHtml} |
${escapeHTML(item.spxxname)} |
${stockNum} |
`;
// 绑定添加事件
const addBtn = tr.querySelector('.tm-btn-add');
if(addBtn) {
addBtn.onclick = (e) => {
e.stopPropagation();
addToSalesList(item);
};
}
dom.tbody.appendChild(tr);
});
}
// ================= 拖拽与辅助逻辑 =================
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();
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;
dom.panel.style.left = (initialLeft + (e.clientX - dragStartX)) + 'px';
dom.panel.style.top = (initialTop + (e.clientY - dragStartY)) + 'px';
});
document.addEventListener('mouseup', () => { if (isDragging) { isDragging = false; dragOverlay.style.display = 'none'; } });
const togglePanel = () => {
if (dom.panel.style.display === 'flex') {
dom.panel.style.opacity = '0';
setTimeout(() => dom.panel.style.display = 'none', 300);
} else {
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();
}
};
dom.ball.onclick = togglePanel;
dom.close.onclick = togglePanel;
// ================= 搜索主控 =================
let rawDataCache = [];
async function handleSearch() {
const val = dom.input.value;
if (!val.trim()) return;
dom.btnSearch.disabled = true;
dom.loading.style.display = 'flex';
dom.tableContainer.style.display = 'none';
try {
rawDataCache = await executeAdvancedSearch(val);
const total = rawDataCache.length;
const inStock = rawDataCache.filter(item => getStock(item) > 0).length;
dom.summary.innerText = `总 ${total}, 有货 ${inStock}`;
renderTable(processData(rawDataCache));
} finally {
dom.btnSearch.disabled = false;
dom.loading.style.display = 'none';
dom.tableContainer.style.display = 'block';
}
}
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;
}
dom.btnSearch.onclick = handleSearch;
dom.input.onkeydown = (e) => e.key === 'Enter' && handleSearch();
dom.filter.onchange = () => rawDataCache.length && renderTable(processData(rawDataCache));
dom.sort.onchange = () => rawDataCache.length && renderTable(processData(rawDataCache));
})();