BL Data Query Helper

try to take over the world!

  1. // ==UserScript==
  2. // @name BL Data Query Helper
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.1
  5. // @include http*://www.blushmark.com/*
  6. // @description try to take over the world!
  7. // @author You
  8. // @match http://*/*
  9. // @grant GM_addStyle
  10. // ==/UserScript==
  11.  
  12. const willRequests = []
  13. const onGoingRequests = []
  14. const maxConcurrency = 5
  15.  
  16. const beginRequest = (func) => {
  17. if (func) {
  18. if (onGoingRequests.length < maxConcurrency) {
  19. onGoingRequests.push(func)
  20. func()
  21. } else {
  22. willRequests.push(func)
  23. }
  24. }
  25. }
  26. const stopRequest = () => {
  27. onGoingRequests.pop()
  28. if (willRequests.length > 0) {
  29. const func = willRequests.shift()
  30. onGoingRequests.push(func)
  31. func()
  32. }
  33. }
  34.  
  35. // 强依赖网站的参数
  36. // 1.cookie中的 hasLogin, login_token
  37. // 2.商品div中的class goods-item
  38. // 3.商品div中的class goods-item 的上级div 要有 data-goods-id data-style-id参数
  39.  
  40. const getQueryStringValue = (key) => {
  41. return decodeURIComponent(window.location.search.replace(new RegExp('^(?:.*[&\\?]' + encodeURIComponent(key).replace(/[\.\+\*]/g, '\\$&') + '(?:\\=([^&]*))?)?.*$', 'i'), '$1'))
  42. }
  43.  
  44. const getHost = () => {
  45. const hostName = window.location.hostname
  46. let host = 'https://www.blushmark.com/prod'
  47. if (hostName.startsWith('ft') || hostName.startsWith('ft-x')) {
  48. host = `https://${hostName}/test`
  49. } else if (hostName.startsWith('p')) {
  50. host = `https://${hostName}/pre`
  51. }
  52. return host
  53. }
  54.  
  55. const getUrl = (url, params = {}) => {
  56. let host = getHost()
  57. console.log('url====>', url)
  58. console.log('host====>', host)
  59. console.log('params====>', params)
  60. const keys = Object.keys(params)
  61. keys && keys.map((key) => {
  62. url = url + (url.includes('?') ? '&' : '?') + `${key}=${params[key]}`
  63. })
  64. return host + url
  65. }
  66.  
  67. const GET = (path, params, callback) => {
  68. beginRequest(() => {
  69. const xhr = new XMLHttpRequest()
  70. const url = getUrl(path, params)
  71. console.log('url====>', url)
  72. xhr.open('GET', url, true)
  73. xhr.onreadystatechange = function() {
  74. if (xhr.readyState == 4) {
  75. const info = JSON.parse(xhr.responseText)
  76. if (info && info.code == 0) {
  77. callback(info.data)
  78. } else {
  79. callback(undefined)
  80. }
  81. stopRequest()
  82. }
  83. }
  84. xhr.send()
  85. })
  86. }
  87.  
  88. const fetchData = (goodsId, styleId, callback) => {
  89. const isAllListPage = window.location.pathname.startsWith('/categories/0_0/')
  90. const isClearance = window.location.pathname.startsWith('/clearance')
  91. const isSortByNew = window.location.search.includes('sort_by=new')
  92. const params = {
  93. styleId: styleId,
  94. callback: goodsId,
  95. type: (isAllListPage ? getQueryStringValue('type') : ''),
  96. pageType: (isClearance ? 'clearance' : isSortByNew ? 'just_in' : 'normal')
  97. }
  98. GET('/1.0/plugin/ctr-detail', params, callback)
  99. }
  100.  
  101. const indexGetData = (ec, el, list_page_path, callback) => {
  102. const params = {
  103. path: list_page_path,
  104. ec: ec,
  105. el: el
  106. }
  107. GET('/1.0/home/plugin-ctr', params, callback)
  108. }
  109.  
  110. const productGetData = (goods_id, style_id, size_id, callback) => {
  111. const params = {
  112. goods_id: goods_id,
  113. style_id: style_id,
  114. size_id: size_id
  115. }
  116. GET('/1.0/home/getStockTransit', params, callback)
  117. }
  118.  
  119. const HTTP = {
  120. fetchData,
  121. indexGetData,
  122. productGetData
  123. }
  124.  
  125. const elementIdentifier = 'chrome-extension-info'
  126. const elementProductIdentifer = 'chrome-extension-info-product'
  127. const MOUSE_VISITED_CLASS_NAME = 'goods-item'
  128. const MOUSE_VISITED_CLASS_NAME_INDEX = 'ctr-index'
  129. const MOUSE_VISITED_CLASS_NAME_PRODUCT = 'ctr-product'
  130.  
  131. // 强依赖网站的参数
  132. // 1.cookie中的 hasLogin, login_token
  133. // 2.商品div中的class goods-item
  134. // 3.商品div中的class goods-item 的上级div 要有 data-goods-id data-style-id参数
  135. const createRowElement = (dataElement, text) => {
  136. const container = document.createElement('span')
  137. container.className = 'extension-container'
  138. const textElement = document.createTextNode(text)
  139. container.appendChild(textElement)
  140. dataElement.appendChild(container)
  141. return container
  142. }
  143.  
  144. /**
  145. * 获取containerElement
  146. * @param element
  147. */
  148. const getContainerElement = (element) => {
  149. let containerElement = null
  150. if (element && element.className.indexOf(MOUSE_VISITED_CLASS_NAME) >= 0) {
  151. // 商品在此获取数据
  152. const findGoodsImageElementFunc = (elements) => {
  153. elements && Array.from(elements).some(e => {
  154. if (e && e.classList && e.classList.value.indexOf('goods-item-pic') >= 0) {
  155. containerElement = e
  156. return true
  157. }
  158. findGoodsImageElementFunc(e.children)
  159. })
  160. }
  161. findGoodsImageElementFunc(element.children)
  162. } else {
  163. // 首页和详情页在此获取数据
  164. containerElement = element
  165. }
  166. return containerElement
  167. }
  168.  
  169. const showListCover = (containerElement) => {
  170. if (!containerElement || !containerElement.parentElement) {
  171. return
  172. }
  173. // 商品获取方式
  174. const goodsId = containerElement.parentElement.getAttribute('data-goods-id')
  175. const styleId = containerElement.parentElement.getAttribute('data-style-id')
  176. const pageType = containerElement.parentElement.getAttribute('page-type')
  177. // 参数不合法 退出
  178. if (!goodsId || !styleId || !pageType) return
  179.  
  180. const children = Array.from(containerElement.children)
  181. let isExist = false
  182. children && children.map(e => {
  183. if (e.id === elementIdentifier) {
  184. if (e.dataId === goodsId + '' + styleId) {
  185. isExist = true
  186. } else {
  187. // 存在一个其他商品的数据 移除
  188. e.parentNode.removeChild(e)
  189. }
  190. }
  191. })
  192. // 已经存在了 不再次展示
  193. if (isExist) return
  194. const dataElement = document.createElement('div')
  195. dataElement.id = elementIdentifier
  196. dataElement.dataId = goodsId + '' + styleId
  197. dataElement.style.zIndex = 100
  198.  
  199. const ctrElement = createRowElement(dataElement, 'CTR: -%')
  200. let justInElement
  201. if (pageType == 'just_in') {
  202. justInElement = createRowElement(dataElement, 'BST_CTR: -')
  203. }
  204. const crElement = createRowElement(dataElement, 'CR: -%')
  205. const ctrCrElement = createRowElement(dataElement, pageType == 'just_in' ? 'BST_CTR*CR: -' : 'CTR*CR: -')
  206. const saleElement = createRowElement(dataElement, 'SAL: -')
  207. const averageElement = createRowElement(dataElement, 'AVE: -')
  208. const clickElement = createRowElement(dataElement, 'CLI: -')
  209. const impressionElement = createRowElement(dataElement, 'IMP: -')
  210. const userElement = createRowElement(dataElement, 'DET: -')
  211. const ostElement = createRowElement(dataElement, 'OST: -')
  212. const abElement = createRowElement(dataElement, '')
  213. abElement.style.display = 'none'
  214. containerElement && containerElement.appendChild(dataElement)
  215. HTTP.fetchData(goodsId, styleId, (data) => {
  216. if (data) {
  217. const { cr, ctr, sales, viewCount, clickCount, showNumber, onSaleTime, ab_test, ctrCr, goodsSales, bestSellerCtr, averageDailySales } = data
  218. ctrElement.textContent = 'CTR: ' + ctr
  219. crElement.textContent = 'CR: ' + cr
  220. saleElement.textContent = 'SAL: ' + sales + '(' + goodsSales + ')'
  221. clickElement.textContent = 'CLI: ' + clickCount
  222. impressionElement.textContent = 'IMP: ' + viewCount
  223. userElement.textContent = 'DET: ' + showNumber
  224. ostElement.textContent = 'OST: ' + onSaleTime
  225. ctrCrElement.textContent = (pageType == 'just_in' ? 'BST_CTR*CR:' : 'CTR*CR: ') + ctrCr
  226. averageElement.textContent = 'AVE: ' + averageDailySales
  227. if (ab_test) {
  228. abElement.textContent = 'A/B Test'
  229. abElement.style.display = 'block'
  230. }
  231.  
  232. if (pageType == 'just_in') {
  233. if (bestSellerCtr == undefined) {
  234. justInElement.parentElement.removeChild(justInElement)
  235. } else { justInElement.textContent = 'BST_CTR:' + bestSellerCtr }
  236. }
  237. }
  238. })
  239. }
  240.  
  241. const showHomeCover = (element, containerElement) => {
  242. if (!element || !containerElement) {
  243. return
  244. }
  245. const children = Array.from(containerElement.children)
  246. let isExist = false
  247. children && children.map(e => {
  248. if (e.id === elementIdentifier) {
  249. isExist = true
  250. }
  251. })
  252. // 已经存在了 不再次展示
  253. if (isExist) return
  254. const ec = element.getAttribute('ec')
  255. const el = element.getAttribute('el')
  256. const list_page_path = element.getAttribute('list_page_path')
  257. // 参数不合法
  258. if (!ec || !el || !list_page_path) return
  259.  
  260. const dataElement = document.createElement('div')
  261. dataElement.id = elementIdentifier
  262. dataElement.style.zIndex = 100
  263.  
  264. const ctrElement = createRowElement(dataElement, 'CTR: -%')
  265. const clickElement = createRowElement(dataElement, 'Click: -')
  266. const viewElement = createRowElement(dataElement, 'Impression: -')
  267. HTTP.indexGetData(ec, el, list_page_path, (data) => {
  268. if (data) {
  269. const { ctr, click_count, view_count } = data
  270. ctrElement.textContent = 'CTR: ' + (ctr || '0%')
  271. clickElement.textContent = 'Click: ' + (click_count || 0)
  272. viewElement.textContent = 'Impression: ' + (view_count || 0)
  273. }
  274. })
  275. containerElement && containerElement.appendChild(dataElement)
  276. }
  277.  
  278. const showDetailCover = (element, containerElement) => {
  279. if (!element || !containerElement) {
  280. return
  281. }
  282. const children = Array.from(containerElement.children)
  283. let isExist = false
  284. children && children.map(e => {
  285. if (e.id === elementProductIdentifer && e.style_id === element.getAttribute('selectedColorId')) {
  286. isExist = true
  287. }
  288.  
  289. if (e.style_id !== element.getAttribute('selectedColorId')) {
  290. if (e.id === elementProductIdentifer) {
  291. e && e.parentNode.removeChild(e)
  292. }
  293. }
  294. })
  295.  
  296. // 已经存在了 不再次展示
  297. if (isExist) return
  298.  
  299. const goods_id = element.getAttribute('selectedGoodsId')
  300. const style_id = element.getAttribute('selectedColorId')
  301. const size_id = element.getAttribute('selectedSizeId')
  302. // 参数不合法
  303. if (!goods_id || !style_id || !size_id) return
  304.  
  305. const dataElement = document.createElement('div')
  306. dataElement.id = elementProductIdentifer
  307. dataElement.style.zIndex = 1
  308. const showElement = createRowElement(dataElement, '-')
  309. dataElement.style_id = style_id
  310. const showElement2 = createRowElement(dataElement, '')
  311. HTTP.productGetData(goods_id, style_id, size_id, (data) => {
  312. if (data) {
  313. if (data.has_virtual) {
  314. const result = data.stockTransit && data.stockTransit.split('[')
  315. showElement.innerHTML = result[0] + '<br>[' + result[1]
  316. } else {
  317. showElement.innerHTML = data.stockTransit
  318. }
  319. if (data.has_virtual) { showElement2.textContent = 'Virtual' }
  320. }
  321. })
  322. containerElement && containerElement.appendChild(dataElement)
  323. }
  324.  
  325. const isLogin = () => {
  326. return document.cookie && document.cookie.includes('hasLogin=1;')
  327. }
  328.  
  329. const beginTimer = () => {
  330. console.log('开始脚本====>')
  331. // 每秒检查 一次
  332. setInterval(() => {
  333. // 遍历所有节点,取所有的商品模块
  334. // 判断该商品模块是否已经有
  335. // 如果没有,则加入模块,并请求接口
  336. // 如果已经有了,则啥都不做
  337. if (isLogin()) {
  338. // 如果是首页商品不展示内容
  339. // 列表页面
  340. const elements = Array.from(document.getElementsByClassName(MOUSE_VISITED_CLASS_NAME))
  341. if (window.location.pathname != '/' && elements && elements.length > 0) {
  342. elements.map((element) => {
  343. let containerElement = getContainerElement(element)
  344. showListCover(containerElement)
  345. })
  346. }
  347. // 首页
  348. const indexElements = Array.from(document.getElementsByClassName(MOUSE_VISITED_CLASS_NAME_INDEX))
  349. if (indexElements && indexElements.length > 0) {
  350. indexElements.map((element) => {
  351. showHomeCover(element, element)
  352. })
  353. }
  354. // 详情页面
  355. const productElements = Array.from(document.getElementsByClassName(MOUSE_VISITED_CLASS_NAME_PRODUCT))
  356. if (productElements && productElements.length > 0) {
  357. productElements.map((element) => {
  358. showDetailCover(element, element)
  359. })
  360. }
  361. }
  362. }, 1000)
  363. }
  364.  
  365. const main = () => {
  366. beginTimer()
  367. }
  368.  
  369. main()
  370.  
  371.  
  372. GM_addStyle(`
  373. #chrome-extension-info-product{
  374. position: absolute;
  375. top: 0;
  376. bottom:0;
  377. right: 0;
  378. left: 0;
  379. height: 35px;
  380. }
  381.  
  382. #chrome-extension-info {
  383. position: absolute;
  384. bottom: 0;
  385. right: 0;
  386. left: 0;
  387. min-height: 60px;
  388. background: rgba(255, 255, 255, 0.8);
  389. display: flex;
  390. flex-direction: row;
  391. flex-wrap: wrap;
  392. align-items: center;
  393. padding: 10px 0px;
  394. min-width: 150px;
  395. }
  396.  
  397. #chrome-extension-info .extension-container {
  398. min-width: calc((100% - 84px)/ 2);
  399. display: inline-block;
  400. font-size: 12px;
  401. color: #333;
  402. font-family: 'Montserrat SemiBold';
  403. line-height: 13px;
  404. text-align: left;
  405. margin-left: 20px;
  406. padding-left: 8px;
  407. position: relative;
  408. margin-bottom: 5px;
  409. }
  410.  
  411. #chrome-extension-info .extension-container::before {
  412. content: '';
  413. position: absolute;
  414. left: -8px;
  415. top: 0px;
  416. width: 10px;
  417. height: 10px;
  418. border: 1px solid #FFFFFF;
  419. background: #F2CE99;
  420. border-radius: 10px;
  421. line-height: 13px;
  422. }
  423.  
  424. #chrome-extension-info .extension-container:nth-child(2n+1)::before {
  425. background: #F2CE99;
  426. border: 1px solid #FFFFFF;
  427. }
  428.  
  429. #chrome-extension-info .extension-container:nth-child(2n)::before {
  430. background: #93D1F5;
  431. border: 1px solid #FFFFFF;
  432. }
  433.  
  434. #chrome-extension-info-product .extension-container{
  435. position: absolute;
  436. left: 0;
  437. width: 100%;
  438. height: 15px;
  439. text-align: center;
  440. }
  441.  
  442. #chrome-extension-info-product .extension-container:nth-child(1){
  443. bottom:-15px;
  444. color: red;
  445. }
  446.  
  447. #chrome-extension-info-product .extension-container:nth-child(2){
  448. top:-15px;
  449. color: blue;
  450. }
  451. `)