Greasy Fork is available in English.

淘宝、京东、天猫自动按每斤价格排序 TAOBAO/JD/TMALL / Automatic sort by 500g price.

已知bug:淘宝的价格和商品标题上写的重量通常对不上,此bug无法修复,天猫、京东暂无此问题, 标题出现2个以上重量单位的按最后一个算 ( bug反馈联系: snomiao@gmail.com 或 qq 997596439 )

// ==UserScript==
// @name         淘宝、京东、天猫自动按每斤价格排序 TAOBAO/JD/TMALL / Automatic sort by 500g price.
// @namespace    snomiao@gmail.com
// @version      1.1.4
// @description  已知bug:淘宝的价格和商品标题上写的重量通常对不上,此bug无法修复,天猫、京东暂无此问题, 标题出现2个以上重量单位的按最后一个算 ( bug反馈联系: snomiao@gmail.com 或 qq 997596439 )
// @author       snomiao@gmail.com
// @match        http*://cart.jd.com/cart*
// @match        http*://order.jd.com/center/alwaysbuy.action*
// @match        http*://*.jd.com/*.html
// @match        http*://*.jd.com/*.html?*
// @match        http*://search.jd.com/Search*
// @match        http*://*.tmall.com/*.htm*
// @match        http*://*.taobao.com/search*
// @match        http*://cart.taobao.com/cart.htm*
// @match        http*://www.1688.com/*
// @match        http*://s.1688.com/selloffer/offer_search.htm*
// @match        http*://search.suning.com/*
// @match        http*://*.amazon.*/*
// @grant        none
// ==/UserScript==

//
// (20210221)更新:性能优化、数据单位识别优化、更新天猫超市、amazon、suning、加入中文数字识别
// (20200404)更新:增加天猫超市支持、优化刷新逻辑
//
// (function () {
//     'use strict';
/*
获取质量参数
前缀乘数 基数量 基数单位 后缀乘数
获取
 */
var 单位比例表 = {
    // 重量、容积单位(按水的重量算)
    ton: 1e6, kg: 1e3, g: 1, 克: 1, mg: 1e-3, ug: 1e-6,
    l: 1e3, ml: 1,
    千克: 1e3,
    磅: 453.59237, lb: 453.59237,
    吨: 1e6, 公斤: 1e3, 斤: 500, 两: 50,
    // 数据单位(采用硬盘工业单位)
    t: 1e3, g: 1, m: 1e-3, k: 1e-6,
    pb: 1e6, tb: 1e3, gb: 1, mb: 1e-3, kb: 1e-6,
}

// 前缀乘数? 基数量 基数单位 后缀乘数?
var 质量正则 = RegExp([
    /(?:(\d+)\s?\*)?/,
    /(\d+\.\d+|\d+)/,
    RegExp(`(${Object.keys(单位比例表).join('|')})`),
    /(?:\s?[x×*](\d+))?/,
].map(e => e.source).join(''), 'i')
var 分组计数 = (列, 按 = e => JSON.stringify(e)) => 列.reduce((表, 数) => (表[数] = (表[数] || 0) + 1, 表), {})
var 众数 = (列) => Object.entries(分组计数(列)).sort(([, v1], [, v2]) => v2 - v1)[0][0]
var 中文数字解析 = 大写数字 => 大写数字.split('').reduce((数, 字) =>
    // (e=> e !==-1 && (数 ?? 0) + e)('012345789'.indexOf(字) ) ||
    (e => e !== -1 && (数 ?? 0) + e)('零一二三四五六七八九'.indexOf(字)) ||
    (e => e !== -1 && (数 ?? 0) + e)('零壹贰叁肆伍陆柒捌玖'.indexOf(字)) ||
    (e => e !== -1 && (数 ?? 0) + e)('洞幺两三四五六拐怕勾'.indexOf(字)) ||
    (e => e !== -1 && (数 ?? 1) * 10 ** e)("个十百千万一兆一亿".indexOf(字)) ||
    (e => e !== -1 && (数 ?? 1) * 10 ** e)("个拾佰仟".indexOf(字)) ||
    NaN, null)
var 中文数字替换 = 串 => 串.replace(
    /[幺两三四五六拐怕勾洞零一二三四五六七八九零壹贰叁肆伍陆柒捌玖个十][幺两三四五六拐怕勾洞零一二三四五六七八九零壹贰叁肆伍陆柒捌玖个十百千万一兆一亿个拾佰仟]*/g,
    中文数字解析)
中文数字替换(`四万万 四亿 四亿亿 二万二千 二百二 三十三 四百 六十四 五百亿 两个亿 十一`)

