// ==UserScript==
// @name 微信广告后台增强
// @namespace http://tampermonkey.net/
// @version 0.5.3
// @description 添加批量开启/暂停/删除功能
// @author You
// @require https://unpkg.com/[email protected]/dist/jquery.min.js
// @require https://unpkg.com/[email protected]/dist/async.min.js
// @match https://mp.weixin.qq.com/promotion/*
// @grant GM_addStyle
// ==/UserScript==
'use strict';
var initialized = false;
var parseQueryString = function( queryString ) {
var params = {}, queries, temp, i, l;
// Split into key/value pairs
queries = queryString.split("&");
// Convert the array of strings into an object
for ( i = 0, l = queries.length; i < l; i++ ) {
temp = queries[i].split('=');
params[temp[0]] = temp[1];
}
return params;
};
// 微信的 token
const getToken = () => {
const href = document.location.href;
const [ url, token ] = href.match(/token=(\d+)/)
return token;
}
//添加 checkbox
const addCheck = () => {
$('main [role=table] tbody tr').each((i, tr) => {
const firstCell = $(tr).find('td:first');
const txt = firstCell.text();
// 单元格内容举例:ale1017-2-副本计划 ID: 77405915
const [, id] = txt.split('计划 ID: ')
firstCell.append(`<input class="idSelector" name="selectedId" value="${i}" type="checkbox" />`);
});
}
// 添加进度条
const addProgressBar = ({ total = 0 }) => {
const bar = `<div id="progressbar">
<span id="finished">0</span> / <span id="total">${total}</span>
</div>`;
$('body').append(bar);
};
//进度条自增
const incrProgressBar = (count=1) => {
const current = parseInt($('#finished').html());
$('#finished').html(current + count);
};
//移除进度条
const removeProgressBar = () => {
$('#progressbar').remove();
}
// 获取选择的计划 ID 数组。
// 需要注意的是页面是无刷新的,翻页后 checkbox 不会被销毁,所以直接绑 value 的模式不行
// 除非能检测到 DOM 刷新事件,然后重新生成
const getSelectedIdAll = () => {
const results = [];
const name = getCurrentTab();
switch(name) {
case "投放计划名称":
$('.idSelector:checked').each(function(){
const firstCell = $(this).parent('td');
const txt = firstCell.text();
const [, id] = txt.split('计划 ID: ');
results.push(parseInt(id));
});
break;
case "广告名称":
$('.idSelector:checked').each(function(){
const firstCell = $(this).parent('td');
const txt = firstCell.text();
const [, id] = txt.split('广告 ID: ');
results.push(parseInt(id));
});
break;
default:
return alert('无法获知当前 tab 页');
}
return results;
};
// 生成 pos_type 参数,目前不明白为什么朋友圈广告参数是 999
const getCurrentPos = () => {
const qs = parseQueryString(document.location.search);
// 公众号广告是0,pos_type 是 0;朋友圈广告是1,pos_type 是 999
if(parseInt(qs.type)) {
return 999;
} else {
return 0;
}
}
const request = ({ url, args }) => {
const token = getToken();
return $.post( url, {
args: JSON.stringify(args),
token,
appid: '',
spid: '',
_: (new Date).getTime()
});
};
const getCurrentTab = () => {
return $('[role=columnheader]:first-child')[0].innerText.trim();
};
//删除计划接口
const delete_campaign = ({ cid, pos_type }) => {
const args = { cid, pos_type };
const name = getCurrentTab();
let url = '';
switch(name) {
case "投放计划名称":
url = 'https://mp.weixin.qq.com/promotion/v3/delete_campaign';
break;
case "广告名称":
args.oper_aids = [{ aid: cid, pos_type }];
url = 'https://mp.weixin.qq.com/promotion/v3/batch_delete_adgroup';
break;
default:
return alert('无法获知当前 tab 页');
}
return request({ url, args });
};
//继续投放的接口
const resume_campaign = ({ cid, pos_type }) => {
const args = { cid, pos_type };
const name = getCurrentTab();
let url = '';
switch(name) {
case "投放计划名称":
url = 'https://mp.weixin.qq.com/promotion/v3/resume_campaign';
break;
case "广告名称":
args.oper_aids = [{ aid: cid, pos_type }];
url = 'https://mp.weixin.qq.com/promotion/v3/batch_resume_adgroup';
break;
default:
return alert('无法获知当前 tab 页');
}
return request({ url, args });
};
// 暂停计划的接口
const suspend_campaign = ({ cid, pos_type }) => {
const args = { cid, pos_type };
const name = getCurrentTab();
let url = '';
switch(name) {
case "投放计划名称":
url = 'https://mp.weixin.qq.com/promotion/v3/suspend_campaign';
break;
case "广告名称":
args.oper_aids = [{ aid: cid, pos_type }];
url = 'https://mp.weixin.qq.com/promotion/v3/batch_suspend_adgroup';
break;
default:
return alert('无法获知当前 tab 页');
}
return request({ url, args });
};
//批量暂停的按钮
const addPauseBtn = () => {
// conitnue = ''
//
const pauseBtn = $('<button class="pauseBtn Button_new__base-1H7bt Button_new__default-3C02p Button_new__mini-catyW"><svg width="10" height="10" viewBox="0 0 14 14" style="margin-right: 3px;"><circle cx="7" cy="7" r="7"></circle><path d="M4 4h2v6H4V4zm4 0h2v6H8V4z" fill="#fff"></path></svg> 暂停所选</button>');
pauseBtn.click(() => {
const idArr = getSelectedIdAll();
if (!confirm(`是否确认暂停?共 ${idArr.length} 个计划`)) {
return;
}
const pos = getCurrentPos();
const total = idArr.length;
addProgressBar({ total });
// async.mapLimit 文档: http://caolan.github.io/async/docs.html#mapLimit
async.mapLimit(idArr, 1, (id, callback) => {
// 速度较快时会有频率限制,主要是删除接口的频率限制
setTimeout(()=>{
suspend_campaign({
cid: id,
pos_type: pos
}).done(res => {
callback(null, res);
}).fail(() => {
callback(null, { ret: -1 });
}).always(() => {
incrProgressBar();
});
}, 100);
}, function(err, results) {
removeProgressBar();
let success = 0;
let failed = 0;
for(let result of results) {
if(result.ret === 0) {
success++;
} else {
failed++;
console.log(result);
}
}
if(failed > 0) {
alert(`提醒:失败数 ${failed},完成 ${success}`);
}
});
});
// .ui-flex 是翻页的区块
$('.ui-flex').prepend(pauseBtn);
// 继续投放的按钮
const startBtn = $(`<button class="startBtn Button_new__base-1H7bt Button_new__default-3C02p Button_new__mini-catyW">
<svg width="10" height="10" style="margin-right: 3px;"><path d="M5 10A5 5 0 1 1 5 0a5 5 0 0 1 0 10zM3.5 2.5v5l4-2.5-4-2.5z" fill-rule="nonzero"></path></svg> 开启投放</button>`);
startBtn.click(() => {
const idArr = getSelectedIdAll();
if (!confirm(`是否确认投放?共 ${idArr.length} 个计划`)) {
return;
}
const pos = getCurrentPos();
const total = idArr.length;
addProgressBar({ total });
async.mapLimit(idArr, 1, (id, callback) => {
setTimeout(()=>{
resume_campaign({
cid: id,
pos_type: pos
}).done(res => {
incrProgressBar();
callback(null, res);
incrProgressBar();
}).fail(() => {
callback(null, { ret: -1 });
});
}, 100);
}, function(err, results) {
removeProgressBar();
let success = 0;
let failed = 0;
for(let result of results) {
if(result.ret === 0) {
success++;
} else {
failed++;
console.log(result);
}
}
if(failed > 0) {
alert(`提醒:失败数 ${failed},完成 ${success}`);
}
});
});
$('.ui-flex').prepend(startBtn);
// 删除计划的按钮
const delBtn = $('<button class="delBtn Button_new__base-1H7bt Button_new__default-3C02p Button_new__mini-catyW"><svg width="10" height="10" viewBox="0 0 14 14" style="margin-right: 3px;"><circle cx="7" cy="7" r="7"></circle><path d="M4 4h2v6H4V4zm4 0h2v6H8V4z" fill="#fff"></path></svg> 删除所选</button>');
delBtn.click(() => {
const idArr = getSelectedIdAll();
if (!confirm(`是否确认删除?共 ${idArr.length} 个计划`)) {
return;
}
const pos = getCurrentPos();
const total = idArr.length;
addProgressBar({ total });
async.mapLimit(idArr, 1, (id, callback) => {
setTimeout(()=>{
delete_campaign({
cid: id,
pos_type: pos
}).done(res => {
callback(null, res);
}).fail(() => {
callback(null, { ret: -1 });
}).always(() => {
incrProgressBar();
});
}, 1000);
}, function(err, results) {
removeProgressBar();
let success = 0;
let failed = 0;
for(let result of results) {
if(result.ret === 0) {
success++;
} else {
failed++;
console.log(result);
}
}
if(failed > 0) {
alert(`提醒:失败数 ${failed},完成 ${success}`);
}
});
});
$('.ui-flex').prepend(delBtn);
// 全选/取消全选
const selectAll = $('<label class="selAllLabel"><input type="checkbox" class="selAll" /> 全选/取消</label>');
selectAll.change(function(i) {
var status = $(this).find('.selAll')[0].checked;
$('.idSelector').each(function(){
this.checked = status;
});
});
$('.ui-flex').prepend(selectAll);
};
//开始操作的按钮
// 设计这个按钮的原因是: 不知道何时DOM渲染完毕,后台可能使用了 React 或者 Vue 之类的技术
// 如果能检测到渲染完毕,可以自动加载
const addStartBtn = () => {
const startBtn = $('<button style="position: fixed;top:50%;left:0;z-index:2;" class="addBtn Button_new__base-1H7bt Button_new__primary-2diSJ Button_new__mini-catyW Button_new__iconBtn-NYPRT">批量操作</button>');
startBtn.click(() => {
if(initialized) {
return;
}
initialized = true;
addCheck();
addPauseBtn();
startBtn.remove();
});
$('body').append(startBtn);
};
// 添加 css 样式到当前页面 GM_addStyle 的文档 https://tampermonkey.net/documentation.php?ext=dhdg#GM_addStyle
GM_addStyle(`
.pauseBtn {}
.startBtn{
color: #67C23A !important;
}
.delBtn{
color:#F56C6C !important;
}
.pauseBtn,
.startBtn,
.delBtn {
margin-right: 15px;
}
.selAllLabel {
line-height: 1;
padding:6px 12px;
}
.idSelector {
position:absolute;
left:5px;
top:5px;
height: 36px;
font-size: 43px;
}
.addBtn {
position: fixed;
top:50%;
left:0;
z-index:2;
padding: 0 8px;
background-color: #44b549;
box-shadow: inset 0 0 0 1px #39973d;
color: #fff;
border-radius: 3px;
}
#progressbar{
z-index:2;
position: fixed;
top:30%;
left:46%;
padding:0 20px;
border-radius: 12px;
font-size:64px;
background-color: hsla(0,87%,69%,.1);
border-color: hsla(0,87%,69%,.2);
color: #f56c6c;
}
.ui-flex{
display:block !important;
min-height:80px;
}
#test_pagination{
float: right
}
`);
$(() => {
addStartBtn();
});