// ==UserScript==
// @name online-selection
// @namespace com.hho.middle.fe.online.selection
// @version 0.7
// @description 在线选品
// @author bosiwan
// @match *://*/*
// @icon https://www.google.com/s2/favicons?domain=camp-fire.jp
// @require https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js
// @grant GM.setValue
// @grant GM.getValue
// @grant GM.deleteValue
// @grant GM.listValues
// @grant GM.setClipboard
// @grant unsafeWindow
// ==/UserScript==
class Dep{ //订阅池
constructor(name){
this.id = new Date() // 使用时间戳做订阅池的ID
this.subs = [] // 该事件下对象的集合
}
defined(){ // 添加订阅者
Dep.watch.add(this);
}
notify(){ // 通知订阅者有变化
this.subs.forEach((e,i)=>{
if(typeof e.update === 'function'){
try{
e.update.apply(e); // 触发订阅者更新函数
}catch(err){
console.warr(err);
}
}
})
}
}
Dep.watch = null;
class Watch{
constructor(name,fn){
this.name = name; // 订阅消息的名称
this.id = new Date(); // 使用时间戳做订阅者的ID
this.callBack = fn; // 订阅消息发送改变时 -> 订阅者执行的回调函数
}
add(dep){ // 将订阅者放入dep订阅池
dep.subs.push(this);
}
update(){ // 将订阅者更新方法
var cb = this.callBack; // 赋值为了不改变函数内调用的this
cb(this.name);
}
}
(function() {
'use strict';
const enableSites = ['camp-fire.jp', 'greenfunding.jp', 'www.makuake.com']
const SourcingSites = ['item.taobao.com', 'detail.tmall.com']
const LoggingURL = 'containertest.hhodata.com'
const enableListPathname = ['/projects/category', '/portals/search', '/discover/categories']
const enableDetailPathname = ['/projects/view', '/project']
const DomainMap = {
'camp-fire.jp': 'camp-fire',
'greenfunding.jp': 'GreenFunding',
'www.makuake.com': 'makuake'
}
let preX = 0, preY = 0, startX = 0, startY = 0, canMoving = false
const CSSStyle = styles => {
return styles.join(';')
}
const PrimaryButtonStyle = {
'background-color': 'rgb(3, 193, 253)',
'color': 'white',
'font-weight': 'bold',
}
const DeleteButtonStyle = {
'background-color': '#f52743',
'color': 'white',
'font-weight': 'bold',
}
const SecondaryButtonStyle = {
'background-color': 'white',
'color': '#03c1fd',
'font-weight': 'bold',
'border-style': 'solid',
'border-width': '1px',
'border-color': '#03c1fd',
}
const ButtonStyle = (style, other) => {
const styles = {...{
'padding-left': '16px',
'padding-right': '16px',
'padding-top': '6px',
'padding-bottom': '6px',
'min-width': '100px',
'border-radius': '4px',
display: 'flex',
'align-items': 'center',
'justify-content': 'center',
'-moz-user-select': '-moz-none',
'-khtml-user-select': 'none',
'-webkit-user-select': 'none',
'-o-user-select': 'none',
'user-select': 'none',
'cursor': 'pointer',
}, ...style, ...other}
return Object.keys(styles).map(key => `${key}: ${styles[key]}`).join(';')
}
const ProjectListItemStyle = CSSStyle([
'padding: 4px',
'border-width: 1px',
'border-bottom-color: gray',
'border-bottom-style: solid'
])
const ProjectListItemTitleStyle = CSSStyle([
'white-space: nowrap',
'text-overflow: ellipsis',
'word-break: break-word',
'overflow: hidden'
])
const ProjectListItemStatusStyle = CSSStyle([
'border-width: 1px',
'border-color: red',
'border-style: solid',
'padding: 2px',
'width: fit-content',
'font-size: 12px',
'border-radius: 4px',
'color: red'
])
function init() {
$(document).ready(function() {
if (window.location.host === LoggingURL) {
console.log(localStorage.getItem('__hho_token__'))
GM.setValue('hho_token', localStorage.getItem('__hho_token__'))
window.close()
return
}
if (SourcingSites.indexOf(window.location.host) !== -1) {
}
// 站点白名单
if (enableSites.indexOf(window.location.host) === -1) return
// 添加按钮
$('body').append(`<div id="hho-online-selection-container" style="position:fixed; top: 50px; right: 40px; z-index: 9007199254740991; background: white; width: 300px;
border-radius: 6px;box-shadow: 0px 0px 6px 3px rgba(0,0,0,0.5); padding: 8px;">
<div onmousedown="handleMouseDown(event)" onmousemove="handleMouse(event)" onmouseup="handleMouseUp(event)" onmouseout="handleMouseUp(event)" style="cursor: move;">
<div style="font-weight: bold;"><a href="https://cdn.hhodata.com/chrome-extension/online-selection/script.user.js">【更新插件】</a>7sou - 7秒寻源 <span id="loading-text"></span></div>
<div class="button-list" style="display: flex; margin-top: 6px"></div>
</div>
<label>关键词:</label><input name="search" style="margin: 6px" />
<div class="result-container" style="max-height: 520px; overflow-y: scroll; cursor: default;"></div>
</div>`)
if (window.location.pathname.startsWith('/projects/category') ||
window.location.pathname.startsWith('/portals/search') ||
window.location.pathname.startsWith('/discover/categories')
) {
$('#hho-online-selection-container').append('<button style="z-index: 9007199254740991;background: yellow;color: #333;cursor: pointer;" id="hhoBtn">开始选品</button>');
} else {
const yesButton = `<div style="${ButtonStyle(PrimaryButtonStyle)}" id="hhoYesBtn">寻源</div>`
$('#hho-online-selection-container .button-list').append(yesButton);
const noButton = `<div style="${ButtonStyle(DeleteButtonStyle, {'margin-left': '6px'})}" id="hhoNoBtn">下一个</div>`
$('#hho-online-selection-container .button-list').append(noButton);
}
$(document).on('click',"#hhoBtn", function(){ startSelection() });
$(document).on('click',"#hhoYesBtn", function(){ save() });
$(document).on('click', "#hhoNoBtn", function () { closeTab() });
$(document).on('paste', handlePaste)
$('input[name="search"]').on("keyup", function (event) {
if (event.key === "Enter") {
if (!unsafeWindow.hhoSourcingResults) return
const searchKey = event.currentTarget.value
if (searchKey) {
unsafeWindow.hhoSourcingResultsFilter = unsafeWindow.hhoSourcingResults.filter(d => d.itemName.indexOf(searchKey) !== -1)
} else {
unsafeWindow.hhoSourcingResultsFilter = unsafeWindow.hhoSourcingResults
}
unsafeWindow.renderResultContainer(unsafeWindow.hhoSourcingResultsFilter)
}
});
})
}
var $ = window.$
var addHistoryMethod = (function(){
var historyDep = new Dep();
return function(name){
if(name === 'historychange'){
return function(name,fn){
var event = new Watch(name,fn)
Dep.watch = event;
historyDep.defined();
Dep.watch = null; // 置空供下一个订阅者使用
}
}else if(name === 'pushState' || name === 'replaceState'){
var method = history[name];
return function(){
init()
method.apply(history,arguments);
historyDep.notify();
}
}
}
}());
window.addHistoryListener = addHistoryMethod('historychange');
history.pushState = addHistoryMethod('pushState');
history.replaceState = addHistoryMethod('replaceState');
init()
function startSelection() {
if (!window.location.pathname.startsWith('/projects/category') &&
!window.location.pathname.startsWith('/portals/search') &&
!window.location.pathname.startsWith('/discover/categories')
) {
alert('不是列表页')
return;
}
const domain = DomainMap[window.location.host]
switch (domain) {
case 'camp-fire':
ListSelection.campfile()
break;
case 'GreenFunding':
ListSelection.greenfunding()
break;
case 'makuake':
ListSelection.makuake()
break;
default:
break;
}
}
function scrollToBottom(top = 1000, overHandler) {
window.scroll({
top,
left: 0,
behavior: 'smooth'
})
setTimeout(() => {
var clientHeight = document.documentElement.scrollTop === 0 ? document.body.clientHeight : document.documentElement.clientHeight;
var scrollTop = document.documentElement.scrollTop === 0 ? document.body.scrollTop : document.documentElement.scrollTop;
var scrollHeight = document.documentElement.scrollTop === 0 ? document.body.scrollHeight : document.documentElement.scrollHeight;
if (scrollTop != 0 && clientHeight + scrollTop == scrollHeight) {
overHandler && overHandler()
} else {
scrollToBottom(top * 2, overHandler)
}
}, 500);
}
function _ajax (options, headers = { 'Content-Type': 'application/json' }) {
console.log('123123', GM.getValue('hho_token'))
$("#loading-text").text('加载中...')
return new Promise((resolve, reject) => {
GM.getValue('hho_token').then(res => {
if (!res) {
window.open('http://containertest.hhodata.com/', '_blank')
return
}
$.ajax({
...options,
headers: {...headers, 'x-token': res},
complete: function (data) {
$("#loading-text").text('')
if (data.status === 200) {
if (data.responseJSON.code === 7) {
// 登录
window.open('http://containertest.hhodata.com/', '_blank')
reject()
} else {
resolve(data.responseJSON.data)
}
} else {
reject()
}
},
fail: function(err) {
reject(err)
}
})
}).catch(err => window.open('http://containertest.hhodata.com/', '_blank'))
})
}
function getSourcingResult(project) {
return _ajax({
url: `http://containertest.hhodata.com/api/sevenSmall/sevenSmallResult?projectCode=${project.code}&domain=${project.domain}`,
method: 'GET',
dataType: 'json',
})
}
function startSourcing(project) {
return _ajax({
url: `http://containertest.hhodata.com/api/sevenSmall/sevenSmallParseSouring`,
method: 'POST',
dataType: 'json',
data: JSON.stringify({
html: document.documentElement.innerHTML,
url: window.location.href,
projectCode: project.code,
domain: project.domain
}),
})
}
function resourcing (file, project) {
var formData = new FormData()
formData.append('projectCode', project.code)
formData.append('domain', project.domain)
formData.append('imgfile', file)
return _ajax({
url: `http://containertest.hhodata.com/api/sevenSmall/sevenSmallSourcingAgain`,
enctype: 'multipart/form-data',
type: 'POST',
dataType: 'json',
processData: false,
contentType: false,
data: formData,
}, {})
}
async function save() {
if (!window.location.pathname.startsWith('/projects/view') &&
!(/\/.*\/projects\/[0-9]+/.exec(window.location.pathname)) &&
!window.location.pathname.startsWith('/project')
) {
alert('不是详情页页')
return;
}
let project = {}
const domain = DomainMap[window.location.host]
switch (domain) {
case 'camp-fire':
project = DetailSelection.campfile()
break;
case 'GreenFunding':
project = DetailSelection.greenfunding()
break;
case 'makuake':
project = DetailSelection.makuake()
break;
default:
break;
}
let results = await getSourcingResult(project)
getSourcingResult(project).then(results => {
if (!results || results.length === 0) {
startSourcing(project).then(() => {
getSourcingResult(project).then(showSourcingResult).catch(err => alert('寻源失败,再点一次试试'))
}).catch(error => alert('寻源失败,再点一次试试'))
} else {
showSourcingResult(results)
}
}).catch(error => alert('寻源失败,再点一次试试'))
}
function closeTab() {
if (!window.location.pathname.startsWith('/projects/view') &&
!(/\/.*\/projects\/[0-9]+/.exec(window.location.pathname)) &&
!window.location.pathname.startsWith('/project')
) {
alert('不是详情页页')
return;
}
const urls = JSON.parse(localStorage.getItem('hho-list-selection-urls'))
const index = parseInt(localStorage.getItem('hho-list-selection-index')) + 1
const maxIndex = parseInt(localStorage.getItem('hho-list-selection-max-index'))
if (maxIndex >= index) {
window.close()
localStorage.setItem('hho-list-selection-index', index)
window.open(urls[index])
} else {
alert('当前页已选完,请开始下一页')
localStorage.setItem('hho-list-selection-urls', JSON.stringify([]))
localStorage.setItem('hho-list-selection-index', 0)
localStorage.setItem('hho-list-selection-max-index', 0)
}
}
function handlePaste(event) {
const items = event.originalEvent.clipboardData && event.originalEvent.clipboardData.items
if (!items || items.length <= 0) { return }
let file = null
// 检索剪切板items
file = Array.from(items).find(d => d.type.indexOf('image') !== -1)?.getAsFile()
if (!file) { return }
const confirm = window.confirm('确定重新寻源吗?')
if(!confirm) return
let project = {}
const domain = DomainMap[window.location.host]
switch (domain) {
case 'camp-fire':
project = DetailSelection.campfile()
break;
case 'GreenFunding':
project = DetailSelection.greenfunding()
break;
case 'makuake':
project = DetailSelection.makuake()
break;
default:
break;
}
resourcing(file, project).then(res => {
getSourcingResult(project).then(showSourcingResult).catch(err => alert('寻源失败,再点一次试试'))
})
}
// 移动container
unsafeWindow.handleMouseDown = function handleMouseDown (event) {
event.preventDefault()
event.stopPropagation()
preX = event.clientX
preY = event.clientY
startX = $('#hho-online-selection-container')[0].offsetLeft
startY = $('#hho-online-selection-container')[0].offsetTop
if (!canMoving) {
canMoving = true
}
}
unsafeWindow.handleMouseUp = function handleMouseUp (event) {
event.preventDefault()
event.stopPropagation()
if (canMoving) {
canMoving = false
}
}
unsafeWindow.handleMouse = function handleMouse (event) {
event.preventDefault()
event.stopPropagation()
if (!canMoving) return
$("#hho-online-selection-container").css("top", event.clientY - preY + startY + 'px')
$("#hho-online-selection-container").css("left", startX + event.clientX - preX + 'px')
}
window.showSourcingResult = function showSourcingResult (results) {
unsafeWindow.hhoSourcingResults = results
unsafeWindow.renderResultContainer(results)
}
unsafeWindow.renderResultContainer = function renderResultContainer (results) {
$('#hho-online-selection-container .result-container').empty()
{$('#hho-online-selection-container .result-container').append(results.map(r => `
<div class='result-item' onclick="window.open('${r.itemUrl}', '_blank')">
<div>
<img src="${r.itemPrcUrl}" style="width: 180px">
</div
<div>
<div>${r.itemName}</div>
</div>
</div>
`).join(''))}
}
const ListSelection = {
campfile() {
const urls = Array.from(document.querySelectorAll('.boxes4 .box .box-title a')).map(d => d.href)
localStorage.setItem('hho-list-selection-urls', JSON.stringify(urls))
localStorage.setItem('hho-list-selection-index', 0)
localStorage.setItem('hho-list-selection-max-index', urls.length - 1)
window.open(urls[0])
},
greenfunding() {
const urls = Array.from(document.querySelectorAll('.m-projects__card .js-methods__hover')).map(d => d.href)
localStorage.setItem('hho-list-selection-urls', JSON.stringify(urls))
localStorage.setItem('hho-list-selection-index', 0)
localStorage.setItem('hho-list-selection-max-index', urls.length - 1)
window.open(urls[0])
},
makuake() {
const urls = Array.from(document.querySelectorAll('.projects .ProjectItem a')).map(d => d.href)
localStorage.setItem('hho-list-selection-urls', JSON.stringify(urls))
localStorage.setItem('hho-list-selection-index', 0)
localStorage.setItem('hho-list-selection-max-index', urls.length - 1)
window.open(urls[0])
}
}
const DetailSelection = {
campfile() {
const code = window.dataLayer[1].dynx_itemid;
const name = document.title
const domain = 'camp-file'
return {code, name, domain}
},
greenfunding() {
const code = window.location.pathname.split('/').reverse()[0]
const name = document.title
const domain = 'GreenFunding'
return {code, name, domain}
},
makuake() {
const code = window.location.pathname.split('/').reverse()[1]
const name = document.title
const domain = 'makuake'
return {code, name, domain}
}
}
})();