// ==UserScript==
// @name 挂刀网优化
// @namespace https://gf.qytechs.cn/users/1362311
// @version 1.1.0
// @description 优化界面。优化表格价格列展示信息,增加数据更新时间;支持配置查询参数首次进入时自动查询;增加挂刀比例计算器;
// @author honguangli
// @license MIT
// @match https://www.hangknife.com/
// @icon https://www.hangknife.com/static/imgs/logo_home_black.png
// @run-at document-end
// @grant GM_addStyle
// ==/UserScript==
(function() {
'use strict';
const routerHash = '#/Home'; // 需要开启优化的页面
let state = true; // 是否默认开启优化
const updateDurationMinutes = 30; // 期望更新时间间隔,单位为分钟,当前时间与更新时间差值小于此配置时会标黄色
// csgo配置
const csgoOption = {
name: 'csgo',
selector: '#pane-csgo .el-table', // 表格选择器
priceColumnIndex: 15, // 价格列下标
search: true, // 是否自定义查询
searchParam: {
orderBy: 'minPrice / steamSafeBuyerPrice,ascending', // 排序方式
minPrice: '1', // 最低价
maxPrice: '', // 最高价
last24Volume: '', // 成交数
platform: ["uupin"], // 交易平台
},
minWidth: 190, // 表格价格列最小宽度
_isInit: false, // 是否已初始化
_isSearch: false, // 是否已触发自定义查询
_width: undefined, // 表格价格列宽度,缓存初始值,用于恢复默认样式
_minWidth: undefined, // 表格价格列最小宽度,缓存初始值,用于恢复默认样式
};
// dota2配置
const dota2Option = {
name: 'dota2',
selector: '#pane-dota2 .el-table', // 表格选择器
priceColumnIndex: 9, // 价格列下标
search: true, // 是否自定义查询,仅执行一次
searchParam: {
orderBy: 'minPrice / steamSafeBuyerPrice,ascending', // 排序方式
minPrice: '1', // 最低价
maxPrice: '', // 最高价
last24Volume: '', // 成交数
platform: ["buff"], // 交易平台
},
minWidth: 190, // 表格价格列最小宽度
_isInit: false, // 是否已初始化
_isSearch: false, // 是否已触发自定义查询
_width: undefined, // 表格价格列宽度,缓存初始值,用于恢复默认样式
_minWidth: undefined, // 表格价格列最小宽度,缓存初始值,用于恢复默认样式
};
// csgo 查询参数
// 交易平台可选项
// ['buff', 'dmarket', 'c5game', 'skinPort', 'igxe', 'uupin', 'v5Item', 'csMoney', 'waxpeer', 'eco', 'bitSkins', 'haloskins']
// 排序方式可选项
// {value: 'minPrice / steamSellerPrice,ascending', label: '最优寄售'}
// {value: 'minPrice / steamBuyerPrice,ascending', label: '最优求购'}
// {value: 'minPrice / steamSafeBuyerPrice,ascending', label: '稳定成交'}
// {value: 'minPrice,ascending', label: '底价升序'}
// {value: 'minPrice / steamSellerPrice,descending', label: '第三方寄售价降序'}
// {value: '(buffBuyOrderPrice - minPrice)/minPrice,descending', label: '低于Buff求购'}
// {value: '(uupinBuyerPrice - minPrice)/minPrice,descending', label: '低于UU求购'}
// {value: '(dmarketOrderPrice - minPrice)/minPrice,descending', label: '低于DMarket求购'}
// 隐藏原生的折扣内容
// 通过改变DOM属性实现
GM_addStyle('tbody > tr > td:last-child > div.cell[data-optimize="true"] > div:not([data-type="optimize"]) { display: none; }');
// 优化成交数量输入框尺寸
GM_addStyle('.el-tabs .el-tab-pane > div > div > div > div:nth-child(2) > div:nth-child(2) > div:first-child > div:first-child { width: 185px !important; }');
GM_addStyle('.el-tabs .el-tab-pane > div > div > div > div:nth-child(2) > div:nth-child(2) > div:first-child > div:first-child > div:first-child { padding: 0 12px; }');
let btnOptimize; // 优化按钮
// 初始化
const init = () => {
// 重置配置
csgoOption._isInit = false;
csgoOption._isSearch = false;
csgoOption._width = undefined;
csgoOption._minWidth = undefined;
dota2Option._isInit = false;
dota2Option._isSearch = false;
dota2Option._width = undefined;
dota2Option._minWidth = undefined;
// 重新监听标签页
observerTab();
// 重新插入按钮
insertOptimizeBtn();
// 重新插入挂刀比例计算器
insertCalculator();
}
// 监听页面变化
// 匹配路由成功后插入优化按钮
const observerApp = () => {
// 获取app节点
const domApp = document.getElementById('app');
const app = domApp.__vue__;
// 开启监听
const observer = new MutationObserver(() => {
// 等待DOM刷新
app.$nextTick(() => {
if (location.hash === routerHash) {
init();
}
});
});
observer.observe(domApp, { childList: true });
return observer;
};
// 监听标签页变化
// 触发一次优化功能
const observerTab = () => {
// 获取tab节点
const domTab = document.getElementById('tab-csgo');
const tab = domTab.__vue__;
// 开启监听
const observer = new MutationObserver(() => {
// 重新触发优化
changeState(state);
});
observer.observe(domTab, { attributes: true });
return observer;
};
// 监听表格变化
// 触发优化功能
// @param {element} tableElement 表格节点
// @param {object} table 表格vue组件
// @param {function} callback 更新回调
const observerTable = (tableElement, table, callback) => {
// 开启监听,当表格属性变化时触发优化
const observer = new MutationObserver(() => {
// 优化已关闭
if (!state) {
//observer.disconnect();
return;
}
// 等待DOM刷新后触发优化
table.$nextTick(callback);
});
observer.observe(tableElement, { attributes: true });
};
// 插入优化按钮
const insertOptimizeBtn = () => {
// 获取右侧导航栏
const domRightMenu = document.getElementsByClassName('avatarDiv')[0];
// 添加优化按钮
btnOptimize = document.createElement('button');
btnOptimize.appendChild(document.createTextNode('自定义优化:关闭'));
btnOptimize.classList.add('el-button', 'el-button--small');
btnOptimize.style.setProperty('margin-right', '8px');
btnOptimize.onclick = function () {
state = !state;
changeState(state);
};
domRightMenu.insertBefore(btnOptimize, domRightMenu.children[0]);
// 是否默认开启优化
if (state) {
changeState(state);
}
};
// 插入挂刀比例计算器
const insertCalculator = () => {
const row = document.querySelector('.el-main');
if (!row) {
return;
}
const h = `<div style="display: flex; justify-content: end; align-items: center; column-gap: 12px; margin-bottom: 20px;">
<div class="el-input el-input--small el-input-group el-input-group--prepend" style="width: 100px;">
<div class="el-input-group__prepend">第三方平台购入价</div>
<input id="calculator-buy-price" type="text" autocomplete="off" class="el-input__inner" style="width: 100px;">
</div>
<div class="el-input el-input--small el-input-group el-input-group--prepend" style="width: 100px;">
<div class="el-input-group__prepend">Steam出售价</div>
<input id="calculator-sell-price" type="text" autocomplete="off" class="el-input__inner" style="width: 100px;">
</div>
<div class="el-input el-input--small el-input-group el-input-group--prepend is-disabled" style="width: 100px;">
<div class="el-input-group__prepend">到手余额</div>
<input id="calculator-steam-balance" type="text" autocomplete="off" class="el-input__inner" style="width: 100px; color: #67c23a;" disabled>
</div>
<div class="el-input el-input--small el-input-group el-input-group--prepend is-disabled" style="width: 100px;">
<div class="el-input-group__prepend">比例</div>
<input id="calculator-discount" type="text" autocomplete="off" class="el-input__inner" style="width: 100px;" disabled>
</div>
<div>
`;
row.insertAdjacentHTML('afterBegin', h);
// TODO 插入到表格上方,切换页面时支持恢复
// 计算折扣
const calculateDiscount = (buyPrice, sellPrice) => {
buyPrice = parseFloat(buyPrice);
sellPrice = parseFloat(sellPrice);
// 计算方式保守,可能与实际到手余额低1分
// steam手续费15%
// 到手余额保留2位小数后向下取整
// 折扣保留3位小数后向上取整
const balance = Math.floor(sellPrice / 1.15 * 100) / 100;
const discount = Math.ceil(buyPrice / balance * 1000) / 1000;
const inputSteamBalance = document.getElementById('calculator-steam-balance');
const inputDiscount = document.getElementById('calculator-discount');
inputSteamBalance.value = balance.toFixed(2);
inputDiscount.value = discount.toFixed(3);
if (discount <= 0.7) {
inputDiscount.style.setProperty('color', '#67c23a');
} else if (discount <= 0.8) {
inputDiscount.style.setProperty('color', '#409eff');
} else if (discount <= 0.9) {
inputDiscount.style.setProperty('color', '#e6a23c');
} else {
inputDiscount.style.setProperty('color', '#f56c6c');
}
};
const inputBuyPrice = document.getElementById('calculator-buy-price');
const inputSellPrice = document.getElementById('calculator-sell-price');
inputBuyPrice.addEventListener('input', (e) => {
calculateDiscount(inputBuyPrice.value, inputSellPrice.value);
});
inputSellPrice.addEventListener('input', (e) => {
calculateDiscount(inputBuyPrice.value, inputSellPrice.value);
});
};
// 优化状态变更
const changeState = (state) => {
if (!state) {
// 关闭
btnOptimize.innerText = '自定义优化:关闭';
btnOptimize.classList.remove('el-button--success');
reset(csgoOption);
reset(dota2Option);
} else {
// 开启
btnOptimize.innerText = '自定义优化:开启';
btnOptimize.classList.add('el-button--success');
optimize(csgoOption);
optimize(dota2Option);
}
};
// 优化
const optimize = (option) => {
const tableElement = document.querySelector(option.selector);
if (!tableElement) {
return;
}
const table = tableElement.__vue__;
if (!table) {
return;
}
// 初始化
if (!option._isInit) {
option._isInit = true;
// 缓存参数
option._width = table.columns[option.priceColumnIndex].width;
option._minWidth = table.columns[option.priceColumnIndex].minWidth;
// 监听表格变化
observerTable(tableElement, table, () => {
// 仅执行一次自定义查询
if (option.search && !option._isSearch) {
option._isSearch = true;
Object.entries(option.searchParam).forEach(item => {
table.$parent.initPage[item[0]] = item[1];
});
table.$parent.queryItemList();
return;
}
optimizeCell(table, option);
});
} else {
// 执行优化
optimizeCell(table, option);
}
};
// 优化表格
// @param {object} option 配置
const optimizeCell = (table, option) => {
// 开启表格纵向边框,开启后可以拖动改变列宽度
table.border = true;
// 设置折扣列宽度,需清除width并设置最小宽度,否则宽度占不满则会出现空白列
table.columns[option.priceColumnIndex].width = undefined;
table.columns[option.priceColumnIndex].minWidth = option.minWidth;
// 修改UI后需重新渲染表格布局
table.doLayout();
// 移除优化生成的内容
const domOptimizes = table.$el.querySelectorAll(`tbody > tr > td:nth-child(${ option.priceColumnIndex+1 }) > div.cell > div[data-type="optimize"]`);
for (let i = 0; i < domOptimizes.length; i++) {
domOptimizes[i].parentNode.removeAttribute('data-optimize');
domOptimizes[i].remove();
}
// 找到每行的折扣内容
const cells = table.$el.querySelectorAll(`tbody > tr > td:nth-child(${ option.priceColumnIndex+1 }) > div.cell`);
const nowUnix = Math.round(new Date() / 1000);
// 遍历每行生成优化内容
const data = table.data;
for (let i = 0; i < data.length; i++) {
const h = `
<div data-type="optimize">
<div style="display: flex;">
<div style="min-width: 33px;">寄售:</div>
<div style="flex: 1">${ table.$parent.formatterPrice(data[i].steamSellerPrice) } → ${ table.$parent.formatterPrice(data[i].steamSellerPrice / 1.15) }</div>
<span class="el-tag el-tag--${ table.$parent.formatterTagType(data[i].lowestDisCount * 1.15) } el-tag--mini el-tag--plain">${ (data[i].lowestDisCount * 1.15).toFixed(3) }</span>
</div>
<div style="display: flex;">
<div style="min-width: 33px;">求购:</div>
<div style="flex: 1">${ table.$parent.formatterPrice(data[i].steamBuyerPrice) } → ${ table.$parent.formatterPrice(data[i].steamBuyerPrice / 1.15) }</div>
<span class="el-tag el-tag--${ table.$parent.formatterTagType(data[i].buyerPriceDisCount * 1.15) } el-tag--mini el-tag--plain">${ (data[i].buyerPriceDisCount * 1.15).toFixed(3) }</span>
</div>
<div style="display: flex;">
<div style="min-width: 33px;">稳定:</div>
<div style="flex: 1">${ table.$parent.formatterPrice(data[i].steamSafeBuyerPrice) } → ${ table.$parent.formatterPrice(data[i].steamSafeBuyerPrice / 1.15) }</div>
<span class="el-tag el-tag--${ table.$parent.formatterTagType(data[i].safeBuyerPriceDisCount * 1.15) } el-tag--mini el-tag--plain">${ (data[i].safeBuyerPriceDisCount * 1.15).toFixed(3) }</span>
</div>
<div style="display: flex;">
<div style="min-width: 33px;">更新:</div>
<div style="${ nowUnix - Math.round(new Date(data[i].updateTime)/1000) < updateDurationMinutes * 60 ? 'color: #e6a23c' : '' }">${ data[i].updateTime }</div>
</div>
</div>`;
cells[i].insertAdjacentHTML('beforeEnd', h);
cells[i].setAttribute('data-optimize', 'true');
}
};
// 重置
// @param {object} option 配置
const reset = (option) => {
const tableElement = document.querySelector(option.selector);
if (!tableElement) {
return;
}
const table = tableElement.__vue__;
if (!table) {
return;
}
// 关闭表格纵向边框
table.border = false;
// 重置列宽度
table.columns[option.priceColumnIndex].width = option._width;
table.columns[option.priceColumnIndex].minWidth = option._minWidth;
// 修改UI后需重新渲染表格布局
table.doLayout();
// 移除优化生成的内容
const domOptimizes = table.$el.querySelectorAll(`tbody > tr > td:nth-child(${ option.priceColumnIndex+1 }) > div.cell > div[data-type="optimize"]`);
for (let i = 0; i < domOptimizes.length; i++) {
domOptimizes[i].parentNode.removeAttribute('data-optimize');
domOptimizes[i].remove();
}
};
// 监听页面
observerApp();
// 初始化
init();
})();