var 质量千克自标题解析 = (标题) => {
    var 质量表述列 = (中文数字替换(标题) + 标题).match(RegExp(质量正则.source, 'ig')) || []
    var 质量列 = 质量表述列.map(串 => {
        const [_, 前乘数串, 质量串, 单位串, 后乘数串] = 串.match(质量正则)
        const [前乘数, 后乘数] = [前乘数串, 后乘数串].map(e => parseFloat(e) || 1)
        const 质量 = parseFloat(质量串) || 0
        const 单位乘数 = 单位比例表[单位串.toLowerCase()]
        return 前乘数 * 质量 * 单位乘数 * 后乘数 / 1e3 //kg
    })
    return 质量列.length ? parseFloat(众数(质量列)) : NaN
}
var 查看 = e => (console.log(e), e)
var 每千克价格按每斤解释 = (每千克价格) => (每千克价格 / 2).toFixed(2) + "¥/500g"
var 范围映射 = (x, [a, b], [c, d]) => (x - a) / (b - a) * (d - c) + c
var 页面特定商品列获取 = ({ 选项目, 选标题, 选价格 }) =>
    [...document.querySelectorAll(选项目)].map(元素 => {
        var [标题元素, 价格元素] = [选标题, 选价格].map(选 =>
            选?.startsWith('@@') ? document.querySelector(选?.slice(2)) : 元素?.querySelector(选))
        if (!标题元素 || !价格元素) return null
        var 标题 = 标题元素.innerText.trim()
        var 价格 = parseFloat(价格元素.innerText.trim().replace(/¥|¥/g, "")) || NaN //无报价
        var 千克质量 = 质量千克自标题解析(标题)
        var 每千克价格 = 价格 / (千克质量 || 0)
        return { 标题, 价格, 千克质量, 每千克价格, 标题元素, 价格元素, 元素 }
    }).filter(e => e)
var 新元素 = (innerHTML, attributes = {}) =>
    Object.assign(Object.assign(document.createElement("div"), { innerHTML }).children[0], attributes)
var 商品列每斤价格排序显示 = (新增商品列) => {
    console.log('[pricesof500g] 正在处理' + 新增商品列.length + '个商品价格。')
    var 现存商品列 = [...document.querySelectorAll('span.priceof500g')].map(价格标签 => 价格标签.商品信息)
    var 有序商品列 = [...现存商品列, ...新增商品列].sort((a, b) => a.每千克价格 - b.每千克价格)
    var 最低每千克价格 = Math.min(...有序商品列.map(e => e.每千克价格).filter(e => !isNaN(e)))
    var 最高每千克价格 = Math.max(...有序商品列.map(e => e.每千克价格).filter(e => !isNaN(e)))
    有序商品列.forEach(({ 元素 }) => 元素.parentNode.appendChild(元素.parentNode.removeChild(元素)))
    有序商品列.forEach(商品信息 => {
        const { 标题, 千克质量, 价格, 每千克价格, 标题元素 } = 商品信息
        var 价率 = 范围映射(每千克价格, [最低每千克价格, 最高每千克价格], [1, 0])
        // 从最低价到最高价由红到绿渐变
        var 颜色 = 价率 && `rgba(${价率 * 255},${255 - 价率 * 255},0.1,1)` || 'black'
        var 描述 = `${标题}\n\n${价格}¥/${千克质量}kg = ${每千克价格按每斤解释(每千克价格)}\n\n © 2016 - 2021 雪星实验室 \n  ( bug反馈联系: snomiao@gmail.com 或 qq 997596439 )`
        var 价格标签 = 新元素(`
            <span class="priceof500g" style="background: ${颜色}; color: white" title="${描述}">
                ${每千克价格按每斤解释(每千克价格)}
            </span>`)
        价格标签.商品信息 = 商品信息
        // 标签换新或显示
        标题元素.价格标签 && 标题元素.parentNode.removeChild(标题元素.价格标签)
        if (!价格 || !千克质量) return;
        标题元素.价格标签 = 标题元素.parentNode.insertBefore(价格标签, 标题元素)
        // console.debug(标题元素, 价格标签, 每千克价格按每斤解释(每千克价格))
    })
}


