Greasy Fork

Greasy Fork is available in English.

淘宝搜索PLUS

淘宝搜索页面自动加载店铺评分,无需鼠标悬停查看!快速查看店铺评分以及同行对比! | 搜索页面自定义排序方式与显示方式

当前为 2017-08-09 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name:zh-CN          淘宝搜索PLUS
// @name:zh-TW          淘寶搜索PLUS
// @name                taobao search plus
// @description:zh-CN   淘宝搜索页面自动加载店铺评分,无需鼠标悬停查看!快速查看店铺评分以及同行对比! | 搜索页面自定义排序方式与显示方式
// @description:zh-TW   淘寶搜索頁面自動加載店鋪評分,無需鼠標懸停查看!快速查看店鋪評分以及同行對比! | 搜索頁面自定義排序方式與顯示方式
// @description         Auto load shop information, customize the default sort order and ui style(gird or list).
// @icon                https://www.taobao.com/favicon.ico
// @version             1.0.1
// @author              olOwOlo
// @namespace           https://olowolo.com
// @homepage            https://github.com/olOwOlo/script/tree/master/taobao-search-plus
// @supportURL          https://github.com/olOwOlo/script
// @license             MIT
// @match               https://s.taobao.com/search?*
// @run-at              document-start
// @grant               GM_setValue
// @grant               GM_getValue
// @grant               GM_addStyle
// @grant               GM_registerMenuCommand
// ==/UserScript==
//
/*
 * Copyright (c) 2017 olOwOlo
 * Released under the MIT License.
 */
(function () {
  'use strict'

  const DEBUG = false
  function myLog (str) {
    if (DEBUG) window.console.log(str)
  }

  (function () {
    let globalSort = typeof GM_getValue('sort') === 'undefined' ? 'default' : GM_getValue('sort')
    let globalStyle = typeof GM_getValue('style') === 'undefined' ? 'grid' : GM_getValue('style')
    myLog(`[sort = ${GM_getValue('sort')}, style = ${GM_getValue('style')}]`)

    function parseArgument () {
      const argvMap = new Map()
      const argvArray = location.search.substr(1).split('&')
      for (const string of argvArray) {
        argvMap.set(...string.split('='))
      }
      return argvMap
    }

    const argv = parseArgument()

    let href = location.href
    if (globalSort !== 'default' && typeof argv.get('sort') === 'undefined') {
      href += `&sort=${globalSort}`
    }
    if (globalStyle !== 'grid' && typeof argv.get('style') === 'undefined') {
      href += `&style=${globalStyle}`
    }
    if (href !== location.href) {
      myLog(`redirect... Old href = [${location.href}]`)
      location.href = href
    }

    const sortOption = ['default', 'renqi-desc', 'sale-desc', 'credit-desc', 'price-asc', 'price-desc', 'total-asc', 'total-desc']
    const styleOption = ['grid', 'list']

    class Setting {
      // 传入数据{sort: sort, style: style}为有效值
      constructor (setting) {
        this.sort = setting.sort
        this.style = setting.style
      }

      toString () {
        return `[sort = ${this.sort}, style = ${this.style}]`
      }

      createUI () {
        const userInterface = document.createElement('div')
        userInterface.innerHTML = '<div class="taobao-plus-container modal-open"><div class="modal fade in" id="taobao-plus-modal" tabindex="-1" role="dialog" aria-labelledby="settingModalLabel"><div class="modal-dialog modal-sm" role="document"><div class="modal-content"><div class="modal-header"><h4 class="modal-title">搜索设置</h4></div><div class="modal-body"><p>默认排序规则</p><div class="radio"><label><input type="radio" name="sortOption" value="default">默认(综合排序)</label></div><div class="radio"><label><input type="radio" name="sortOption" value="renqi-desc">人气从高到低</label></div><div class="radio"><label><input type="radio" name="sortOption" value="sale-desc">销量从高到低</label></div><div class="radio"><label><input type="radio" name="sortOption" value="credit-desc">信用从高到低</label></div><div class="radio"><label><input type="radio" name="sortOption" value="price-asc">价格从低到高</label></div><div class="radio"><label><input type="radio" name="sortOption" value="price-desc">价格从高到低</label></div><div class="radio"><label><input type="radio" name="sortOption" value="total-asc">总价从低到高</label></div><div class="radio"><label><input type="radio" name="sortOption" value="total-desc">总价从高到低</label></div><hr/><p>默认显示风格</p><div class="radio"><label><input type="radio" name="styleOption" value="grid">默认(大部分情况下为网格)</label></div><div class="radio"><label><input type="radio" name="styleOption" value="list">列表</label></div><hr/><div style="text-align:right"><button type="button" id="submit" class="btn btn-success"> 确定</button><button type="button" id="cancel" class="btn btn-default"> 取消</button></div></div></div></div></div> '
        document.body.appendChild(userInterface)
        // display: none
        this.modal = document.getElementById('taobao-plus-modal')
        this.modal.style.display = 'none'
        // init checked
        document.getElementsByName('sortOption')[sortOption.indexOf(this.sort)].checked = true
        document.getElementsByName('styleOption')[styleOption.indexOf(this.style)].checked = true

        /** return checked value */
        function getRadioValue (radioName) {
          const elements = document.getElementsByName(radioName);
          for (const element of elements) {
            if (element.checked) return element.value
          }
        }
        // bind event
        document.getElementById('cancel').onclick = () => this.toggle()
        document.getElementById('submit').onclick = () => {
          const sort = getRadioValue('sortOption')
          const style = getRadioValue('styleOption')
          GM_setValue('sort', sort)
          globalSort = sort
          GM_setValue('style', style)
          globalStyle = style
          this.toggle()
        }
      }

      static isCreated () {
        return document.getElementById('taobao-plus-modal') !== null
      }

      toggle () {
        if (!Setting.isCreated()) {
          GM_addStyle('.taobao-plus-container{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;-webkit-tap-highlight-color:rgba(0,0,0,0);-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;}.taobao-plus-container.modal-open{overflow:hidden;}.taobao-plus-container .modal-open .modal{overflow-x:hidden;overflow-y:auto;}.taobao-plus-container .fade.in{opacity:1;}.taobao-plus-container .modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;overflow:hidden;-webkit-overflow-scrolling:touch;outline:0;}.taobao-plus-container .fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear;}.taobao-plus-container .modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0);}.taobao-plus-container .modal.fade .modal-dialog{-webkit-transition:-webkit-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out;}.taobao-plus-container .modal-dialog{position:relative;width:auto;margin:10px;}.taobao-plus-container .modal-content{position:relative;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:6px;outline:0;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5);}.taobao-plus-container .modal-header{padding:15px;border-bottom:1px solid #e5e5e5;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;content:" ";clear:both;}.taobao-plus-container .modal-title{margin:0;line-height:1.42857143;}.taobao-plus-container h4{font-size:18px;font-family:inherit;font-weight:500;color:inherit;}.taobao-plus-container p{margin:0 0 10px;}.taobao-plus-container .modal-body{position:relative;padding:15px;}.taobao-plus-container .radio{position:relative;display:block;margin-top:10px;margin-bottom:10px;}.taobao-plus-container .radio+.radio{margin-top:-5px;}.taobao-plus-container .radio label{min-height:20px;padding-left:20px;margin-bottom:0;font-weight:400;cursor:pointer;}.taobao-plus-container label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:700;}.taobao-plus-container .radio input[type=radio]{position:absolute;margin-top:4px\\9;margin-left:-20px;}.taobao-plus-container input[type=radio]{margin:4px 0 0;margin-top:1px\\9;line-height:normal;}.taobao-plus-container input[type=radio]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0;}.taobao-plus-container .btn{display:inline-block;margin-right: 10px;padding:6px 12px;margin-bottom:0;font-size:14px;font-weight:400;line-height:1.42857143;text-align:center;white-space:nowrap;vertical-align:middle;-ms-touch-action:manipulation;touch-action:manipulation;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-image:none;border:1px solid transparent;border-radius:4px;}.taobao-plus-container .btn-default{color:#333;background-color:#fff;border-color:#ccc;}.taobao-plus-container .btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c;}.taobao-plus-container hr{height:0;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee;}@media (min-width:768px){.taobao-plus-container .modal-dialog{width:600px;margin:30px auto;}.taobao-plus-container .modal-sm{width:300px;}.taobao-plus-container .modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,.5);box-shadow:0 5px 15px rgba(0,0,0,.5);}}')
          this.createUI(this.sort, this.style)
        }
        this.modal.style.display === 'none'
          ? this.modal.style.display = 'block'
          : this.modal.style.display = 'none'
      }

    }

    let settingEntry = null
    GM_registerMenuCommand('淘宝搜索PLUS设置', () => {
      if (settingEntry === null) settingEntry = new Setting({sort: globalSort, style: globalStyle})
      settingEntry.toggle()
    })
  })()

  document.addEventListener("DOMContentLoaded", () => {
    myLog('The taobao-search-load-shop-info scrip start.')
    setTimeout(tryLoad, 500)

    GM_addStyle('.my-shopinfo-grid{min-width:auto!important;padding:0 5px 5px!important;border:unset!important;-webkit-box-shadow:unset!important;box-shadow:unset!important}.my-shopinfo-grid .score-box{margin-left:5px;margin-top:auto!important;border-top:unset!important;padding:unset!important}.my-shopinfo-list{float:left;min-width:auto!important;padding:5px!important;border:unset!important;-webkit-box-shadow:unset!important;box-shadow:unset!important}.my-shopinfo-list .score-box{margin-top:auto!important;border-top:unset!important;padding:unset!important}.m-itemlist .items .item.J_MouserOnverReq{height:425px!important}@media(max-width:1250px){.my-shopinfo-grid .score{text-align:center}.my-shopinfo-grid .score .percent{float:none!important}.m-itemlist .items .item.J_MouserOnverReq{height:390px!important}}')
    GM_addStyle('.m-widget-shopinfo .score{margin:3px 0;height:18px;white-space:nowrap}  .m-widget-shopinfo .score .text{float:left;width:96px;color:#3c3c3c}.m-widget-shopinfo .score .highlight{float:left;padding:0 3px;color:#fff}  .m-widget-shopinfo .score .percent{float:left;width:52px;text-align:right}.m-widget-shopinfo .morethan .highlight{background:#f40}.m-widget-shopinfo .morethan .percent{color:#f40}.m-widget-shopinfo .equalthan .highlight{background:#f40}  .m-widget-shopinfo .equalthan .percent{color:#f40}.m-widget-shopinfo .lessthan .highlight{background:#00ba97}.m-widget-shopinfo .lessthan .percent{color:#00ba97}')

    let handleTaskRuning = false
    let taskQueue = []
    let fakeElementQueue = []
    let fakeMirror = []

    /* 捕获 排序|筛选|分页 请求 */
    {
      const MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver
      const target = document.querySelector('head')
      const observer = new MutationObserver((mutations) =>
        mutations.forEach((mutation) => {
          if (mutation.addedNodes) {
            mutation.addedNodes.forEach((node) => {
              const src = node.getAttribute('src')
              if (src !== null && src.includes('https://s.taobao.com/search?')) {
                taskQueue = []
                fakeElementQueue = []
                fakeMirror = []
                setTimeout(() => tryLoad(), 1000)
              }
            })
          }
        })
      )
      const config = { childList: true }
      observer.observe(target, config)
    }

    /**
     * 加载店铺评分
     * @param rest 剩余尝试次数
     * @returns {Promise.<void>}
     */
    async function tryLoad (rest = 10) {
      let {items, state} = getItemsAndPageState()
      if (checkIfSafe() && typeof items !== 'undefined'){
        const length = items.length
        myLog(`Find ${length} Items`)

        const date = new Date()
        for (const item of items) {
          const shopId = getShopId(item)
          if (shopId !== null) taskQueue.push({shopId: shopId, item: item, state: state})
          item.classList.add('flag-last')
        }
        await handleTask(state)

        const realLength = items.length
        myLog(`The Real Length is ${realLength}.`)
        for (let index = length; index < realLength; index++) {
          const shopId = getShopId(items[index])
          if (shopId !== null) taskQueue.push({shopId: shopId, item: items[index], state: state})
          items[index].classList.add('flag-last')
        }
        if (!handleTaskRuning) await handleTask(state)

        myLog(`Task [${date}] finished.`)
      } else {
        myLog(`Items Not Found or unsafe. Rest attempt time is ${rest}.`)
        if (rest !== 0) setTimeout(() => tryLoad(rest - 1), 500)
      }
    }

    async function handleTask (state) {
      handleTaskRuning = true
      while (taskQueue.length !== 0 || fakeElementQueue.length !== 0) {
        myLog(`handleTask start. taskQueue.length = ${taskQueue.length}, fakeElementQueue.length = ${fakeElementQueue.length}`)
        while (taskQueue.length !== 0) {
          myLog('handle a taskQueue.')
          const {shopId, item, state} = taskQueue.shift()
          requestAndCreateShopInfo(shopId, item, state)
          await sleep(250)
        }

        await sleep(500)
        fakeMirror = fakeElementQueue
        fakeElementQueue = []
        while (fakeMirror.length !== 0) {
          myLog('handle a fakeElementQueue.')
          const item = fakeMirror.shift()
          const shopId = getShopId(item)
          if (shopId !== null) taskQueue.push({shopId: shopId, item: item, state: state})
        }
      }
      handleTaskRuning = false
    }

    /**
     * 检查 是否载入了新商品
     * @returns {boolean}
     */
    function checkIfSafe () {
      return  document.getElementsByClassName('flag-last').length === 0
    }

    /**
     * 获取商品信息、列表形式
     * @returns {*}
     */
    function getItemsAndPageState () {
      let items = document.getElementsByClassName('item J_MouserOnverReq')
      if (items.length !== 0) {
        return {items: items, state: 'grid'}
      } else if ((items = document.getElementsByClassName('item g-clearfix')).length !== 0) {
        return {items: items, state: 'list'}
      } else {
        return {}
      }
    }

    function getShopId (item) {
      let shopInfo
      if ((shopInfo = item.getElementsByClassName('J_ShopInfo')).length !== 0 ||
        (shopInfo = item.getElementsByClassName('dsr-old J_Dsr')).length !== 0) {
        return shopInfo[0].getAttribute('data-userid')
      } else if (item.getElementsByClassName('fake-line').length !== 0) {
        fakeElementQueue.push(item)
        return null
      } else {
        window.console.log('无法从页面中获取店铺id...')
        return null
      }
    }


    /**
     * 请求并创建店铺数据
     * @param sid shop id
     * @param item
     * @param state
     */
    function requestAndCreateShopInfo (sid, item, state) {
      //https://s.taobao.com 2454747176
      fetch(`/api?sid=${sid}&callback=shopcard&app=api&m=get_shop_card`, {
        method: "GET", mode: 'same-origin', credentials: 'same-origin'
      }).then((response) => {
        if (response.status >= 200 && response.status < 300) return response
        const error = new Error(response.statusText)
        error.response = response
        throw error
      }).then((response) => {
        return response.text()
      }).then((text) => {
        text = text.trim()
        createShopInfoElement(item, JSON.parse(text.slice(9, text.length - 2)), state)
      }).catch(err => window.console.log(err))
    }

    /**
     * 创建店铺信息元素
     * @param itemElement
     * @param shopInfo
     * @param state grid or list
     */
    function createShopInfoElement (itemElement, shopInfo, state) {
      const SMALL_LAPTOP = document.body.scrollWidth <= 1250
      const highlightFormat = (state === 'grid' && !SMALL_LAPTOP) ? '均值' : '比同行均值'

      const shopInfoElement = document.createElement('div')
      shopInfoElement.setAttribute('class', `m-widget-shopinfo my-shopinfo-${state}`)
      shopInfoElement.innerHTML = `<div class="score-box"><ul class="scores">
       <li class="score ${shopInfo.descriptionCompared.class}">
         <span class="text">如实描述:${shopInfo.matchDescription}</span>
         <span class="highlight">${shopInfo.descriptionCompared.text.replace(highlightFormat, '')}</span>
         <span class="percent">${shopInfo.descriptionCompared.rate}</span>
       </li>
       <li class="score ${shopInfo.attitudeCompared.class}">
         <span class="text">服务态度:${shopInfo.serviceAttitude}</span>
         <span class="highlight">${shopInfo.attitudeCompared.text.replace(highlightFormat, '')}</span>
         <span class="percent">${shopInfo.attitudeCompared.rate}</span>
       </li>
       <li class="score ${shopInfo.deliveryCompared.class}">
         <span class="text">物流服务:${shopInfo.deliverySpeed}</span>
         <span class="highlight">${shopInfo.deliveryCompared.text.replace(highlightFormat, '')}</span>
         <span class="percent">${shopInfo.deliveryCompared.rate}</span>
       </li></ul></div>`

      state === 'grid'
        ? itemElement.appendChild(shopInfoElement)
        : itemElement.getElementsByClassName('col col-5')[0].appendChild(shopInfoElement)
    }

    function sleep(ms) {
      return new Promise(resolve => setTimeout(resolve, ms))
    }

  })

})()