var 商品选择列 = `
| taobao.com | .item                       | .title a              | .price             |
| taobao.com | .item-holder                | .item-basic-info a    | .td.td-price       |
| taobao.com | .bundle                     | .item-basic-info a    | .td.td-price       |
| taobao.com | #J_OrderList>div            | .item-basic-info a    | .td.td-price       |
| tmall.com  |  .product                   | .productTitle a       | .productPrice      |
| tmall.com  | .product                    | .product-title a      | .ui-price          |
| tmall.com  | .tm-detail-meta             | .tb-detail-hd h1      | .tm-promo-price    |
| tmall.com  | .j_ItemInfo                 | .title                | .price             |
| tmall.com  | .one-grid-price             | .floor-item-title     | .floor-price       |
| tmall.com  | .wonderful-item             | .item-desc            | .item-price        |
| tmall.com  | .J_TSaleProp>li.tb-selected | a                     | @@.tm-price        | 小的商品描述标签
| tmall.com  | .tb-property                | h1                    | .tm-price          | 详情页标题
| jd.com     | .itemInfo-wrap              | .sku-name             | .p-price           | 当前浏览商品
| jd.com     | ul>li.more2_item            | .more2_info_name      | .more2_info_price  | 首页推荐
| jd.com     | .freqt-item                 | .p-name a             | .p-price           | 常购商品
| jd.com     | .gl-item                    | .p-name em            | .p-price           |
| jd.com     | .track-con>ul>li            | a>div                 | a>p                | 看了又看
| jd.com     | ul.plist>li                 | .p-name               | .p-price           | 店铺新品、店铺热销、店长推荐等
| jd.com     | ul>li.item                  | .p-name               | .p-price           | 本店好评
| jd.com     | .goods-list>ul>li           | .p-name a             | .p-price           | JD购物车
| jd.com     | .item-item,.item-full       | .p-name a             | .p-price           | JD购物车
| jd.com     | .smart-items>ul>li          | .item-name a          | .item-price        | JD购物车
| 1688.com   | .grid.rec-offer             | .offer_titles         | .price-num         | 首页
| 1688.com   | .sm-offer-item              | .sm-offer-title       | .sm-offer-priceNum | 商品搜索页面
| 1688.com   | .card-container             | .title                | div.price          | 商品搜索页面
| amazon.cn  | .s-result-item              | h2                    | .a-price           | 商品搜索页面
| suning.com | li.item-wrap                | .title-selling-point  | .price-box         | 商品搜索页面
`   .replace(/\/\/.*/gm, '').split(/\r?\n/g)
    .map(e => e.trim().split('|').slice(1).map(e => e.trim())).filter(e => e && e[1])
    .map(([域名, 选项目, 选标题, 选价格]) => ({ 域名, 选项目, 选标题, 选价格 }))
var 页面商品列获取 = () => 商品选择列.filter(({ 域名 }) => location.origin.match(域名)).flatMap(页面特定商品列获取)
var 页面商品列商品列每斤价格排序显示 = () => 商品列每斤价格排序显示(页面商品列获取())
var 节流 = (间隔, 函数, 提示函数 = () => null, 上次执行 = 0) => async (...参数) =>
    +new Date() - 上次执行 > 间隔 ? ((上次执行 = +new Date()), await 函数(...参数)) : await 提示函数(...参数)
var 防抖 = (间隔, 函数, 提示函数 = () => null, timerId = null) => (...参数) => new Promise((resolve, reject) =>
    (timerId && (clearTimeout(timerId), resolve(提示函数(...参数))), timerId = setTimeout(() => resolve(函数(...参数)), 间隔)))
var 节流防抖 = (间隔, 函数, 提示函数 = () => null) => 节流(间隔, 函数, 防抖(间隔, 函数, 提示函数))
var 刷新函数 = 节流防抖(10e3 /* 3s */, () => !document.hidden && 页面商品列商品列每斤价格排序显示())
var 主动刷新函数 = 节流防抖(200, () => !document.hidden && 页面商品列商品列每斤价格排序显示())

// オブザーバインスタンスを作成
var 目标 = document.documentElement || document.body
var 监视配置 = { attributes: false, childList: true, characterData: false };
if (typeof 页面变动监视器 !== 'undefined') 页面变动监视器.disconnect()
var 页面变动监视器 = new MutationObserver((mutations) => {
    if (!mutations.some(record => record.addedNodes.length)) return;
    页面变动监视器.disconnect(); 刷新函数(); 目标 && 页面变动监视器.observe(目标, 监视配置)
}); 页面变动监视器.observe(目标, 监视配置)
window.addEventListener('load', 刷新函数, false)
刷新函数()
document.addEventListener('keyup', () => setTimeout(刷新函数, 100), false)
document.addEventListener('mouseup', () => setTimeout(主动刷新函数, 100), false)
document.addEventListener('visibilitychange', () => setTimeout(刷新函数, 100), false)

// })();