Repository: sunoj/teaclub
Branch: master
Commit: 52857fa40390
Files: 28
Total size: 140.0 KB
Directory structure:
gitextract_q6d88nwp/
├── .babelrc
├── .vscode/
│ └── settings.json
├── package.json
├── public/
│ ├── background.html
│ ├── manifest.json
│ ├── popup.html
│ └── start.html
├── readme.md
├── src/
│ ├── account.js
│ ├── background.js
│ ├── components/
│ │ ├── app.vue
│ │ ├── discounts.vue
│ │ ├── events.vue
│ │ ├── guide.vue
│ │ ├── links.vue
│ │ ├── loading.vue
│ │ ├── popup.vue
│ │ └── report.vue
│ ├── content_script.js
│ ├── popup.js
│ ├── start.js
│ ├── tasks.js
│ ├── utils.js
│ └── variables.js
├── static/
│ └── style/
│ ├── popup.css
│ ├── start.css
│ └── style.css
└── webpack.config.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .babelrc
================================================
{
"presets": [
["env", {
"targets": {
"browsers": ["last 1 Chrome version"]
}
}]
],
"plugins": [
["transform-runtime",
{
"polyfill": false,
"regenerator": true
}]
]
}
================================================
FILE: .vscode/settings.json
================================================
{
"cSpell.words": [
"Lazyload",
"fliggy",
"metatit",
"tmall"
]
}
================================================
FILE: package.json
================================================
{
"name": "teaclub",
"version": "0.0.1",
"author": "Ming",
"private": true,
"scripts": {
"start": "npx webpack --config webpack.config.js",
"build": "NODE_ENV=production yarn start",
"dev": "webpack --watch"
},
"dependencies": {
"@sunoj/touchemulator": "^0.0.1",
"babel-plugin-transform-runtime": "^6.23.0",
"dexie": "^2.0.4",
"gulp": "^4.0.0",
"gulp-clean-css": "^3.9.0",
"gulp-concat": "^2.6.1",
"gulp-preprocess": "^2.0.0",
"gulp-replace": "^0.6.1",
"gulp-watch": "^4.3.11",
"hooper": "^0.1.5",
"jobs": "^0.0.4",
"jquery": "3.5",
"lodash": "^4.17.19",
"logline": "^1.1.2",
"luxon": "^1.4.3",
"microtip": "^0.2.2",
"parcel-bundler": "^1.12.3",
"qrcode-svg": "^1.1.0",
"vue": "^2.6.11",
"vue-infinite-loading": "^2.4.5",
"vue-lazyload": "^1.3.3",
"weui": "^2.3.0",
"weui.js": "^1.2.1",
"zepto": "^1.2.0"
},
"alias": {
"vue": "./node_modules/vue/dist/vue.common.js"
},
"devDependencies": {
"@vue/component-compiler-utils": "^2.6.0",
"babel-core": "^6.26.3",
"babel-preset-env": "^1.7.0",
"clean-webpack-plugin": "^3.0.0",
"copy-webpack-plugin": "^5.1.1",
"css-loader": "^3.5.3",
"file-loader": "^6.0.0",
"html-webpack-plugin": "^4.3.0",
"less": "^3.11.1",
"less-loader": "^6.0.0",
"parcel-plugin-static-files-copy": "^2.3.1",
"style-loader": "^1.2.1",
"svg-inline-loader": "^0.8.2",
"svg-url-loader": "^5.0.0",
"url-loader": "^4.1.0",
"vue-loader": "^15.9.1",
"vue-template-compiler": "^2.6.11",
"webpack": "^4.43.0",
"webpack-cli": "^3.3.11"
}
}
================================================
FILE: public/background.html
================================================
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="static/background.js"></script>
</head>
<body>
<iframe id="iframe" width="400 px" height="600 px"></iframe>
</body>
</html>
================================================
FILE: public/manifest.json
================================================
{
"manifest_version": 2,
"name": "茶友会 - 淘宝查券助手",
"short_name": "茶友会",
"description": "茶友会是自动为你查找淘宝优惠券,自动签到领飞猪里程的多功能购物助手",
"version": "0.3.1",
"background": {
"page": "background.html",
"persistent": true
},
"browser_action": {
"default_icon": "static/image/icon.png",
"default_popup": "popup.html"
},
"content_scripts": [
{
"matches": ["*://*.taobao.com/*", "*://*.tmall.com/*", "*://*.fliggy.com/*"],
"exclude_matches": ["*://ratewrite.tmall.com/*", "*://rate.taobao.com/*", "*://passport.taobao.com/*", "*://buy.taobao.com/*", "*://buy.tmall.com/*"],
"js": [
"static/zepto.min.js",
"static/content_script.js"
],
"run_at": "document_end",
"all_frames": true
}
],
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self';",
"icons": {
"16": "static/image/icon@16.png",
"48": "static/image/icon@48.png",
"128": "static/image/icon@128.png"
},
"web_accessible_resources": [
"static/touch-emulator.js"
],
"permissions": [
"*://*.taobao.com/*",
"*://*.tmall.com/*",
"*://*.fliggy.com/*",
"webRequest",
"webRequestBlocking",
"alarms",
"contextMenus",
"notifications"
]
}
================================================
FILE: public/popup.html
================================================
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>茶友会</title>
<meta name="viewport" content="user-scalable=no" />
</head>
<body data-weui-theme="light">
<div class="popup" id="popup">
<div id="app"></div>
</div>
<!-- body 最后 -->
<script src="static/popup.js"></script>
</body>
</html>
================================================
FILE: public/start.html
================================================
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>茶友会安装成功!</title>
</head>
<body>
<div class="start">
<div class="page msg_success js_show">
<div class="weui-msg">
<div class="weui-msg__icon-area"><i class="weui-icon-success weui-icon_msg"></i></div>
<div class="weui-msg__text-area">
<h2 class="weui-msg__title">茶友会安装成功</h2>
<p class="weui-msg__desc">我可以自动为你自动签到领飞猪里程、自动查找渠道优惠券</p>
<img class="find-coupon" src="https://jjbcdn.zaoshu.so/teaclub/find-coupon.gif" alt="自动找券演示">
</div>
<div class="weui-msg__opr-area">
<p class="weui-btn-area">
<a href="https://s.click.taobao.com/t?e=m%3D2%26s%3D%2B9llzsJIcM0cQipKwQzePCperVdZeJvipRe%2F8jaAHci5VBFTL4hn2dDrVEnLBoSlc4zWPc6e822Db670SJ30CbMrQT2ouyP2AXDMm4A2j9lz%2B247WylknIVK987Q3rqfM7kxpdONUAI%2BTx4Gw%2F2vfyT8IooNW8SzlmpSTfxBwAxgkfYt2XlEMH96zNtHoLpw2dsnFJC0C7enlwybGdCkReewTZ7SVcdIGejP7SXXNsoWP2mcc3LMu5j%2B7kFbwj1JvDj80XT4gyoWV9M8fA%2BI3AZR9GdqBzz5P8z7%2F1pu85%2FWMe%2FYdh9vkNlnivLx%2B4MuRicMup9U2qVxKmPmpIKZsA%3D%3D" class="weui-btn weui-btn_primary">亲自试试</a>
</p>
</div>
<div class="weui-msg__extra-area">
<div class="weui-footer">
<p class="weui-footer__text">Copyright © Ming</p>
</div>
</div>
</div>
</div>
</div>
<script src="static/start.js"></script>
</body>
</html>
================================================
FILE: readme.md
================================================
# 茶友会 - 淘宝查券助手
茶友会是自动为你查找淘宝优惠券,自动签到领飞猪里程的多功能购物助手。
**强烈推荐使用 Chrome 商店安装**(这样才能获得自动更新):
<a target='_blank' rel='nofollow' href='https://chrome.google.com/webstore/detail/igedhbjllcmgidlmhclmphmhlllkibkb'>
<img alt='Chrome
web store' width='496' height='150' src='https://jjbcdn.zaoshu.so/web/cws_badge_496x150.png' />
</a>
或者直接下载的 CRX文件手动安装(非常不建议)
<a href="http://jjb.zaoshu.so/updates/latest?app=teaclub" target="_black">
<img alt="下载 CRX 文件" class="firefox" src="https://jjbcdn.zaoshu.so/crx-icon.png" width="32px">
</a>
*此方法通常只适用于 Chromium 内核的国产浏览器,因为 Chrome 出于安全原因已禁止通过除 Chrome 官方商店以外的其他渠道安装拓展。*
## 主要功能
* 自动查询当前浏览商品的渠道优惠券
* 自动签到领取飞猪里程
* 自动找拼多多低价同款
## 演示

## 重要提示
1. 茶友会并非开源软件,不许可您以任何形式进行再发行,请仔细阅读[#协议和授权](https://github.com/sunoj/teaclub#%E5%8D%8F%E8%AE%AE%E5%92%8C%E6%8E%88%E6%9D%83)。
2. 当前仓库是插件源代码,无法直接安装,如需安装请自行参考 [#如何开发](https://github.com/sunoj/teaclub#%E5%A6%82%E4%BD%95%E5%BC%80%E5%8F%91) 编译。
3. 茶友会绝对不会在任何情况下强行劫持任何网页的访问,如果发现类似问题请善用 Google 搜索并使用二分法停用插件排除,同时考虑运营商劫持的可能性。或者,为了防止茶友会的影响亦可直接卸载茶友会。故不再回复类似的 Issue。详情参考:[#安全提示](https://github.com/sunoj/teaclub#%E5%AE%89%E5%85%A8%E6%8F%90%E7%A4%BA)
## 如何开发
* 安装依赖
> yarn
* 开始开发
> BUILDID=1 VERSION=1.1.1 BROWSER=chrome yarn build
`主要作用就是合并压缩代码,质疑代码和市场版本不一致,请先自行打包一下再对比`
## 安全提示
茶友会不会在任何情况下强行劫持访问、插入恶意代码、上传隐私信息或利用你的电脑挖矿。
若你发现任何类似问题,请首先确保你使用的是商店版本,不建议在任何情况下使用第三方提供的安装包。
## 系统支持
目前茶友会对 Windows 和 Mac 平台的 Chrome 有较好的支持。
Ubuntu 有明确的兼容问题,由于作者不拥有任何 Ubuntu 设备,因此暂时无法解决。
## 协议和授权
茶友会并非一个开源软件,作者保留全部的权利。
公开源代码的目的是为了让使用者能够审计代码,但是你仍然可以就以下方式合法的使用本项目的全部代码和资源:
1. 个人使用
2. 以学习目的使用全部或部分代码
但你不可以:
1. 将本项目的部分或全部代码和资源进行任何形式的再发行(尤其是上传到 Chrome 商店)
2. 利用本项目的部分或全部代码和资源进行任何商业行为
## 贡献代码
茶友会并非一个开源项目,也不是社区共同创造,其全部功能由作者独立完成。
如果你愿意放弃所有权利,并将权利无条件转让给茶友会作者,欢迎您贡献代码。
## 提交反馈
欢迎提交 issue,请写清楚遇到问题的原因,浏览器和操作系统环境,重现的流程。
任何反馈问题的 issue 均需按照模板格式填写,否则将被直接关闭。
如果有开发能力,建议在本地调试出出错的代码。
## 联系作者
请发邮件至:`ming@tiny.group`
请勿发送功能咨询邮件,将不会收到回复。相关功能细节请自行了解。
## 相关项目
<h3>
<a href="https://github.com/sunoj/jjb" target="_blank">京价保</a>
</h3>
================================================
FILE: src/account.js
================================================
import { getSetting } from './utils'
export const getLoginState = function () {
let loginState = {
pc: getSetting('login-state_pc', {
state: "unknown"
}),
m: getSetting('login-state_m', {
state: "unknown"
}),
class: "unknown"
}
// 处理登录状态
if (loginState.pc.state == 'alive' || loginState.m.state == 'alive') {
loginState.class = "alive"
}
if (loginState.pc.state == 'failed' || loginState.m.state == 'failed') {
loginState.class = "failed"
}
if (loginState.pc.time && loginState.m.time) {
if (new Date(loginState.pc.time) > new Date(loginState.m.time)) {
loginState.class = loginState.pc.state
} else {
loginState.class = loginState.m.state
}
}
return loginState
}
================================================
FILE: src/background.js
================================================
$ = window.$ = window.jQuery = require('jquery')
import * as _ from "lodash"
import Logline from 'logline'
import Dexie from 'dexie';
import {DateTime} from 'luxon'
import {tasks, mapFrequency, getTasks, getTask} from './tasks'
import {rand, getSetting, saveSetting} from './utils'
import {getLoginState} from './account'
const db = new Dexie("messages");
db.version(1).stores({ messages: "++id,type,timestamp" });
async function newMessage(messageId, data) {
let order = await db.messages.where('id').equals(messageId).toArray();
if (order && order.length > 0) return await db.messages.update(messageId, data)
let messageInfo = Object.assign(data, {
id: messageId,
})
return await db.messages.add(messageInfo);
}
async function updateMessages() {
// 最多只展示最近 30 天的消息
let last30Day = Date.now() - 60*60*1000*24*30;
let messages = await db.messages.where('timestamp').above(last30Day).reverse().sortBy('timestamp')
saveSetting('messages', messages)
chrome.runtime.sendMessage({
action: "messages_updated",
messages: messages
});
}
Logline.using(Logline.PROTOCOL.INDEXEDDB)
var logger = {}
var mobileUAType = getSetting('uaType', 1)
// 设置默认频率
_.forEach(tasks, (task) => {
let frequency = getSetting(`task-${task.id}_frequency`)
if (!frequency) {
localStorage.setItem(`task-${task.id}_frequency`, task.frequency)
}
})
// This is to remove X-Frame-Options header, if present
chrome.webRequest.onHeadersReceived.addListener(
function(info) {
var headers = info.responseHeaders;
for (var i=headers.length-1; i>=0; --i) {
var header = headers[i].name.toLowerCase();
if (header == 'x-frame-options' || header == 'frame-options') {
headers.splice(i, 1); // Remove header
}
}
return {responseHeaders: headers};
},
{
urls: ['*://*.taobao.com/*', '*://*.tmall.com/*', '*://*.fliggy.com/*'],
types: ['sub_frame']
},
['blocking', 'responseHeaders']
);
chrome.runtime.onInstalled.addListener(function (object) {
let installed = localStorage.getItem('installed')
let uaType = localStorage.getItem('uaType')
if (installed) {
if (!uaType) {
localStorage.setItem('uaType', 1);
}
localStorage.setItem('oldUser', 'Y')
console.log("已经安装")
} else {
localStorage.setItem('installed', 'Y');
localStorage.setItem('uaType', rand(3));
chrome.tabs.create({url: "/start.html"}, function (tab) {
console.log("茶友会安装成功!");
});
}
});
var popularPhoneUA = [
'Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1 AliApp(TB-PD/3.0.2)',
'Mozilla/5.0 (iPhone; CPU iPhone OS 9_4_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/10.2 Mobile/15E148 Safari/604.1 AliApp(TB-PD/3.0.2)',
'Mozilla/5.0 (iPhone9,4; U; CPU iPhone OS 10_0_1 like Mac OS X) AppleWebKit/602.1.50 (KHTML, like Gecko) Version/10.0 Mobile/14A403 Safari/602.1 AliApp(TB-PD/3.0.2)',
'Mozilla/5.0 (Linux; Android 6.0.1; SM-G920V Build/MMB29K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.98 Mobile Safari/537.36 AliApp(TB-PD/2.6.5)'
];
chrome.webRequest.onBeforeSendHeaders.addListener(
function (details) {
for (var i = 0; i < details.requestHeaders.length; ++i) {
if (details.requestHeaders[i].name === 'User-Agent') {
details.requestHeaders[i].value = popularPhoneUA[mobileUAType];
break;
}
}
return {
requestHeaders: details.requestHeaders
};
}, {
urls: [
"*://*.m.taobao.com/*",
]
}, ['blocking', 'requestHeaders']);
// 判断浏览器
try {
browser.runtime.getBrowserInfo().then(function (browserInfo) {
localStorage.setItem('browserName', browserInfo.name);
})
} catch (error) {}
chrome.alarms.onAlarm.addListener(function( alarm ) {
log('background', "onAlarm", alarm)
let taskId = alarm.name.split('_')[1]
switch(true){
// 计划任务
case alarm.name.startsWith('runScheduleJob'):
runJob(taskId, true)
break;
// 定时任务
case alarm.name.startsWith('runJob'):
runJob(taskId)
break;
// 周期运行(10分钟)
case alarm.name == 'cycleTask':
findJobs()
runJob()
updateIcon()
break;
case alarm.name.startsWith('clearIframe'):
resetIframe(taskId || 'iframe')
break;
case alarm.name.startsWith('destroyIframe'):
$("#" + taskId).remove();
break;
case alarm.name.startsWith('closeTab'):
try {
chrome.tabs.get(taskId, (tab) => {
if (tab) {
chrome.tabs.remove(tab.id)
}
})
} catch (e) {}
break;
case alarm.name == 'reload':
chrome.runtime.reload()
chrome.alarms.clearAll()
// 保留3天内的log
Logline.keep(3);
break;
}
})
// 保存任务栈
function saveJobStack(jobStack) {
jobStack = _.uniq(jobStack)
localStorage.setItem('jobStack', JSON.stringify(jobStack));
}
function scheduleJob(task) {
let hour = DateTime.local().hour;
for (var i = 0, len = task.schedule.length; i < len; i++) {
let scheduledHour = task.schedule[i]
if (scheduledHour > hour) {
let scheduledTime = DateTime.local().set({
hour: scheduledHour,
minute: rand(2) - 1,
second: rand(55)
}).valueOf()
chrome.alarms.create('runScheduleJob_' + task.id, {
when: scheduledTime
})
log('background', "schedule job created", {
job: task,
time: scheduledHour,
when: scheduledTime
})
break;
}
}
}
// 寻找乔布斯
function findJobs() {
let jobStack = getSetting('jobStack', [])
let taskList = getTasks()
taskList.forEach(function(task) {
if (task.suspended) {
return console.log(task.title, '由于账号未登录已暂停运行')
}
// 如果任务有时间安排,则把任务安排到最近的下一个时段
if (task.schedule) {
return chrome.alarms.get('runScheduleJob_' + task.id, function (alarm) {
if (!alarm || alarm.scheduledTime < Date.now()) {
return scheduleJob(task)
} else {
console.log("job already scheduled ", alarm)
}
})
}
switch(task.frequency){
case '2h':
// 如果从没运行过,或者上次运行已经过去超过2小时,那么需要运行
if (!task.last_run_at || (DateTime.local() > DateTime.fromMillis(task.last_run_at).plus({ hours: 2 }))) {
jobStack.push(task.id)
}
break;
case '5h':
// 如果从没运行过,或者上次运行已经过去超过5小时,那么需要运行
if (!task.last_run_at || (DateTime.local() > DateTime.fromMillis(task.last_run_at).plus({ hours: 5 }))) {
jobStack.push(task.id)
}
break;
case 'daily':
// 如果从没运行过,或者上次运行不在今天,或者是签到任务但未完成
if (!task.last_run_at || !(DateTime.local().hasSame(DateTime.fromMillis(task.last_run_at), 'day')) || (task.checkin && !task.checked)) {
jobStack.push(task.id)
}
break;
default:
console.log('ok, never run ', task.title)
}
});
saveJobStack(jobStack)
}
function log(type, message, details) {
if (!logger[type]) {
logger[type] = new Logline(type)
}
logger[type].info(message, details)
console.log(new Date(), type, message, details)
}
function resetIframe(domId) {
$("#" + domId).remove();
let iframeDom = `<iframe id="${domId}" width="400 px" height="600 px" src=""></iframe>`;
$('body').append(iframeDom);
}
function incrementUsage(task) {
let year = new Date().getFullYear()
let today = DateTime.local().toFormat("o")
let hour = new Date().getHours()
saveSetting(`temporary:usage-${task.id}_${year}d:${today}:h:${hour}`, task.usage.hour + 1)
saveSetting(`temporary:usage-${task.id}_${year}d:${today}`, task.usage.daily + 1)
}
// 执行组织交给我的任务
function runJob(taskId, force = false) {
// 不在凌晨阶段运行非强制任务
if (DateTime.local().hour < 6 && !force) {
return console.log('Silent Night')
}
log('background', "run job", {
jobId: taskId,
force: force
})
// 如果没有指定任务ID 就从任务栈里面找一个
if (!taskId) {
let jobStack = getSetting('jobStack', [])
if (jobStack && jobStack.length > 0) {
taskId = jobStack.shift();
saveJobStack(jobStack)
} else {
return log('info', new Date(), '好像没有什么事需要我做...')
}
}
let task = getTask(taskId)
// 如果任务已暂停
if (task.pause) {
return log('job', task.title, '由于运行次数超限而被暂停')
}
// 如果任务暂停或者已经完成
if ((task.suspended || task.checked) && !force) {
return log('job', task.title, '由于账号未登录已暂停运行')
}
if (task && (task.frequency != 'never' || force)) {
log('background', "run", task)
incrementUsage(task)
if (task.mode == 'iframe') {
openByIframe(task.url, 'job')
} else {
chrome.tabs.create({
index: 1,
url: task.url,
active: false,
pinned: true
}, function (tab) {
// 将标签页静音
chrome.tabs.update(tab.id, {
muted: true
}, function (result) {
log('background', "muted tab", result)
})
chrome.alarms.create('closeTab_'+tab.id, {delayInMinutes: 3})
})
}
}
}
function openByIframe(src, type, delayTimes = 0) {
// 加载新的任务
let iframeId = "iframe"
let keepMinutes = 3
if (type == 'temporary') {
iframeId = 'iframe' + Math.random().toString(36).substring(7);
keepMinutes = 1
}
// 当前任务过多则等待
if ($('iframe').length > 5 && delayTimes < 6) {
setTimeout(() => {
openByIframe(src, type, delayTimes + 1)
}, (10 + rand(10)) * 1000);
return console.log('too many iframe pages', src, delayTimes)
}
// 运行
resetIframe(iframeId)
$("#" + iframeId).attr('src', src)
// 设置重置任务
chrome.alarms.create(`${(type == 'temporary' ? 'destroyIframe' : 'clearIframe')}_${iframeId}`, {
delayInMinutes: keepMinutes
})
}
function updateUnreadCount(change = 0) {
let lastUnreadCount = localStorage.getItem('unreadCount') || 0
let unreadCount = parseInt(Number(lastUnreadCount) + change)
if (unreadCount < 0) {
unreadCount = 0
}
localStorage.setItem('unreadCount', unreadCount);
if (unreadCount > 0) {
let unreadCountText = unreadCount.toString()
if (unreadCount > 100) {
unreadCountText = '99+'
}
chrome.browserAction.setBadgeText({ text: unreadCountText });
chrome.browserAction.setBadgeBackgroundColor({ color: "#4caf50" });
} else {
chrome.browserAction.setBadgeText({ text: "" });
}
}
$( document ).ready(function() {
log('background', "document ready")
// 每20分钟运行一次定时任务
chrome.alarms.create('cycleTask', {
periodInMinutes: 20
})
// 每600分钟完全重载
chrome.alarms.create('reload', {periodInMinutes: 600})
// 载入后马上运行一次任务查找
findJobs()
// 载入显示未读数量
updateUnreadCount()
// 加载任务参数
loadSettingsToLocalStorage('teaclub:task-parameters')
loadSettingsToLocalStorage('teaclub:action-links')
// 加载推荐设置
loadRecommendSettingsToLocalStorage()
})
function openWebPageAsMobile(url) {
chrome.windows.create({
width: 420,
height: 800,
url: url,
type: "popup"
});
}
// 点击通知
chrome.notifications.onClicked.addListener(function (notificationId) {
if (notificationId.split('_').length > 0) {
let type = notificationId.split('_')[1]
if (type && type.length > 1) {
switch (type) {
case 'fliggy':
chrome.tabs.create({
url: "https://teaclub.zaoshu.so/sites/fliggy"
})
break;
default:
chrome.tabs.create({
url: "https://teaclub.zaoshu.so/sites/taobao"
})
}
}
}
})
// 根据登录状态调整图标显示
function updateIcon() {
let loginState = getLoginState()
switch (loginState.class) {
case 'alive':
chrome.browserAction.getBadgeText({}, function (text){
if (text == "X" || text == " ! ") {
chrome.browserAction.setBadgeText({
text: ""
});
chrome.browserAction.setTitle({
title: "茶友会"
})
}
})
chrome.browserAction.setIcon({
path : {
"19": "static/image/icon@19.png",
"38": "static/image/icon@38.png"
}
});
chrome.contextMenus.removeAll();
break;
case 'failed':
chrome.browserAction.setBadgeBackgroundColor({
color: [190, 190, 190, 230]
});
chrome.browserAction.setBadgeText({
text: "X"
});
chrome.browserAction.setTitle({
title: "账号登录失效"
})
chrome.contextMenus.removeAll();
chrome.contextMenus.create({
title: "账号登录失效,点击登录",
contexts: ["browser_action"],
onclick: function() {
openLoginPage()
}
});
break;
case 'warning':
chrome.browserAction.setBadgeBackgroundColor({
color: "#EE7E1B"
});
chrome.browserAction.setBadgeText({
text: " ! "
});
chrome.contextMenus.removeAll();
chrome.contextMenus.create({
title: "账号登录失效,点击登录",
contexts: ["browser_action"],
onclick: function() {
openLoginPage()
}
});
break;
default:
break;
}
}
function openLoginPage() {
chrome.tabs.create({
url: "https://buyertrade.taobao.com/trade/itemlist/list_bought_items.htm"
})
}
// 保存登录状态
function saveLoginState(loginState) {
let previousState = getLoginState()
localStorage.setItem('login-state_' + loginState.type, JSON.stringify({
time: new Date(),
message: loginState.content || loginState.message,
state: loginState.state
}));
chrome.runtime.sendMessage({
action: "loginState_updated",
data: loginState
});
// 如果登录状态从失败转换到了在线
if (previousState.class != 'alive' && loginState.state == "alive") {
console.log('user account turn alive')
setTimeout(() => {
findJobs()
}, 5000);
setTimeout(() => {
runJob()
}, 15000);
}
}
// 浏览器通知(合并)
// mute_night
function sendChromeNotification(id, content) {
let hour = DateTime.local().hour;
let muteNight = getSetting('mute_night');
if (muteNight && hour < 6) {
log('background', 'mute_night', content);
} else {
chrome.notifications.create(id, content)
log('message', id, content);
}
}
function runTask(msg, sendResponse) {
let task = getTask(msg.taskId)
// set 临时运行
localStorage.setItem('temporary_job' + task.id + '_frequency', 'onetime');
// 任务因为频率受限无法运行
if (task.pause) {
sendChromeNotification(new Date().getTime().toString(), {
type: "basic",
title: "任务因为频率受限无法运行",
message: task.title + "已达到最大时段频率,每小时:" + task.rateLimit.hour,
iconUrl: 'static/image/128.png'
})
sendResponse({
result: "pause",
message: "任务因为频率受限无法运行"
})
} else {
runJob(task.id, true)
sendResponse({
result: "success"
})
if (!msg.hideNotice) {
sendChromeNotification(new Date().getTime().toString(), {
type: "basic",
title: "正在重新运行" + task.title,
message: "任务运行大约需要2分钟,如果有情况我再叫你(请勿连续运行)",
iconUrl: 'static/image/128.png'
})
}
}
}
function markCheckinStatus(msg) {
let task = getTask(msg.taskId)
if (task) {
let year = new Date().getFullYear()
let checkinKey = `checkin_${task.key}`
let currentStatus = getSetting(checkinKey, null)
let data = {
date: DateTime.local().toFormat("o"),
time: new Date(),
value: msg.value
}
if (msg.month) {
localStorage.setItem(`order-fliggy-${year}-${msg.month}`, 'Y');
}
if (msg.orderId) {
localStorage.setItem(`order-fliggy_${msg.orderId}`, 'Y');
}
if (currentStatus && currentStatus.date == DateTime.local().toFormat("o")) {
console.log('已经记录过今日签到状态了')
} else {
localStorage.setItem(checkinKey, JSON.stringify(data));
return data
}
}
}
function updateRunStatus(msg) {
let task = getTask(msg.taskId)
if (task) {
localStorage.setItem('task-' + task.id + '_lasttime', new Date().getTime())
saveLoginState({
content: task.title + "成功运行",
state: "alive",
type: msg.mode || task.type[0]
})
// 如果任务周期小于10小时,且不是计划任务,则安排下一次运行
if (mapFrequency[task.frequency] < 600 && !task.schedule) {
chrome.alarms.create('runJob_' + task.id, {
delayInMinutes: mapFrequency[task.frequency]
})
}
}
}
// 加载任务参数
function loadSettingsToLocalStorage(key) {
$.getJSON(`https://teaclub.zaoshu.so/setting/${key}`, function (json) {
saveSetting(key, json)
})
}
// 加载推荐设置
function loadRecommendSettingsToLocalStorage() {
$.getJSON("https://teaclub.zaoshu.so/setting/teaclub:recommend", function (json) {
if (json.displayPopup) {
saveSetting('displayPopup', json.displayPopup)
}
if (json.events) {
saveSetting('events', json.events)
}
if (json.announcements && json.announcements.length > 0) {
saveSetting('announcements', json.announcements)
}
if (json.promotions) {
saveSetting('promotions', json.promotions)
}
if (json.recommendedLinks && json.recommendedLinks.length > 0) {
saveSetting('recommendedLinks', json.recommendedLinks)
} else {
localStorage.removeItem('recommendedLinks')
}
if (json.uninstallURL) {
chrome.runtime.setUninstallURL(json.uninstallURL)
}
if (json.recommendServices && json.recommendServices.length > 0) {
saveSetting('recommendServices', json.recommendServices)
}
});
}
function sendMessageToPage(targetPage, data) {
chrome.tabs.sendMessage(targetPage.tab.id, data, {}, function (response) {
console.log('send message to tabs response', data, response)
})
}
function timeoutPromise(promise, ms) {
return new Promise(function(resolve, reject) {
setTimeout(function() {
reject(new Error("timeout"))
}, ms)
promise.then(resolve, reject)
})
}
// 查找优惠券
async function searchCoupon(params) {
try {
let response = await timeoutPromise(fetch(`https://teaclub.zaoshu.so/coupon/search?sku=${params.sku}&keyword=${params.title}&merchant=${params.merchant}`), 10000)
let details = await response.json();
return details;
} catch (error) {
console.error(error)
return null
}
}
// 处理消息通知
chrome.runtime.onMessage.addListener(function(msg, sender, sendResponse) {
if (!msg.action) {
msg.action = msg.text
}
let loginState = getLoginState()
log('msg', new Date(), msg);
switch(msg.action){
// 保存登录状态
case 'saveLoginState':
saveLoginState(msg)
break;
// 获取登录状态
case 'getLoginState':
sendResponse(loginState)
break;
// 打开登录页面
case 'openLogin':
openLoginPage()
break;
// 保存变量值
case 'setVariable':
localStorage.setItem(msg.key, JSON.stringify(msg.value));
break;
// 获取设置
case 'getSetting':
let setting = getSetting(msg.content)
let temporarySetting = localStorage.getItem('temporary_' + msg.content)
// 如果存在临时设置
if (temporarySetting) {
// 临时设置5分钟失效
setTimeout(() => {
localStorage.removeItem('temporary_' + msg.content)
}, 60*5*1000);
return sendResponse(temporarySetting)
}
sendResponse(setting)
break;
// 领取订单的里程
case 'getOrderFliggy':
let orderStatus = localStorage.getItem(`order-fliggy_${msg.orderId}`)
console.log('getOrderFliggy', msg.orderId, orderStatus)
// 如果没有领取过
if (!(orderStatus && orderStatus == 'Y')) {
let url = `https://www.fliggy.com/mytrip/?tvm=tcd&orderId=${msg.orderId}`
setTimeout(() => {
openByIframe(url, 'temporary')
}, rand(20) * 1000);
}
sendResponse({
working: true
})
break;
case 'openUrlAsMobile':
openWebPageAsMobile(msg.url)
break;
case 'option':
localStorage.setItem(''+msg.title, msg.content);
break;
// 查询消息列表
case 'getMessages':
setTimeout(async () => {
await updateMessages()
}, 50);
break;
// 手动运行任务
case 'runTask':
runTask(msg, sendResponse)
break;
// 签到通知
case 'checkin_notice':
let mute_checkin = getSetting('mute_checkin')
if (mute_checkin && mute_checkin == 'checked' && !msg.test) {
console.log('checkin', msg)
} else {
let icon = 'static/image/coin.png'
let type = "basic"
if (msg.type == 'mileage') {
icon = 'static/image/mileage.png'
type = 'fliggy'
}
sendChromeNotification( new Date().getTime().toString() + '_' + msg.reward, {
type: type,
title: msg.title,
message: msg.content,
iconUrl: icon
})
}
break;
// 签到状态
case 'markCheckinStatus':
let result = markCheckinStatus(msg)
sendResponse({
result
})
break;
// 更新运行状态
case 'updateRunStatus':
updateRunStatus(msg)
sendResponse({
result: true
})
break;
// 查询优惠券
case 'queryCoupon':
var disable_same_goods = getSetting('disable_same_goods')
setTimeout(async () => {
let result = await searchCoupon(msg.params)
if (disable_same_goods) {
result.similarGoods = null
}
sendMessageToPage(sender, {
type: "couponInfo",
content: result
})
}, 50);
sendResponse({
result: true
})
break;
case 'coupon':
var coupon = msg.content
var mute_coupon = getSetting('mute_coupon')
if (mute_coupon && mute_coupon == 'checked') {
console.log('coupon', msg)
} else {
sendChromeNotification( new Date().getTime().toString() + "_coupon_" + coupon.batch, {
type: "basic",
title: msg.title,
message: coupon.name + coupon.price,
isClickable: true,
iconUrl: 'static/image/coupon.png'
})
}
break;
case 'clearUnread':
updateUnreadCount(-999)
break;
case 'myTab':
sendResponse({
tab: sender.tab
});
break;
default:
console.log("Received %o from %o, frame", msg, sender.tab, sender.frameId);
}
// 更新图标
updateIcon()
// 保存消息
switch (msg.action) {
case 'coupon':
case 'notice':
case 'checkin_notice':
if (msg.test) {
break;
}
let message = {
type: msg.type || msg.action, // 通知的类型
batch: msg.batch, // 批次,通常是优惠券的属性
reward: msg.reward, // 奖励的类型
unit: msg.unit || msg.reward || msg.batch, // 奖励的单位
value: msg.value, // 奖励的数量
title: msg.title,
content: msg.content,
timestamp: Date.now()
}
let uuid = msg.uuid || Date.now()
updateUnreadCount(1)
setTimeout(async () => {
await newMessage(uuid, message);
}, 50);
setTimeout(async () => {
await updateMessages()
}, 3000);
break;
}
if (msg.text != 'saveAccount') {
log('message', msg.text, msg);
}
// 如果消息 300ms 未被回复
return true
});
Logline.keep(3);
================================================
FILE: src/components/app.vue
================================================
<template>
<div>
<div class="main-container">
<div class="settings">
<div class="weui-tab">
<div :class="`${scienceOnline} weui-navbar`">
<div class="weui-navbar__item weui-bar__item_on" data-type="frequency_settings">任务设置</div>
<div class="weui-navbar__item" data-type="other_settings">高级设置</div>
</div>
<div class="weui-tab__panel">
<form
id="settings"
data-persist="garlic"
data-domain="true"
data-destroy="false"
method="POST"
>
<div class="frequency_settings settings_box">
<div class="weui-cells weui-cells_form">
<div
class="weui-cell weui-cell_select weui-cell_select-after"
v-for="task in taskList"
:key="task.id"
>
<div class="weui-cell__bd job-m">
<span
data-microtip-position="bottom-right"
role="tooltip"
:aria-label="task.description"
>
<a
v-if="task.platform == 'm'"
class="openMobilePage"
:data-url="task.baseUrl || task.url"
>{{task.title}}</a>
<a v-else :href="task.url" target="_blank">{{task.title}}</a>
</span>
<span
v-show="task.suspended && !task.checked"
data-microtip-position="bottom-right"
role="tooltip"
aria-label="未知登录状态,请点击任务名称手动运行"
>
<i class="job-state weui-icon-waiting-circle"></i>
</span>
<span
v-show="task.checked"
data-microtip-position="bottom-right"
role="tooltip"
:aria-label="task.checkin_description"
>
<i class="today weui-icon-success-circle"></i>
</span>
<i
v-show="!task.checked && !task.suspended"
@click="retryTask(task)"
class="reload-icon"
data-microtip-position="bottom-right"
role="tooltip"
:aria-label="task.last_run_description"
></i>
</div>
<div class="weui-cell__bd">
<select class="weui-select" v-auto-save :name="`task-${task.id}_frequency`">
<option
v-for="option in task.frequencyOption"
:value="option"
:key="option"
>{{ frequencyOptionText[option] }}</option>
</select>
</div>
</div>
</div>
<div class="other_actions">
<div class="recommendation">
<h3 style="text-align: center;color: #666;">服务推荐</h3>
<p class="recommendServices">
<span
:class="service.class"
v-for="service in recommendServices"
:key="service.title"
>
<a
target="_blank"
data-microtip-position="top"
role="tooltip"
:aria-label="service.description"
:href="service.link"
>{{service.title}}</a>
</span>
</p>
<div class="recommendedLink">
<p v-for="link in recommendedLinks" :key="link.title">
<a
v-if="link.mobile"
class="openMobilePage"
:style="link.style"
:data-url="link.url"
>{{link.title}}</a>
<a
v-else
:href="link.url"
:style="link.style"
class="weui-form-preview__btn weui-form-preview__btn_primary"
target="_blank"
>{{link.title}}</a>
</p>
</div>
</div>
</div>
<div class="tips bottom-tips"></div>
</div>
<div class="other_settings settings_box" style="display: none">
<div class="weui-cells weui-cells_form">
<div class="weui-cell weui-cell_switch">
<div class="weui-cell__bd">停用自动找券</div>
<div class="weui-cell__ft">
<input
class="weui-switch"
v-auto-save
type="checkbox"
name="disable_find_coupon"
>
</div>
</div>
<div class="weui-cell weui-cell_switch">
<div class="weui-cell__bd">停止“找同款”</div>
<div class="weui-cell__ft">
<input
class="weui-switch"
v-auto-save
type="checkbox"
name="disable_same_goods"
>
</div>
</div>
<div class="weui-cell weui-cell_switch">
<div class="weui-cell__bd">不再提示签到通知</div>
<div class="weui-cell__ft">
<input class="weui-switch" v-auto-save type="checkbox" name="mute_checkin">
</div>
</div>
<div class="weui-cell weui-cell_switch">
<div class="weui-cell__bd">
<span
data-microtip-position="top"
role="tooltip"
aria-label="开启后不再晚上12点至凌晨6点发送浏览器通知"
>开启夜晚防打扰</span>
</div>
<div class="weui-cell__ft">
<input class="weui-switch" type="checkbox" v-auto-save name="mute_night">
</div>
</div>
</div>
<div class="other_actions">
<p
class="tips"
style="text-align: center;"
>茶友会当前版本为预览版,相关功能并不完善,若有功能建议或反馈,请写邮件至:ming@tiny.group</p>
</div>
<p
class="text-tips version showChangelog"
@click="showChangelog"
data-microtip-position="top"
role="tooltip"
aria-label="点击查看版本更新记录"
>
当前版本:{{currentVersion}}
<span
class="weui-badge weui-badge_dot"
v-if="newChangelog"
></span>
<span class="weui-badge new-version" v-if="newVersion">有新版</span>
</p>
</div>
</form>
</div>
</div>
<div class="bottom-box weui-tabbar">
<div
class="avatar"
data-microtip-position="top-right"
role="tooltip"
:aria-label="loginStateDescription"
>
<a
id="loginState"
:class="` login-state ${loginState.class}`"
:target="loginState.class != 'alive' ? '_blank' : '_self'"
:href="loginState.class != 'alive' ? 'https://i.taobao.com/my_taobao.htm' : ''"
></a>
</div>
<links></links>
</div>
</div>
<div class="contents">
<div class="weui-tab">
<div class="weui-navbar">
<div
:class="`weui-navbar__item ${contentType == 'messages' ? 'weui-bar__item_on' : ''}`"
@click="switchContentType('messages')"
>
最近通知
<span class="weui-badge" v-if="unreadCount > 0">{{unreadCount}}</span>
</div>
<div
:class="`weui-navbar__item zaoshu-tab ${contentType == 'discounts' ? 'weui-bar__item_on' : ''}`"
@click="switchContentType('discounts')"
>
<img src="../../static/image/zaoshu.png" alt="" class="zaoshu-icon">
枣树集惠
<span
class="weui-badge weui-badge_dot new-discounts"
v-if="newDiscounts"
></span>
</div>
</div>
<div class="weui-tab__panel">
<div id="messages" v-if="contentType == 'messages'" class="contents-box messages">
<div class="weui-cells message-items" v-if="messages && messages.length > 0">
<li v-for="(message, index) in messages" :key="index">
<div
:class="`weui-panel__bd message-item type-${message.type}`"
v-show="!selectedTab || selectedTab == message.type"
>
<div class="weui-media-box weui-media-box_text">
<h4 class="weui-media-box__title message">
<i :class="`${message.type} ${message.reward}`"></i>
{{message.title}}
</h4>
<p class="weui-media-box__desc">{{message.content}}</p>
<ul class="weui-media-box__info">
<li class="weui-media-box__info__meta">时间: {{message.time}}</li>
</ul>
</div>
</div>
</li>
</div>
<div class="no_message" v-else>暂时还没有未读消息</div>
</div>
<discounts v-if="contentType == 'discounts'"/>
</div>
</div>
<div class="bottom">
<div class="weui-tabbar">
<a
class="showChangelog weui-tabbar__item"
@click="showChangelog"
data-microtip-position="top"
role="tooltip"
aria-label="查看茶友会最近更新记录"
style="position: relative;"
>
<img src="../../static/image/update.png" alt="" class="weui-tabbar__icon">
<p class="weui-tabbar__label">
最近更新
<span
class="weui-badge weui-badge_dot"
style="position: absolute;top: 0;right: 4em;"
v-if="newChangelog"
></span>
<span
class="weui-badge"
style="position: absolute;top: -.4em;right: 2em;"
v-if="newVersion"
>有新版</span>
</p>
</a>
<a
id="openGithub"
class="weui-tabbar__item"
href="https://github.com/sunoj/teaclub"
data-microtip-position="top"
role="tooltip"
aria-label="点击查看本插件的全部代码"
target="_blank"
>
<img src="../../static/image/github.png" alt="" class="weui-tabbar__icon">
<p class="weui-tabbar__label">源代码</p>
</a>
</div>
</div>
</div>
</div>
<div class="dialogs">
<guide v-if="showGuide" :login-state="loginState"></guide>
<popup v-if="showPopup" @close="showPopup = false"></popup>
</div>
</div>
</template>
<script>
import * as _ from "lodash";
import weui from "weui.js";
import Vue from "vue";
import { DateTime } from "luxon";
import { getLoginState } from "../account";
import { tasks, frequencyOptionText, getTasks } from "../tasks";
import { getSetting, versionCompare, readableTime } from "../utils";
import { stateText, recommendServices } from "../variables";
Vue.directive("autoSave", {
bind(el, binding, vnode) {
function revertValue(el) {
let current = getSetting(el.name, null);
if (el.type == "checkbox") {
if (current == "checked") {
el.checked = true;
} else {
el.checked = false;
}
} else if (el.type == "select-one") {
el.value = current || el.options[0].value;
} else {
el.value = current;
}
}
function saveToLocalStorage(el, binding) {
if (el.type == "checkbox") {
if (el.checked) {
localStorage.setItem(el.name, "checked");
} else {
localStorage.removeItem(el.name);
}
} else {
localStorage.setItem(el.name, el.value);
}
weui.toast("设置已保存", 500);
}
revertValue(el);
el.addEventListener("change", function(event) {
if (binding.value && binding.value.notice && el.checked) {
weui.confirm(
binding.value.notice,
function() {
saveToLocalStorage(el, binding);
},
function() {
event.preventDefault();
setTimeout(() => {
revertValue(el);
}, 50);
},
{
title: "选项确认"
}
);
} else {
saveToLocalStorage(el, binding);
}
});
}
});
import loading from "./loading.vue";
import discounts from "./discounts.vue";
import guide from "./guide.vue";
import popup from "./popup.vue";
import links from "./links.vue";
export default {
name: "App",
components: { loading, discounts, guide, popup, links },
data() {
return {
taskList: [],
messages: [],
skuPriceList: {},
recommendedLinks: getSetting("recommendedLinks", []),
stateText: stateText,
newDiscounts: false,
showPopup: true,
frequencyOptionText: frequencyOptionText,
recommendServices: getSetting("recommendServices", recommendServices),
currentVersion: process.env.VERSION,
contentType: "messages",
newChangelog:
versionCompare(
getSetting("changelog_version", "2.0"),
process.env.VERSION
) < 0,
hiddenPromotionIds: getSetting("hiddenPromotionIds", []),
unreadCount: getSetting("unreadCount", null),
selectedTab: null,
scienceOnline: false,
newVersion: getSetting("newVersion", null),
loginStateDescription: "未能获取登录状态",
olduser: getSetting("oldUser", false),
showGuideAt: getSetting("showGuideAt", false),
loginState: {
default: true,
m: {
state: "unknown"
},
pc: {
state: "unknown"
}
},
discountTab: "featured"
};
},
computed: {
showGuide: function() {
if (!this.olduser && !this.showGuideAt) {
return true;
} else {
return false;
}
}
},
mounted: async function() {
// 准备数据
this.getTaskList();
// 渲染通知
setTimeout(() => {
this.renderMessages();
}, 50);
// 查询最新优惠
setTimeout(() => {
this.getLastDiscount();
}, 100);
// 测试是否科学上网
setTimeout(() => {
this.tryGoogle();
}, 200);
this.dealWithLoginState();
// 接收消息
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
switch (message.action) {
case "messages_updated":
this.renderMessages(message.messages);
break;
case "loginState_updated":
this.dealWithLoginState();
setTimeout(() => {
this.getTaskList();
}, 1000);
break;
default:
break;
}
});
},
methods: {
tryGoogle: async function() {
let response = await fetch(
"https://www.googleapis.com/discovery/v1/apis?name=abusiveexperiencereport"
);
if (response.status == "200") {
this.scienceOnline = true;
} else {
this.scienceOnline = false;
}
},
switchContentType: function(type) {
this.contentType = type;
switch (type) {
case "messages":
this.renderMessages();
this.readMessages();
break;
case "discounts":
this.readDiscounts();
break;
default:
break;
}
},
getLastDiscount: async function() {
let response = await fetch("https://teaclub.zaoshu.so/discount/last");
let lastDiscount = await response.json();
let readDiscountAt = localStorage.getItem("readDiscountAt");
if (
!readDiscountAt ||
new Date(lastDiscount.createdAt) > new Date(readDiscountAt)
) {
this.newDiscounts = true;
}
},
retryTask: function(task, hideNotice = false) {
chrome.runtime.sendMessage(
{
action: "runTask",
hideNotice: hideNotice,
taskId: task.id
},
function(response) {
if (!hideNotice) {
if (response.result == "success") {
weui.toast("手动运行成功", 3000);
} else if (response.message) {
weui.alert(response.message, { title: "任务暂未运行" });
}
}
}
);
},
// 任务列表
getTaskList: function() {
this.taskList = getTasks();
},
// 处理登录状态
dealWithLoginState: function() {
let loginState = getLoginState();
this.loginState = loginState;
if (loginState.class == "failed") {
weui.dialog({
title: "淘宝账号登录失效",
content: `<p>账号登录失效后,签到任务将无法运行</p>`,
className: "login-failed",
buttons: [
{
label: "去登录",
type: "primary",
onClick: function() {
chrome.runtime.sendMessage(
{
action: "openLogin"
},
function(response) {
console.log("Response: ", response);
}
);
}
},
{
label: "知道了",
type: "default"
}
]
});
}
function getStateDescription(loginState, type) {
return (
stateText[loginState[type].state] +
(loginState[type].message
? `( ${loginState[type].message} 上次检查: ${readableTime(
DateTime.fromISO(loginState[type].time)
)} )`
: "")
);
}
this.loginStateDescription =
"PC网页版登录" +
getStateDescription(loginState, "pc") +
",移动网页版登录" +
getStateDescription(loginState, "m");
},
renderMessages: function(messages) {
if (!messages) {
messages = getSetting("message", []);
chrome.runtime.sendMessage({ action: "getMessages" });
}
this.messages = messages.map(function(message) {
if (message.type == "coupon") {
message.coupon = message.content;
}
message.time = readableTime(
message.timestamp
? DateTime.fromMillis(message.timestamp)
: DateTime.fromISO(message.time)
);
return message;
});
},
showLoginState: function() {
$("#loginNotice").show();
},
selectType: function(type) {
this.selectedTab = type;
},
readMessages: function() {
this.unreadCount = 0;
chrome.runtime.sendMessage(
{
text: "clearUnread"
},
function(response) {
console.log("Response: ", response);
}
);
},
readDiscounts: function() {
this.newDiscounts = false;
},
showChangelog: function() {
this.newChangelog = false;
localStorage.setItem("changelog_version", this.currentVersion);
weui.dialog({
title: "更新记录",
content: `<iframe id="changelogIframe" frameborder="0" src="https://teaclub.zaoshu.so/changelog?buildId=${
process.env.BUILDID
}&browser=${
process.env.BROWSER
}&app=teaclub" style="width: 100%;min-height: 350px;"></iframe>`,
className: "changelog",
buttons: [
{
label: "完成",
type: "primary"
}
]
});
}
}
};
</script>
<style scoped>
.weui-cells {
margin-top: 0;
overflow: unset;
}
.main-container {
display: flex;
justify-content: space-between;
}
.messages, .discounts {
overflow: hidden;
height: 515px;
background: #f9f9f9;
}
.message-items {
margin-top: 10px;
height: 504px;
overflow-y: auto;
}
.order-good.suspended {
opacity: 0.5;
}
</style>
================================================
FILE: src/components/discounts.vue
================================================
<template>
<div id="discounts" class="contents-box discounts">
<div class="top-bar">
<div class="tabs">
<ul class="tab-list">
<li class="tabs-item">
<a
:class="discountTab == 'featured' ? 'tabs-link is-active' : 'tabs-link'"
@click="switchTab('featured')"
>精选</a>
</li>
<li class="tabs-item">
<a
:class="discountTab == 'concerned' ? 'tabs-link is-active' : 'tabs-link'"
@click="switchTab('concerned')"
>关注</a>
</li>
<li class="tabs-item">
<a
:class="discountTab == 'hot' ? 'tabs-link is-active' : 'tabs-link'"
@click="switchTab('hot')"
>热榜</a>
</li>
</ul>
</div>
<div class="select-tag" v-if="selectTag">
<div class="tag-box">
<span class="tag-name">{{selectTag.name}}</span>
</div>
<button
v-if="followed"
class="weui-btn weui-btn_mini weui-btn_disabled"
@click="unfollowTag(selectTag)"
>取消</button>
<button
v-else
class="weui-btn weui-btn_mini weui-btn_primary"
@click="followTag(selectTag)"
>关注</button>
</div>
<div class="search" v-else>
<input v-model="keyword" placeholder="输入关键词搜索" v-on:keyup.enter="search">
<i v-if="showClear" class="circle-close" @click="clear">×</i>
</div>
</div>
<div class="weui-cells discount-list" v-if="discountList">
<events :events="events" v-if="discountTab == 'featured' && events && events.length > 0"></events>
<div class="discounts-box" v-for="discount in discountList" :key="discount.id">
<div :class="discount.pinned ? 'discount pinned' : 'discount'">
<div class="title" @mouseover="discount.focus = true" @mouseout="discount.focus = false">
<span class="merchant" v-if="discount.merchant">
<img v-lazy="discount.merchant.icon" :alt="discount.merchant.name">
</span>
<a :href="`${discount.goodLink}`" target="_blank">{{discount.title}}</a>
<span class="discount_price">{{discount.price}}</span>
<report :discount="discount"></report>
</div>
<div class="description">
<a :href="`${discount.goodLink}`" target="_blank">
<img
v-if="discount.photo"
v-lazy="`${discount.photo}`"
@error.once="backup_picture($event)"
width="75"
class="discount-photo backup_picture"
:alt="discount.title"
>
</a>
<p>{{discount.description}}</p>
</div>
<div class="tags">
<span
class="tag"
v-for="tag in discount.tags"
:key="tag.id"
@click="filterByTag(tag)"
>{{tag.name}}</span>
</div>
<div class="weui-cell__ft">
<span class="time">{{discount.displayTime}}</span>
<a
v-if="discount.couponLink"
class="get-coupon"
data-microtip-position="top" role="tooltip"
:aria-label="discount.couponName"
:href="`${discount.couponLink}`"
target="_blank"
>优惠券</a>
<a class="go-buy" :href="`${discount.goodLink}`" target="_blank">去购买</a>
</div>
</div>
</div>
<infinite-loading v-if="discountList.length > 20" spinner="waveDots" @infinite="infiniteHandler">
<div slot="no-more" class="no-more">😭暂时没有近期优惠了</div>
<div slot="no-results" class="no-results">😭没有更多优惠信息了</div>
</infinite-loading>
<div v-if="discountTab == 'concerned' && followedTagIds.length < 1" class="no_message">
<h4>暂时还没有关注任何标签</h4>
<p class="tips">点击优惠信息中的标签可以筛选并关注标签哦</p>
</div>
<div v-if="keyword && discountList.length < 1" class="no_message">
<h4>没有找到任何优惠</h4>
<p class="tips">为了保证结果有效性,只展示近两周的优惠</p>
</div>
<div v-if="discountList.length > 0" class="self-recommendation">
<p class="tips">商家自荐/优惠爆料可联系微信:cindywchat</p>
</div>
</div>
<div class="loading" v-else>
<loading></loading>
</div>
</div>
</template>
<script>
import { DateTime } from "luxon";
import InfiniteLoading from 'vue-infinite-loading';
import { getSetting, readableTime } from "../utils";
import loading from "./loading.vue";
import report from "./report.vue";
import events from "./events.vue";
export default {
name: "discounts",
components: { loading, report, events, InfiniteLoading },
data() {
return {
followedTagIds: getSetting("followedTagIds", []),
discountTab: "featured",
discountList: null,
selectTag: null,
page: 1,
keyword: null
};
},
mounted: async function() {
this.getDiscounts();
},
computed: {
followed: function() {
return (
this.selectTag &&
this.followedTagIds.length > 0 &&
this.followedTagIds.indexOf(this.selectTag.id) > -1
);
},
showClear: function() {
return this.keyword && this.keyword.length > 0;
},
events: function() {
return this.discountList ? this.discountList.filter(discount => discount.event) : [];
},
condition: function() {
if (this.keyword) {
return {
keyword: this.keyword
}
}
switch (this.discountTab) {
case "featured":
return {
all: false
};
break;
case "concerned":
if (this.followedTagIds.length > 0) {
return {
tagIds: this.followedTagIds.join(",")
};
} else {
return {};
}
break;
case "hot":
return {
hot: true
};
break;
default:
break;
}
return {}
}
},
methods: {
backup_picture: function(e) {
e.currentTarget.src = "https://jjbcdn.zaoshu.so/web/img_error.png";
},
loadDiscountFormApi: async function(params) {
let queryParams = new URLSearchParams(params);
let response = await fetch(
`https://teaclub.zaoshu.so/discount?${queryParams.toString()}`
);
return await response.json();
},
getDiscounts: async function(condition) {
this.discountList = null;
this.selectTag = null;
this.page = 1
const discounts = await this.loadDiscountFormApi(condition)
this.discountList = discounts.map(function(discount) {
discount.displayTime = readableTime(
DateTime.fromISO(discount.createdAt)
);
discount.focus = false
return discount;
});
localStorage.setItem("readDiscountAt", new Date());
this.$forceUpdate();
},
infiniteHandler: async function ($state) {
if (this.selectTag) return $state.complete();
const discounts = await this.loadDiscountFormApi(Object.assign({}, this.condition, {
page: this.page,
}))
if (discounts.length) {
this.page += 1;
this.discountList.push(...discounts.map(function(discount) {
discount.displayTime = readableTime(
DateTime.fromISO(discount.createdAt)
);
discount.focus = false
return discount;
}));
$state.loaded();
} else {
$state.complete();
}
},
search: async function() {
this.getDiscounts(this.condition);
},
clear: async function() {
this.keyword = null;
this.switchTab("featured");
},
filterByTag: async function(tag) {
this.discountTab = null;
this.discountList = null;
this.page = 1
let response = await fetch(
`https://teaclub.zaoshu.so/discount/tag/${tag.id}`
);
let data = await response.json();
this.selectTag = data.tag;
this.discountList = data.discounts.map(function(discount) {
discount.displayTime = readableTime(
DateTime.fromISO(discount.createdAt)
);
discount.focus = false
return discount;
});
this.$forceUpdate();
},
unfollowTag: async function(tag) {
this.followedTagIds = this.followedTagIds.filter(
tagId => tagId != tag.id
);
localStorage.setItem(
"followedTagIds",
JSON.stringify(this.followedTagIds)
);
},
followTag: async function(tag) {
let followedTagIds = this.followedTagIds;
followedTagIds.push(tag.id);
localStorage.setItem(
"followedTagIds",
JSON.stringify(this.followedTagIds)
);
},
switchTab: async function(type) {
this.discountTab = type;
this.selectTag = null;
if (type == "concerned" && this.followedTagIds.length < 1) {
this.discountList = []
} else {
this.getDiscounts(this.condition);
}
}
}
};
</script>
<style scoped>
.tabs {
height: 40px;
width: 200px;
float: left;
}
.tab-list {
margin-bottom: 0;
box-shadow: none;
padding-top: 0.2em;
padding-left: 0.2em;
}
.tab-list li {
list-style-type: none;
}
.tab-list .tabs-item {
display: inline-block;
padding: 0 15px;
}
.tab-list .tabs-link {
position: relative;
display: inline-block;
padding: 12px 0;
font-size: 16px;
line-height: 22px;
text-align: center;
text-decoration: none;
cursor: pointer;
}
.tabs-link.is-active {
font-weight: 600;
color: #921714;
}
.tabs-link.is-active::after {
position: absolute;
right: 0;
bottom: -1px;
left: 0;
height: 3px;
background: #921714;
content: "";
}
.tags {
overflow: hidden;
text-overflow: ellipsis;
width: 55%;
height: 26px;
display: -webkit-box;
max-height: 26px;
-webkit-line-clamp: 1;
}
.tag {
font-size: 12px;
color: #696969;
cursor: pointer;
padding: 0.5em;
margin-right: 0.5em;
}
.top-bar {
height: 47px;
border-bottom: 1px solid #eeeeee3d;
z-index: 10;
display: flex;
justify-content: space-between;
}
.select-tag,
.search {
line-height: 50px;
font-size: 14px;
text-align: right;
position: relative;
}
.search input {
-webkit-appearance: none;
border-radius: 4px;
border: 1px solid #0000003d;
box-sizing: border-box;
color: #606266;
display: inline-block;
font-size: inherit;
height: 30px;
line-height: 30px;
outline: none;
padding: 0 15px;
transition: border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
margin-top: 12px;
margin-right: 6px;
background: #eeeeee42;
}
.search input:focus {
outline: none;
border-color: #409eff;
}
.search .circle-close {
position: absolute;
right: 12px;
color: #ccc;
padding: 0 2px;
cursor: pointer;
}
.tag-box {
width: 140px;
text-align: right;
float: left;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 1;
-moz-box-orient: vertical;
-webkit-box-orient: vertical;
}
.select-tag span.tag-name {
font-size: 14px;
padding: 6px 8px;
vertical-align: middle;
color: #737373;
height: 30px;
line-height: 30px;
margin-top: 11px;
}
.select-tag a.weui-btn {
vertical-align: middle;
margin-right: 1em;
padding-right: 16px;
}
.discount-list {
overflow-y: auto;
height: 465px;
margin-top: 0;
}
.discount-list li {
display: block;
}
.discount-list h5 {
padding: 0.2em 1em;
}
.discount {
border-bottom: 1px solid #eeeeee3d;
padding: 12px 8px;
position: relative;
}
.discount.pinned {
background-color: #fff7e0;
}
.discount .title {
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
max-height: 36px;
-webkit-line-clamp: 2;
-moz-box-orient: vertical;
-webkit-box-orient: vertical;
padding: 5px;
line-height: 22px;
font-size: 15px;
font-weight: 500;
}
.discount .description {
display: flex;
padding: 15px 5px;
}
.discount .discount-photo {
width: 75px;
height: 75px;
}
.discount_price {
color: #f04848;
font-weight: bold;
}
.discount .time {
font-size: 12px;
}
.discount .description p {
display: inline-block;
font-size: 13px;
color: #666;
width: 340px;
padding-left: 10px;
max-height: 75px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-moz-box-orient: vertical;
-webkit-box-orient: vertical;
line-height: 1.5;
}
.discount .merchant {
height: 15px;
line-height: 15px;
display: inline-block;
vertical-align: middle;
margin-top: -4px;
}
.discount .get-coupon {
font-size: 12px;
padding: 0.3em;
}
.discount .weui-cell__ft {
margin-top: -25px;
padding-bottom: 5px;
}
.discount a.go-buy:hover {
color: #fff;
background: #149813;
}
.weui-cells:after{
border-bottom: none
}
.no-more, .no-results{
color: #666;
padding: 3px;
font-size: 14px;
}
</style>
================================================
FILE: src/components/events.vue
================================================
<template>
<hooper>
<slide v-for="(event, index) in events" :key="event.id" :index="index">
<a :href="`${event.goodLink}`" target="_blank">
<img :src="event.banner || event.photo" :title="event.title" height="120"/>
<p class="title">{{event.title}}</p>
</a>
</slide>
<hooper-navigation slot="hooper-addons"></hooper-navigation>
</hooper>
</template>
<script>
import { Hooper, Slide, Navigation as HooperNavigation } from 'hooper';
import 'hooper/dist/hooper.css';
import weui from "weui.js";
export default {
name: "report",
props: ["events"],
components: {
Hooper,
Slide,
HooperNavigation
},
data() {
return {
show: false
};
},
methods: {}
};
</script>
<style scoped>
.hooper{
height: 120px;
}
.hooper li{
text-align: center;
}
.hooper p.title{
position: absolute;
bottom: 0px;
text-align: center;
width: 100%;
color: #fff;
background-color: #33333352;
font-size: 14px;
padding: 3px 6px;
overflow: hidden;
text-overflow: ellipsis;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
display: -webkit-box;
}
</style>
================================================
FILE: src/components/guide.vue
================================================
<template>
<div class="guide">
<div class="js_dialog" style="opacity: 1;" v-if="step > 0">
<div class="weui-mask"></div>
<div class="weui-dialog">
<div class="testbox step-1" v-if="step == 1">
<div class="weui-dialog__hd">
<strong class="weui-dialog__title">恭喜安装成功!</strong>
</div>
<div class="weui-dialog__bd">
<p>感谢你使用茶友会!以下是一些简单的介绍:</p>
<p>茶友会是一个<a href="https://github.com/sunoj/teaclub" target="_blank">公开源代码</a>的浏览器插件。它能自动搜索淘宝商品的渠道优惠券,还内置一系列替你进行签到、领飞猪里程的小任务。</p>
<p>由于淘宝网页经常更新,茶友会受其影响可能部分功能有时变得不可用,因此茶友会会经常更新以保持功能正常。如果你使用的不是
<a v-if="browser == 'chrome'" href="https://chrome.google.com/webstore/detail/igedhbjllcmgidlmhclmphmhlllkibkb" target="_blank">Chrome 拓展商店</a>
<a v-if="browser == 'firefox'" href="https://addons.mozilla.org/zh-CN/firefox/addon/teaclub/" target="_blank">Firefox 官方商店</a>
<a v-if="browser == 'edge'" href="https://microsoftedge.microsoft.com/addons/detail/aniokofeapdnfnihjgfeonlkmhfajobk" target="_blank">Microsoft Edge 扩展中心</a>
安装,强烈建议您使用上述渠道安装,只有这样你才能获得官方的自动更新。</p>
</div>
<div class="weui-dialog__ft">
<a class="weui-dialog__btn weui-dialog__btn_primary answer" @click="step = 2">继续</a>
</div>
</div>
<div class="testbox step-2" v-if="step == 2">
<div class="weui-dialog__hd">
<strong class="weui-dialog__title">登录账号</strong>
</div>
<div class="weui-dialog__bd">
<p>如你所知,茶友会是一个浏览器插件。在你的授权下,茶友会代替你自动访问淘宝的网页来执行一系列操作。 </p>
<p>很显然,
<b>茶友会需要您登录淘宝才能完成你指定的工作</b>。由于淘宝的登录有效期较短,你通常需要每天登录一次淘宝账号才能保证签到任务自动运行。
</p>
<p>当登录失效时,茶友会将会提醒你。除非你重新登录,否则茶友会将无法继续完成任何工作。</p>
</div>
<div class="weui-dialog__ft">
<a v-if="loginState.class == 'unknown'" :class="`weui-dialog__btn weui-dialog__btn_primary answer`" @click="done('openLogin')">现在登录</a>
<a class="weui-dialog__btn weui-dialog__btn_primary answer" @click="done">知道了</a>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { getSetting } from "../utils";
export default {
name: "guide",
props: ["loginState"],
data() {
return {
step: 1,
browser: process.env.BROWSER
};
},
methods: {
done: async function(action) {
this.step = 0
localStorage.setItem('showGuideAt', new Date())
if (action == 'openLogin') {
chrome.runtime.sendMessage({
action: "openLogin",
}, function(response) {
console.log("Response: ", response);
});
}
}
}
};
</script>
================================================
FILE: src/components/links.vue
================================================
<template>
<div class="links">
<span :class="action.class" v-for="(action, index) in actionLinks" :key="action.id" :index="index" :style="action.style">
<a
v-if="action.type == 'dialog'"
href="#"
data-microtip-position="top" role="tooltip"
:aria-label="action.description"
:style="action.linkStyle"
:class="action.linkClass"
@click="showDialog(action)"
>{{action.title}}
</a>
<a
v-if="action.type == 'link'"
data-microtip-position="top" role="tooltip"
:aria-label="action.description"
:href="action.url"
:style="action.linkStyle"
:class="action.linkClass"
target="_blank"
>{{action.title}}</a>
</span>
</div>
</template>
<script>
import weui from "weui.js";
import { getSetting } from "../utils";
export default {
name: "links",
data() {
return {
actionLinks: getSetting("teaclub:action-links", [
{
type: "dialog",
class: "el-tag el-tag--warning",
linkClass: "tippy",
title: "活动推荐",
description: "热门的促销活动推荐",
style: "margin-right: 3px;",
mode: "iframe",
url: "https://jjb.zaoshu.so/recommend"
},
{
type: "dialog",
"mode": "image",
class: "el-tag el-tag--danger",
linkClass: "tippy",
title: "支付宝红包",
description: "天天领支付宝红包",
url: "https://jjbcdn.zaoshu.so/chrome/alipayred.png"
}
])
};
},
methods: {
showDialog: async function(action) {
let content = ""
if (action.mode == "iframe") {
content = `
<iframe frameborder="0" src="${action.url}" style="width: 100%;min-height: 420px;min-width: 400px;"></iframe>
`
}
if (action.mode == "image") {
content = `
<img src="${action.url}" style="width: 270px;"></img>
`
}
weui.dialog({
title: action.title,
content: content,
className: "dialog",
buttons: [
{
label: "完成",
type: "primary"
}
]
});
},
}
};
</script>
<style scoped>
.links {
display: inline-block;
}
</style>
================================================
FILE: src/components/loading.vue
================================================
<template>
<div class="loading-masker">
<div class="white-widget grey-bg author-area" v-for="(masker, index) in numbers" :key="index">
<div class="auth-info row">
<div class="timeline-wrapper">
<div class="timeline-item">
<div class="animated-background">
<div class="background-masker header-top"></div>
<div class="background-masker header-left"></div>
<div class="background-masker header-right"></div>
<div class="background-masker header-bottom"></div>
<div class="background-masker subheader-left"></div>
<div class="background-masker subheader-right"></div>
<div class="background-masker subheader-bottom"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "loading",
data() {
return {
numbers: [1, 2, 3, 4, 5]
};
}
};
</script>
<style scoped>
.timeline-item {
background: #ffffff03;
border-bottom: 1px solid #f2f2f22e;
padding: 25px;
margin: 0 auto;
}
@keyframes placeHolderShimmer {
0% {
background-position: -468px 0;
}
100% {
background-position: 468px 0;
}
}
.animated-background {
animation-duration: 1s;
animation-fill-mode: forwards;
animation-iteration-count: infinite;
animation-name: placeHolderShimmer;
animation-timing-function: linear;
background: #f6f7f8;
background: linear-gradient(to right, #eeeeee42 8%, #dddddd61 18%, #eeeeee3d 33%);
background-size: 800px 104px;
height: 40px;
position: relative;
}
.background-masker {
background: #ffffff17;
position: absolute;
}
/* Every thing below this is just positioning */
.background-masker.header-top,
.background-masker.header-bottom,
.background-masker.subheader-bottom {
top: 0;
left: 40px;
right: 0;
height: 10px;
}
.background-masker.header-left,
.background-masker.subheader-left,
.background-masker.header-right,
.background-masker.subheader-right {
top: 10px;
left: 40px;
height: 8px;
width: 10px;
}
.background-masker.header-bottom {
top: 18px;
height: 6px;
}
.background-masker.subheader-left,
.background-masker.subheader-right {
top: 24px;
height: 6px;
}
.background-masker.header-right,
.background-masker.subheader-right {
width: auto;
left: 300px;
right: 0;
}
.background-masker.subheader-right {
left: 230px;
}
.background-masker.subheader-bottom {
top: 30px;
height: 10px;
}
.background-masker.content-top,
.background-masker.content-second-line,
.background-masker.content-third-line,
.background-masker.content-second-end,
.background-masker.content-third-end,
.background-masker.content-first-end {
top: 40px;
left: 0;
right: 0;
height: 6px;
}
.background-masker.content-top {
height: 20px;
}
.background-masker.content-first-end,
.background-masker.content-second-end,
.background-masker.content-third-end {
width: auto;
left: 380px;
right: 0;
top: 60px;
height: 8px;
}
.background-masker.content-second-line {
top: 68px;
}
.background-masker.content-second-end {
left: 420px;
top: 74px;
}
.background-masker.content-third-line {
top: 82px;
}
.background-masker.content-third-end {
left: 300px;
top: 88px;
}
</style>
================================================
FILE: src/components/popup.vue
================================================
<template>
<div v-if="events && events.length > 0 && loadEvents">
<div class="popup-show" v-if="showPopup">
<div class="js_dialog" style="opacity: 1;">
<div class="weui-mask"></div>
<hooper>
<slide class="slide" v-for="(event, index) in events" :key="event.id" :index="index">
<a :href="`${event.link}`" target="_blank">
<img :src="event.poster" :title="event.title" width="300"/>
</a>
</slide>
<hooper-navigation slot="hooper-addons"></hooper-navigation>
</hooper>
<div class="close-popup" @click="close">×</div>
</div>
</div>
<div class="preload">
<img :src="events[0].poster" width="300"/>
</div>
</div>
</template>
<script>
import { DateTime } from 'luxon'
import { getSetting, saveSetting } from "../utils";
import { Hooper, Slide, Navigation as HooperNavigation } from 'hooper';
import 'hooper/dist/hooper.css';
const today = DateTime.local().toFormat("o")
export default {
name: "popup",
components: {
Hooper,
Slide,
HooperNavigation
},
data() {
return {
showPopup: false,
loadEvents: false,
events: getSetting('events', []),
usage: getSetting(`temporary:usage-popup_d:${today}`, 0),
display: getSetting("displayPopup", {
"percentage": 0,
"limit": 0
}),
};
},
mounted: async function() {
setTimeout(() => {
this.preload()
}, 200);
setTimeout(() => {
this.show()
}, 400);
},
methods: {
close: async function() {
this.showPopup = false
this.$emit('close')
},
preload: function () {
let events = this.getEvents()
if (events && events.length > 0 && this.display.limit > 0) {
this.loadEvents = true
}
},
show: function () {
if (this.loadEvents && this.usage < this.display.limit && this.display.percentage > Math.floor(Math.random() * 100) + 1) {
if ($(".js_dialog:visible").length < 1) {
this.showPopup = true
saveSetting(`temporary:usage-popup_d:${today}`, this.usage + 1)
}
}
},
getEvents: function() {
let events = getSetting("events", []);
events = events.filter(event => {
const isValid = event.validUntil ? DateTime.fromJSDate(new Date(event.validUntil)) > DateTime.local() : true
const isStarted = event.startAt ? DateTime.fromJSDate(new Date(event.startAt)) < DateTime.local() : true
return isValid && isStarted
});
this.events = events
return events;
},
}
};
</script>
<style scoped>
.popup-show{
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
}
.preload{
display: none;
}
.popup-show .js_dialog{
height: 100%;
display: flex;
align-items: center;
flex-direction: column;
justify-content: center;
}
.hooper{
max-height: 80%;
min-height: 450px;
z-index: 1001;
width: 400px;
}
.hooper .slide {
text-align: center;
}
.close-popup{
border: 2px solid #cacaca;
border-radius: 20px;
width: 30px;
height: 30px;
z-index: 1002;
text-align: center;
font-size: 29px;
line-height: 26px;
color: #e8e8e8;
font-weight: 100;
margin-top: -20px;
cursor: pointer;
}
</style>
================================================
FILE: src/components/report.vue
================================================
<template>
<div class="reprot">
<div class="report-mask" @click="hide" v-if="show"></div>
<div class="report-problem" :ref="`report-${discount.id}`">
<div class="report-icon" data-microtip-position="bottom-right" role="tooltip" aria-label="反馈问题" @click="showList">
<span>i</span>
</div>
<div :class="`weui-cells weui-cells_radio ${leftList ? 'turn-left': ''}`" v-if="show">
<label class="weui-cell weui-check__label" :for="`report-${discount.id}_expired`">
<div class="weui-cell__bd">
<p>优惠失效</p>
</div>
<div class="weui-cell__ft">
<input
type="radio"
v-model="code"
value="expired"
class="weui-check"
:name="`report-${discount.id}`"
:id="`report-${discount.id}_expired`"
>
<span class="weui-icon-checked"></span>
</div>
</label>
<label class="weui-cell weui-check__label" :for="`report-${discount.id}_soldout`">
<div class="weui-cell__bd">
<p>告罄缺货</p>
</div>
<div class="weui-cell__ft">
<input
type="radio"
v-model="code"
value="soldout"
:name="`report-${discount.id}`"
class="weui-check"
:id="`report-${discount.id}_soldout`"
>
<span class="weui-icon-checked"></span>
</div>
</label>
<label class="weui-cell weui-check__label" :for="`report-${discount.id}_wronglink`">
<div class="weui-cell__bd">
<p>链接错误</p>
</div>
<div class="weui-cell__ft">
<input
type="radio"
v-model="code"
:name="`report-${discount.id}`"
value="wronglink"
class="weui-check"
:id="`report-${discount.id}_wronglink`"
>
<span class="weui-icon-checked"></span>
</div>
</label>
<button class="report-btn" @click="sendReport" v-show="code">{{ loading ? `发送中..`: `发送反馈`}}</button>
</div>
</div>
</div>
</template>
<script>
import weui from "weui.js";
export default {
name: "report",
props: ["discount"],
data() {
return {
code: null,
show: false,
leftList: false
};
},
methods: {
showList: async function() {
this.show = !this.show;
if (this.$refs[`report-${this.discount.id}`].offsetLeft > 300) {
this.leftList = true;
}
},
hide: async function() {
this.show = false
},
sendReport: async function() {
this.loading = true
try {
let response = await fetch(
`https://jjb.zaoshu.so/discount/${this.discount.id}`,
{
body: JSON.stringify({
code: this.code
}),
cache: 'no-cache',
headers: {
"content-type": "application/json"
},
method: "POST",
mode: "cors",
redirect: "follow"
}
);
let result = await response.json();
} catch (error) {
console.error(error)
}
this.loading = false
this.show = false
weui.toast("感谢反馈", 500);
}
}
};
</script>
<style scoped>
.reprot{
display: inline;
}
.report-icon {
display: none
}
.discounts-box:hover .report-icon {
display: inline-block
}
.discounts-box:hover .report-problem{
display: inline;
}
.report-mask {
width: 100%;
height: 100%;
display: block;
position: absolute;
top: 0;
left: 0;
}
.report-problem {
z-index: 5;
position: absolute;
display: none;
margin-left: 5px;
}
.report-problem .weui-cells {
width: 160px;
margin-top: 5px;
border: 1px solid #e0e0e0;
border-top: 0;
border-bottom: 0;
font-size: 14px;
font-weight: normal;
}
.report-problem .weui-cells.turn-left {
margin-left: -120px;
}
.report-btn {
width: 100%;
background: #fdf295;
height: 32px;
border: 0;
cursor: pointer;
}
.report-icon span {
border: 1px solid #ccc;
width: 16px;
height: 16px;
border-radius: 10px;
display: block;
text-align: center;
font-size: 14px;
color: #3a3a3a;
margin-top: 3px;
line-height: 17px;
cursor: pointer;
background: #eee;
font-family: monospace;
}
</style>
================================================
FILE: src/content_script.js
================================================
import 'weui';
import weui from 'weui.js';
import QRCode from "qrcode-svg";
import '../static/style/style.css'
var observeDOM = (function () {
var MutationObserver = window.MutationObserver || window.WebKitMutationObserver
return function (obj, callback) {
// define a new observer
var obs = new MutationObserver(function (mutations, observer) {
if (mutations[0].addedNodes.length || mutations[0].removedNodes.length) {
callback(observer);
}
});
// have the observer observe foo for changes in children
obs.observe(obj, { childList: true, subtree: true });
};
})();
Object.defineProperty(Array.prototype, 'chunk', {
value: function (chunkSize) {
var array = this;
return [].concat.apply([],
array.map(function (elem, i) {
return i % chunkSize ? [] : [array.slice(i, i + chunkSize)];
})
);
}
});
function mockTap(element) {
let rect = element.getBoundingClientRect()
sendTouchEvent(rect.x + 3, rect.y + 3, element, 'touchstart');
sendTouchEvent(rect.x + 3, rect.y + 3, element, 'touchend');
}
// 模拟点击 (原生)
function simulateClick(domNode, mouseEvent) {
if (mouseEvent && domNode) {
return mockClick(domNode)
}
try {
mockTap(domNode)
mockClick(domNode)
} catch (error) {
console.log('fullback to mockClick', error)
mockClick(domNode)
}
}
function mockClick(element) {
var dispatchMouseEvent = function (target, var_args) {
var e = document.createEvent("MouseEvents");
e.initEvent.apply(e, Array.prototype.slice.call(arguments, 1));
target.dispatchEvent(e);
};
if (element) {
dispatchMouseEvent(element, 'mouseover', true, true);
dispatchMouseEvent(element, 'mousedown', true, true);
dispatchMouseEvent(element, 'click', true, true);
dispatchMouseEvent(element, 'mouseup', true, true);
}
}
/* eventType is 'touchstart', 'touchmove', 'touchend'... */
function sendTouchEvent(x, y, element, eventType) {
if ('TouchEvent' in window && TouchEvent.length > 0) {
const touchObj = new Touch({
identifier: Date.now(),
target: element,
clientX: x,
clientY: y,
radiusX: 2.5,
radiusY: 2.5,
rotationAngle: 10,
force: 0.5,
});
const touchEvent = new TouchEvent(eventType, {
cancelable: true,
bubbles: true,
touches: [touchObj],
targetTouches: [],
changedTouches: [touchObj],
shiftKey: true,
});
element.dispatchEvent(touchEvent);
} else {
console.log('no TouchEvent')
}
}
function injectScript(file, node) {
var th = document.getElementsByTagName(node)[0];
var s = document.createElement('script');
s.setAttribute('type', 'text/javascript');
s.setAttribute('charset', "UTF-8");
s.setAttribute('src', file);
th.appendChild(s);
}
function injectScriptCode(code, node = 'body') {
var th = document.getElementsByTagName(node)[0];
var script = document.createElement('script');
script.setAttribute('type', 'text/javascript');
script.setAttribute('language', 'JavaScript');
script.textContent = code;
th.appendChild(script);
}
injectScriptCode(`
if (typeof hrl != 'undefined' && typeof host != 'undefined') {
document.write('<a style="display:none" href="' + hrl + '" id="exe"></a>');
document.getElementById('exe').click()
}
`, 'body')
function escapeSpecialChars(jsonString) {
return jsonString.replace(/\\n/g, "\\n").replace(/\\'/g, "\\'").replace(/\\"/g, '\\"').replace(/\\&/g, "\\&").replace(/\\r/g, "\\r").replace(/\\t/g, "\\t").replace(/\\b/g, "\\b").replace(/\\f/g, "\\f");
}
var pageTaskRunning = false
// 获取设置
function getSetting(name, cb) {
chrome.runtime.sendMessage({
text: "getSetting",
content: name
}, function (response) {
cb(response)
console.log("getSetting Response: ", name, response);
});
}
function createElementFromHTML(htmlString) {
var div = document.createElement('div');
div.innerHTML = htmlString.trim();
return div.firstChild;
}
function addDiscountElement() {
var newDiv = createElementFromHTML(`
<div id="teaclub">
<div class="loading">
<img src="https://jjbcdn.zaoshu.so/teaclub/chicken-serching.gif">
茶友会正在查询优惠券..
</div>
<div id="No-Result" style="display: none;">
<div class="coupon-not-found">
<img src="https://jjbcdn.zaoshu.so/teaclub/coupon-not-found.jpg"/>遗憾,此商品没找到渠道优惠券
</div>
</div>
<div id="Coupon-box" style="display: none;">
<dl class="prop clear">
<dt class="metatit">优惠券</dt>
<dd id="Coupon-list">
</dd>
</dl>
</div>
<div id="PDD-box" style="display: none;">
<dl class="prop clear">
<dt class="metatit">拼多多同款</dt>
<dd id="PDD-goods">
</dd>
</dl>
</div>
<div id="specialEvent-box" style="display: none;">
<dl class="prop clear">
<dt class="metatit">活动推荐</dt>
<dd id="specialEvent-container">
</dd>
</dl>
</div>
<div class="information-from">🍵茶友会提供</div>
</div>
`);
if (document.getElementById("J_isku")) {
document.getElementsByClassName("tb-wrap")[0].insertBefore(newDiv, document.getElementById("J_SepLine"));
} else {
document.getElementsByClassName("tb-wrap")[0].insertBefore(newDiv, document.getElementsByClassName("tm-ser")[0]);
}
}
function addCouponElement(coupon) {
let displayCouponName = coupon.name
const couponNameParsingResults = /满([0-9]*)(.[0-9]{2})?元减([0-9]*)(.[0-9]{2})?元/.exec(coupon.name)
if (couponNameParsingResults && couponNameParsingResults[2] == "00") {
displayCouponName = `满${couponNameParsingResults[1]}元减${couponNameParsingResults[3]}元`
}
const couponQrcode = new QRCode({
width: 70,
height: 70,
background: "#de1d3d",
content: coupon.shortUrl
}).svg();
var newDiv = createElementFromHTML(`
<a class="teaclub-coupon" href="${coupon.url}" target="_blank">
<div class="coupon-bonus-item">
<div class="coupon-item-left">
<div class="qrcode">${couponQrcode}</div>
<p class="coupon-item-rmb">
<span class="rmb">${displayCouponName}</span>
</p>
<p class="coupon-item-surplus">剩余:${coupon.remainCount}</p>
</div>
<div class="coupon-item-right">
<p>有效期</p>
<p>${coupon.startTime}</p>
<p>- ${coupon.endTime}</p>
</div>
</div>
</a>
`);
var currentDiv = document.getElementById("Coupon-list");
currentDiv.appendChild(newDiv);
}
function buildGoodsBatch(goodsBatch) {
injectScriptCode(
`
var slideIndex = 1;
// Next/previous controls
function plusSlides(n) {
showSlides(slideIndex += n);
}
// Thumbnail image controls
function currentSlide(n) {
showSlides(slideIndex = n);
}
function showSlides(n) {
var i;
var slides = document.getElementsByClassName("teaClubSlide");
var dots = document.getElementsByClassName("dot");
if (n > slides.length) {slideIndex = 1}
if (n < 1) {slideIndex = slides.length}
for (i = 0; i < slides.length; i++) {
slides[i].style.display = "none";
}
for (i = 0; i < dots.length; i++) {
dots[i].className = dots[i].className.replace(" active", "");
}
if (slides[slideIndex-1]) {
slides[slideIndex-1].style.display = "block";
dots[slideIndex-1].className += " active";
}
}
`, 'body')
const goodsBatchDom = goodsBatch.map((goods, index) => {
return `<div class="teaClubSlide fade">
<div class="number-text">${index} / ${goodsBatch.length}</div>
<div class="goodCard-list">
${
goods.map((good) => {
return buildGoodCard(good)
}).join('')
}
</div>
</div>`
}).join('')
const batchDots = goodsBatch.map((goods, index) => {
return `<span class="dot" onclick="currentSlide(${index + 1})"></span>`
}).join('')
let goodsElement = ''
if (goodsBatch.length > 1) {
goodsElement = createElementFromHTML(`<div id="teaclub-slides">
<div class="slideshow-container">
${goodsBatchDom}
<a class="prev" onclick="plusSlides(-1)">❮</a>
<a class="next" onclick="plusSlides(1)">❯</a>
</div>
<br>
<div style="text-align:center">
${batchDots}
</div>
</div>`)
} else {
goodsElement = createElementFromHTML(goodsBatchDom)
}
var currentDiv = document.getElementById("PDD-goods");
currentDiv.appendChild(goodsElement);
}
function buildGoodCard(good) {
const goodQrcode = new QRCode({
width: 100,
height: 100,
content: good.url
}).svg();
return `<div>
<a class="PDD-card" href="${good.url}" target="_blank">
<div class="PDD-cardContainer">
<div class="PDD-qrcode">${goodQrcode}</div>
<div class="PDD-imageContainer PDD-imageContainer--square">
<img class="PDD-image" src="${good.thumbnail}" alt=""/>
</div>
<div class="PDD-info">
<div class="PDD-title">
<div class="PDD-titleText">${good.name}</div>
<div class="PDD-tagList PDD-tagList--title">
<div class="PDD-tag PDD-tag--source PDD-tag--jingdong PDD-tag--plain">销量:${good.sales}</div>
</div>
</div>
<div class="PDD-tool">
<div class="PDD-toolLeft">
<div class="PDD-price">¥${good.price}</div>
</div>
<div class="PDD-button PDD-button--plain PDD-button--orange">
去购买
<svg
class="Zi Zi--ArrowRight" fill="currentColor" viewBox="0 0 24 24" width="16" height="16">
<path
d="M9.218 16.78a.737.737 0 0 0 1.052 0l4.512-4.249a.758.758 0 0 0 0-1.063L10.27 7.22a.737.737 0 0 0-1.052 0 .759.759 0 0 0-.001 1.063L13 12l-3.782 3.716a.758.758 0 0 0 0 1.063z"
fill-rule="evenodd"></path>
</svg>
</div>
</div>
</div>
</div>
</a>
</div>`
}
async function findCoupon(disable_find_coupon) {
if (disable_find_coupon) return
addDiscountElement()
const urlParams = new URLSearchParams(window.location.search);
const sku = urlParams.get('id') || urlParams.get('skuId')
const title = document.title.split('-')[0]
const merchant = window.location.host.indexOf('item.taobao.com') > -1 ? 'taobao' : 'tmall'
chrome.runtime.sendMessage({
action: "queryCoupon",
params: {
merchant,
sku,
title
}
})
}
function markCheckinStatus(task, data, cb) {
chrome.runtime.sendMessage({
action: "markCheckinStatus",
taskId: task.id,
status: "signed",
...data
}, function (response) {
console.log('markCheckinStatus response', response)
if (cb && response) { cb() }
});
}
// *********
// 签到任务
// *********
// 飞猪里程
function markFliggyCheckin(task, orderId) {
const signRes = document.getElementsByClassName("tlc-title")[0] && document.getElementsByClassName("tlc-title")[0].innerText
const value = (document.getElementsByClassName("tlc-title")[0] && document.getElementsByClassName("tlc-title")[0].getElementsByTagName("span")[0]) ? document.getElementsByClassName("tlc-title")[0].getElementsByTagName("span")[0].innerText : null
console.log('markFliggyCheckin', task, orderId, signRes, value)
if (signRes && (signRes.indexOf("获得") > -1)) {
return markCheckinStatus(task, {
value: value + '里程',
orderId: orderId
}, () => {
chrome.runtime.sendMessage({
action: "checkin_notice",
value: value,
reward: 'mileage',
title: orderId ? "茶友会自动为您签订单奖励里程" : "茶友会自动为您签到领里程",
content: "恭喜您获得了" + value + '个里程奖励'
}, function (response) {
console.log("Response: ", response);
})
})
} else if (signRes && (signRes.indexOf("今日已领") > -1)) {
markCheckinStatus(task, {
value
})
} else if (signRes && (signRes.indexOf("已领取过") > -1) && orderId) {
markCheckinStatus(task, {
value,
orderId: orderId
})
} else if (signRes && (signRes.indexOf("本月您已领满") > -1)) {
markCheckinStatus(task, {
value,
month: new Date().getMonth(),
})
}
}
function fliggyCheckin(setting) {
if (setting != 'never') {
weui.toast('茶友会运行中', 1000);
chrome.runtime.sendMessage({
action: "updateRunStatus",
taskId: 2
})
let signInButton = document.getElementsByClassName("J_mySignInBtn")[0]
if (signInButton && signInButton.innerText == "已签到") {
markCheckinStatus({
key: 'fliggy-mytrip',
id: 2
})
} else if (signInButton && signInButton.innerText && signInButton.innerText.indexOf("签到") > -1) {
simulateClick(signInButton)
// 监控结果
observeDOM(document.body, function () {
markFliggyCheckin({
key: 'fliggy-mytrip',
id: 2
})
})
}
}
}
function fliggyCheckin2(setting) {
if (setting != 'never') {
weui.toast('茶友会运行中', 1000);
chrome.runtime.sendMessage({
action: "updateRunStatus",
taskId: 3
})
let signInButton = document.getElementsByClassName("J_makesurebuttontvipBtn")[0]
if (signInButton && signInButton.innerText && signInButton.innerText == "确 认") {
simulateClick(signInButton)
// 监控结果
observeDOM(document.body, function () {
markFliggyCheckin({
key: 'fliggy-tvip',
id: 3
})
})
} else {
if (signInButton && signInButton.innerText == "已签到") {
markCheckinStatus({
key: 'fliggy-tvip',
id: 3
})
}
}
}
}
function fliggyCheckin3(setting) {
if (setting != 'never') {
weui.toast('茶友会运行中', 1000);
chrome.runtime.sendMessage({
action: "updateRunStatus",
taskId: 4
})
let signInButton = null
let signInReward = null
let spanElements = document.getElementsByTagName("span")
Array.prototype.slice.call(spanElements).forEach(function (element) {
if (element.innerText && /^签到\+[0-9]+里程/.test(element.innerText)) {
signInButton = element
}
if (element.innerText && /^明日\+[0-9]+里程/.test(element.innerText)) {
signInReward = element
}
});
console.log('signInButton', signInButton)
if (signInButton) {
setTimeout(() => {
simulateClick(signInButton, true)
}, 500);
// 监控结果
observeDOM(document.body, function () {
markFliggyCheckin({
key: 'rx-member',
id: 4
})
})
} else {
if (signInReward && signInReward.innerText) {
markCheckinStatus({
key: 'rx-member',
id: 4
})
}
}
}
}
function fliggyCheckin6(setting) {
if (setting != 'never') {
weui.toast('茶友会运行中', 1000);
chrome.runtime.sendMessage({
action: "updateRunStatus",
taskId: 6
})
const urlParams = new URLSearchParams(window.location.search);
let orderId = urlParams.get('orderId')
let signInButton = document.getElementsByClassName("J_makesurebuttontvip")[0]
if (signInButton && signInButton.innerText && signInButton.innerText == "确 认") {
simulateClick(signInButton)
// 监控结果
observeDOM(document.body, function () {
markFliggyCheckin({
key: 'order-fliggy',
id: 6
}, orderId)
})
}
}
}
function fliggyCheckin7(setting) {
if (setting != 'never') {
weui.toast('茶友会运行中', 1000);
chrome.runtime.sendMessage({
action: "updateRunStatus",
taskId: 7
})
let signInButton = document.getElementsByClassName("check-btn")[0]
if (signInButton && signInButton.innerText && signInButton.innerText == "立即签到") {
simulateClick(signInButton)
// 监控结果
observeDOM(document.body, function () {
markFliggyCheckin({
key: 'welfare-center',
id: 7
})
})
} else {
if (signInButton && signInButton.innerText == "上飞猪App领更多") {
markCheckinStatus({
key: 'welfare-center',
id: 7
})
}
}
}
}
function accountAlive(type, message) {
chrome.runtime.sendMessage({
action: "saveLoginState",
state: "alive",
message: message,
type: type
}, function (response) {
console.log("accountAlive ", type, message, response);
});
}
if (document.getElementById("login-info")) {
observeDOM(document.getElementById("login-info"), function () {
if (document.getElementsByClassName("j_Username")[0] && document.getElementsByClassName("j_Username")[0].innerText) {
accountAlive('pc', 'PC网页检测到用户名')
}
});
}
// 主任务
function CheckDom() {
if (window.location.host.indexOf("m.taobao.com") > -1 && window.location.host.indexOf("item.taobao.com") < 0) {
if (window.location.host != "market.m.taobao.com") {
injectScript(chrome.extension.getURL('/static/touch-emulator.js'), 'body');
injectScriptCode(`
setTimeout(function () {
TouchEmulator();
}, 200)
`, 'body')
}
}
// 判断登录状态
setTimeout(() => {
checkLoginState()
}, 1000)
setTimeout(() => {
if (window.location.host == 'login.taobao.com') {
chrome.runtime.sendMessage({
action: "saveLoginState",
state: "failed",
message: "PC网页需要登录",
type: "pc"
}, function (response) {
console.log("Response: ", response);
});
}
if (window.location.host == 'login.m.taobao.com') {
chrome.runtime.sendMessage({
action: "saveLoginState",
state: "failed",
message: "移动网页需要登录",
type: "m"
}, function (response) {
console.log("Response: ", response);
});
}
}, 8000);
// 订单
if (document.title == "已买到的宝贝" && window.location.host == 'buyertrade.taobao.com') {
let orderElements = document.getElementsByClassName("bought-wrapper-mod__head-info-cell___29cDO")
let time = 0
// 只处理最近五个订单
if (orderElements && orderElements.length > 5) {
orderElements = Array.prototype.slice.call(orderElements).slice(0, 5);
}
if (orderElements) {
accountAlive('pc', 'PC网页检测订单')
}
Array.prototype.slice.call(orderElements).forEach(function (orderElement) {
if (orderElement.lastElementChild && orderElement.lastElementChild.lastElementChild) {
let orderId = orderElement.lastElementChild.lastElementChild.innerText
if (orderId) {
setTimeout(function () {
chrome.runtime.sendMessage({
action: "getOrderFliggy",
orderId: orderId
}, function (response) {
console.log("Response: ", response);
});
}, time)
time += 15000;
}
}
});
}
// 商品页
if (window.location.host.indexOf('item.taobao.com') > -1 || window.location.host.indexOf('detail.tmall.com') > -1) {
setTimeout(() => {
getSetting('disable_find_coupon', (setting) => {
findCoupon(setting)
})
}, 50);
}
// 飞猪签到
if (document.title == "我的旅行" && window.location.host == 'www.fliggy.com') {
setTimeout(() => {
if (document.getElementsByClassName("J_mySignInBtn")[0]) {
getSetting('task-2_frequency', fliggyCheckin)
} else if (document.getElementsByClassName("J_makesurebuttontvipBtn")[0]) {
getSetting('task-3_frequency', fliggyCheckin2)
} else if (document.getElementsByClassName("J_makesurebuttontvip")[0]) {
getSetting('task-6_frequency', fliggyCheckin6)
}
}, 3000);
};
if (document.title == "会员中心" && window.location.host == 'h5.m.taobao.com') {
getSetting('task-4_frequency', fliggyCheckin3)
}
if (document.title == "里程福利中心" && window.location.host == 'h5.m.taobao.com') {
getSetting('task-7_frequency', fliggyCheckin7)
}
}
// 检查登录状态
function checkLoginState() {
// PC 是否登录
if (document.getElementById("mtb-nickname") && document.getElementById("mtb-nickname").value || document.getElementsByClassName("J_MemberNick")[0]) {
accountAlive('pc', 'PC网页检测到用户名')
}
if (document.getElementById("J_SiteNavLogin")) {
if (document.getElementById("J_SiteNavLogin").querySelector(".site-nav-login-info-nick") && document.getElementById("J_SiteNavLogin").querySelector(".site-nav-login-info-nick").text) {
accountAlive('pc', 'PC网页检测到用户头像')
}
}
// M 是否登录
if (document.getElementsByClassName("tb-toolbar-container")[0] || window.location.href == "https://h5.m.taobao.com/mlapp/mytaobao.html") {
accountAlive('m', '移动端打开我的淘宝')
}
if (window.location.href == "https://main.m.taobao.com/mytaobao/index.html") {
if (document.getElementsByClassName(".main-layout")[0].querySelector(".tpl-wrapper")) {
accountAlive('m', '移动端打开我的淘宝')
}
}
}
$(document).ready(function () {
console.log('茶友会注入页面成功');
checkLoginState()
if (!pageTaskRunning) {
setTimeout(function () {
console.log('茶友会开始执行任务');
CheckDom()
}, 2500)
}
});
function dealWithSearchRes(content) {
if (content.coupon) {
setTimeout(() => {
document.getElementById("Coupon-box").style.display = 'block';
addCouponElement(content.coupon)
document.getElementById("teaclub").getElementsByClassName("loading")[0].style.display = 'none';
}, 500);
} else {
document.getElementById("Coupon-box").style.display = 'none';
}
if (content.specialEvent && content.specialEvent.html) {
document.getElementById("specialEvent-box").style.display = 'block';
const specialEventElement = createElementFromHTML(content.specialEvent.html)
const containerDiv = document.getElementById("specialEvent-container");
containerDiv.appendChild(specialEventElement);
}
if (content.similarGoods && content.similarGoods.length > 0) {
setTimeout(() => {
document.getElementById("teaclub").getElementsByClassName("loading")[0].style.display = 'none';
document.getElementById("PDD-box").style.display = 'block';
buildGoodsBatch(content.similarGoods.chunk(3))
}, 500);
setTimeout(() => {
injectScriptCode(`
showSlides(1);
`, 'body')
}, 520);
} else {
document.getElementById("PDD-box").style.display = 'none';
}
if (!content.coupon && (!content.similarGoods || content.similarGoods.length < 1)) {
setTimeout(() => {
document.getElementById("teaclub").getElementsByClassName("loading")[0].style.display = 'none';
document.getElementById("No-Result").style.display = 'block';
}, 1500);
}
}
// 应用消息
chrome.runtime.onMessage.addListener(function (message, sender, sendResponse) {
console.log('onMessage', message)
switch (message.type) {
case 'couponInfo':
dealWithSearchRes(message.content)
break;
default:
break;
}
})
// 消息
var passiveSupported = false;
try {
var options = Object.defineProperty({}, "passive", {
get: function () {
passiveSupported = true;
}
});
window.addEventListener("test", null, options);
} catch (err) { }
window.addEventListener("message", function (event) {
if (event.data && event.data.action == 'productPrice') {
findOrderBySkuAndApply(event.data, event.data.setting)
}
},
passiveSupported ? { passive: true } : false
);
var nodeList = document.querySelectorAll('script');
for (var i = 0; i < nodeList.length; ++i) {
var node = nodeList[i];
node.src = node.src.replace("http://", "https://")
}
================================================
FILE: src/popup.js
================================================
import * as _ from "lodash"
$ = window.$ = window.jQuery = require('jquery')
import 'weui'
import weui from 'weui.js'
import Vue from 'vue'
import microtip from 'microtip/microtip.css'
import '../static/style/popup.css'
$.each(['show', 'hide'], function (i, ev) {
var el = $.fn[ev];
$.fn[ev] = function () {
this.trigger(ev);
return el.apply(this, arguments);
};
});
import App from './components/app.vue';
import VueLazyload from 'vue-lazyload'
Vue.use(VueLazyload)
new Vue({
el: '#app',
render: h => h(App)
})
// 消息已读
function readMessage() {
chrome.runtime.sendMessage({
text: "clearUnread"
}, function (response) {
console.log("Response: ", response);
});
}
$( document ).ready(function() {
// 标记已读
readMessage()
// 查询最新版本
$.getJSON(`https://teaclub.zaoshu.so/updates?buildid=${process.env.BUILDID}&browser=${process.env.BROWSER}&app=teaclub`, function (lastVersion) {
if (!lastVersion) return localStorage.removeItem('newVersion')
let skipBuildId = localStorage.getItem('skipBuildId')
let localBuildId = skipBuildId || process.env.BUILDID
// 如果有新版
if (localBuildId < lastVersion.buildId) {
localStorage.setItem('newVersion', lastVersion.versionCode)
// 如果新版是主要版本,而且当前版本需要被提示
if (lastVersion.major && localBuildId < lastVersion.noticeBuildId) {
let noticeDialog = weui.dialog({
title: `${lastVersion.title} <span class="dismiss">×</span>` || '有版本更新',
content: `${lastVersion.changelog}
<div class="changelog">
<span class="time">${lastVersion.time}</span>` +
(lastVersion.blogUrl ? `<a class="blog" href="${lastVersion.blogUrl}" target="_blank">了解更多</a>` : '') +
`</div>`,
className: 'update',
buttons: [{
label: '不再提醒',
type: 'default',
onClick: function () {
localStorage.setItem('skipBuildId', lastVersion.buildId)
}
}, {
label: '下载更新',
type: 'primary',
onClick: function () {
chrome.tabs.create({
url: lastVersion.downloadUrl || `https://teaclub.zaoshu.so/updates/latest?browser=${process.env.BROWSER}&app=teaclub`
})
}
}]
});
$(".update .dismiss").on("click", function () {
noticeDialog.hide()
})
}
} else {
localStorage.removeItem('newVersion')
}
});
$('.settings .weui-navbar__item').on('click', function () {
$(this).addClass('weui-bar__item_on').siblings('.weui-bar__item_on').removeClass('weui-bar__item_on');
var type = $(this).data('type')
$('.settings_box').hide()
$('.settings_box.' + type).show()
});
$(document).on("click", ".openMobilePage", function () {
chrome.runtime.sendMessage({
action: "openUrlAsMobile",
url: $(this).data('url')
}, function (response) {
console.log("Response: ", response);
});
})
$(".weui-dialog__ft a").on("click", function () {
$("#dialogs").hide()
$("#listenAudio").hide()
$("#changeLogs").hide()
})
$("#dialogs .js-close").on("click", function () {
$("#dialogs").hide()
})
})
// 防止缩放
chrome.tabs.getZoomSettings(function (zoomSettings) {
if (zoomSettings.defaultZoomFactor > 1 && zoomSettings.scope == 'per-origin' && zoomSettings.mode == 'automatic') {
let zoomPercent = (100 / (zoomSettings.defaultZoomFactor * 100)) * 100;
document.body.style.zoom = zoomPercent + '%'
}
})
================================================
FILE: src/start.js
================================================
import 'weui'
import '../static/style/start.css'
================================================
FILE: src/tasks.js
================================================
import {DateTime} from 'luxon'
import { getLoginState } from './account'
import { getSetting, readableTime } from './utils'
const frequencyOptionText = {
'2h': "每2小时",
'5h': "每5小时",
'daily': "每天",
'never': "从不"
}
const mapFrequency = {
'2h': 2 * 60,
'5h': 5 * 60,
'daily': 24 * 60,
'never': 99999
}
const tasks = [
{
id: '6',
src: {
pc: 'https://buyertrade.taobao.com/trade/itemlist/list_bought_items.htm',
},
title: '订单里程',
description: "每个淘宝订单可以领取3个飞猪里程(每月可领5次)",
mode: 'iframe',
key: "order-fliggy",
type: ['pc'],
checkin: true,
frequencyOption: ['daily', 'never'],
frequency: 'daily',
rateLimit:{
daily: 5,
hour: 2
}
},
{
id: '2',
src: {
pc: 'https://www.fliggy.com/mytrip/',
},
baseUrl: "https://www.fliggy.com/mytrip/",
title: '飞猪里程1',
description: "每日签到领取飞猪里程",
mode: 'iframe',
key: "fliggy-mytrip",
type: ['pc'],
checkin: true,
frequencyOption: ['daily', 'never'],
frequency: 'daily',
rateLimit:{
daily: 5,
hour: 2
}
},
{
id: '3',
src: {
pc: 'https://www.fliggy.com/mytrip/?tvm=tvip',
},
title: '飞猪里程2',
description: "每日签到领取飞猪里程",
mode: 'iframe',
key: "fliggy-tvip",
type: ['pc'],
checkin: true,
frequencyOption: ['daily', 'never'],
frequency: 'daily',
rateLimit:{
daily: 5,
hour: 2
}
},
{
id: '4',
src: {
m: 'https://h5.m.taobao.com/trip/rx-member/index/index.html?_projVer=0.1.25',
},
title: '飞猪里程3',
description: "飞猪移动页每日签到里程",
mode: 'iframe',
key: "rx-member",
type: ['m'],
checkin: true,
frequencyOption: ['daily', 'never'],
frequency: 'daily',
rateLimit:{
daily: 5,
hour: 2
}
},
{
id: '7',
src: {
m: 'https://h5.m.taobao.com/trip/welfare-center/mileage/index.html',
},
title: '飞猪里程5',
description: "飞猪签到领里程",
mode: 'iframe',
key: "welfare-center",
type: ['m'],
checkin: true,
frequencyOption: ['daily', 'never'],
frequency: 'daily',
rateLimit:{
daily: 5,
hour: 2
}
}
]
// 根据登录状态选择任务模式
let findTaskPlatform = function (task) {
let loginState = getLoginState()
let platform = null
if (loginState.class == 'alive') {
platform = task.type[0];
}
return platform
}
let getTask = function (taskId, currentPlatform) {
let taskParameters = getSetting('teaclub:task-parameters', [])
let parameters = (Array.isArray(taskParameters) && taskParameters.length > 0) ? taskParameters.find(t => t.id == taskId.toString()) : {}
let task = Object.assign({}, tasks.find(t => t.id == taskId.toString()), parameters)
let taskStatus = {}
let year = new Date().getFullYear()
let today = DateTime.local().toFormat("o")
let hour = new Date().getHours()
taskStatus.usage = {
hour: getSetting(`temporary:usage-${taskId}_${year}d:${today}:h:${hour}`, 0),
daily: getSetting(`temporary:usage-${taskId}_${year}d:${today}`, 0)
}
taskStatus.platform = findTaskPlatform(task);
taskStatus.frequency = getSetting(`task-${taskId}_frequency`, task.frequency)
taskStatus.last_run_at = localStorage.getItem(`task-${task.id}_lasttime`) ? parseInt(localStorage.getItem(`task-${task.id}_lasttime`)) : null
taskStatus.last_run_description = taskStatus.last_run_at ? "上次运行: " + readableTime(DateTime.fromMillis(Number(taskStatus.last_run_at))) : "从未执行";
// 如果是签到任务,则读取签到状态
if (task.checkin) {
let checkinRecord = getSetting(`checkin_${task.key}`, null)
if (checkinRecord && checkinRecord.date == DateTime.local().toFormat("o")) {
taskStatus.checked = true
taskStatus.checkin_description = "完成于:" + readableTime(DateTime.fromISO(checkinRecord.time)) + (checkinRecord.value ? ",领到:" + checkinRecord.value : "");
}
}
// 订单里程任务每月5次
if (task.id == "6") {
let year = new Date().getFullYear()
let month = new Date().getMonth()
let monthStatus = localStorage.getItem(`order-fliggy-${year}-${month}`)
if (monthStatus && monthStatus == 'Y') {
taskStatus.checked = true
taskStatus.checkin_description = "本月已领取五次"
}
}
// 如果限定平台
if (currentPlatform) {
if (task.type && task.type.indexOf(currentPlatform) < 0) {
taskStatus.unavailable = true
}
}
// 选择运行平台
if (!task.url) {
taskStatus.url = taskStatus.platform ? task.src[taskStatus.platform] : task.src[task.type[0]];
}
// 如果任务无可运行平台
if (!taskStatus.platform) {
taskStatus.suspended = true;
taskStatus.platform = task.type[0];
}
// 如果超出限制
if (taskStatus.usage.daily >= task.rateLimit.daily || taskStatus.usage.hour >= task.rateLimit.hour) {
taskStatus.pause = true;
}
return Object.assign(task, taskStatus)
}
let getTasks = function (currentPlatform) {
let taskList = tasks.map((task) => {
return getTask(task.id, currentPlatform)
})
return taskList.filter(task => !(task.unavailable || task.deprecated));
}
export {
frequencyOptionText,
mapFrequency,
tasks,
getTask,
getTasks,
findTaskPlatform
};
================================================
FILE: src/utils.js
================================================
import { DateTime } from 'luxon'
export const rand = function (n) {
return (Math.floor(Math.random() * n + 1));
}
export const price = function (price) {
return Number(Number(price).toFixed(2))
}
export const getSetting = function (settingKey, defaultValue) {
let setting = localStorage.getItem(settingKey)
if (setting) {
try {
setting = JSON.parse(setting)
} catch (error) { }
}
return setting ? setting : defaultValue
}
export const saveSetting = function (settingKey, value) {
return localStorage.setItem(settingKey, JSON.stringify(value))
}
export const readableTime = function (dateTime) {
if (DateTime.local().hasSame(dateTime, 'day')) {
return '今天 ' + dateTime.setLocale('zh-cn').toLocaleString(DateTime.TIME_SIMPLE)
}
if (DateTime.local().hasSame(dateTime.plus({ days: 1 }), 'day')) {
return '昨天 ' + dateTime.setLocale('zh-cn').toLocaleString(DateTime.TIME_SIMPLE)
}
return dateTime.setLocale('zh-cn').toFormat('f')
}
export const versionCompare = function (v1, v2, options) {
var lexicographical = options && options.lexicographical,
zeroExtend = options && options.zeroExtend,
v1parts = v1.split('.'),
v2parts = v2.split('.');
function isValidPart(x) {
return (lexicographical ? /^\d+[A-Za-z]*$/ : /^\d+$/).test(x);
}
if (!v1parts.every(isValidPart) || !v2parts.every(isValidPart)) {
return NaN;
}
if (zeroExtend) {
while (v1parts.length < v2parts.length) v1parts.push("0");
while (v2parts.length < v1parts.length) v2parts.push("0");
}
if (!lexicographical) {
v1parts = v1parts.map(Number);
v2parts = v2parts.map(Number);
}
for (var i = 0; i < v1parts.length; ++i) {
if (v2parts.length == i) {
return 1;
}
if (v1parts[i] == v2parts[i]) {
continue;
}
else if (v1parts[i] > v2parts[i]) {
return 1;
}
else {
return -1;
}
}
if (v1parts.length != v2parts.length) {
return -1;
}
return 0;
}
================================================
FILE: src/variables.js
================================================
module.exports = {
stateText: {
"failed": "失败",
"alive": "有效",
"unknown": "未知"
},
recommendServices: [
{
link: "https://cloud.tencent.com/redirect.php?redirect=1025&cps_key=8c3eff7793dd70781315d9b5c9727c39&from=console",
title: "腾讯云新客礼包",
description: "新客户无门槛领取2775元代金券",
class: "el-tag el-tag--success"
},
{
link: "https://promotion.aliyun.com/ntms/yunparter/invite.html?userCode=sqj7d3bm",
title: "阿里云优惠券",
description: "领取阿里云全品类优惠券",
class: "el-tag"
},
]
};
================================================
FILE: static/style/popup.css
================================================
@media (prefers-color-scheme:dark) {
body:not([data-weui-theme='light']) {
background-color: #1a1919;
color: #999;
}
body:not([data-weui-theme='light']) .messages, body:not([data-weui-theme='light']) .discounts {
background-color: transparent;
}
body:not([data-weui-theme='light']) .weui-navbar__item.weui-bar__item_on.zaoshu-tab {
color: #b9b9b9;
background: #661c1a91;
}
}
@media (prefers-color-scheme:light) {
body:not([data-weui-theme='dark']) {
background-color: #fff;
color: #333;
transition: background-color 0.3s ease;
}
}
select {
text-indent: 0.01px;
text-overflow: '';
-moz-appearance: none;
}
a {
color: #0b85c3;
}
a:hover {
color: #006da5;
}
html, body {
min-height: 580px;
min-width: 780px;
overflow-x: hidden;
overflow-y: hidden;
}
.popup{
height: 600px;
width: 800px;
overflow: hidden;
}
.weui-cell_select {
height: 34px;
font-size: 15px;
padding: 2px 15px;
}
.frequency_settings .weui-cell__bd i.show {
font-size: 20px;
margin-left: -5px;
display: inline-block !important;
height: 20px;
margin-top: -2px;
}
.page__hd {
padding: 10px;
}
.weui-dialog {
max-width: 440px;
}
.page__desc {
padding: 5px 10px;
}
.settings {
width: 45%;
background: #cccccc1f;
border-right: 1px solid var(--weui-FG-3);
}
.settings .weui-cell_switch {
height: 32px;
font-size: 15px;
padding-top: 4px;
padding-bottom: 4px;
}
.contents {
width: 55%;
overflow: hidden;
height: 600px;
}
.weui-navbar__item.weui-bar__item_on.zaoshu-tab {
color: #921714;
}
.weui-navbar__item.weui-bar__item_on{
color: var(--weui-BG-4);
font-weight: bold;
}
.contents .weui-tab {
height: 550px;
}
.weui-cell_switch {
height: 32px;
font-size: 16px;
}
.contents .weui-tab .weui-badge {
margin-left: 5px;
margin-top: -3px;
background-color: #4CAF50;
}
.contents .weui-tab .weui-badge.new-discounts {
background-color: #b9201d;
padding: 0.3em;
position: absolute;
}
.other_actions {
padding: 10px;
padding-bottom: 0;
}
.other_actions p {
padding: 5px 0;
}
.no_order, .no_message {
background: url(../image/empty.svg) no-repeat center 10px;
padding: 5em 0em;
text-align: center;
margin-top: 10em;
opacity: 0.5;
padding-top: 7em;
}
.no_message .tips{
font-size: 12px;
margin-top: .5em;
}
.bottom-tips {
padding: 5px;
}
.other_actions h3 {
font-size: 16px;
}
.recommendation {
height: 210px;
font-size: 12px;
}
.tips .weui-btn_mini {
padding: 0.1em .5em;
line-height: 1.4;
margin-bottom: -7px;
font-size: 12px;
}
.reward_tips .newyear {
color: #f15f5f;
}
#renderFrame {
height: 0px;
}
.reload-icon {
cursor: pointer;
}
.frequency_settings .weui-cell__bd {
line-height: 34px;
}
.frequency_settings .weui-icon-waiting-circle {
font-size: 19px;
}
.switch-paymethod {
cursor: pointer;
}
#notice {
color: #333;
}
.alipay_action {
line-height: 26px;
height: 24px;
width: 120px;
margin: 0 auto;
}
.alipay_action svg {
float: left;
}
.reload-icon {
background: url(../image/reload.svg) no-repeat 1px 1px;
width: 20px;
height: 21px;
color: #dcdcdc;
display: inline-block;
vertical-align: middle;
background-size: 17px;
}
.orders li, .messages li {
display: block;
}
.orders .order_time {
position: relative;
}
.orders .show-order {
-webkit-mask: url(../image/show.svg) no-repeat center;
mask: url(../image/show.svg) no-repeat center;
-webkit-mask-size: 16px;
mask-size: 16px;
}
.orders .show-order, .orders .hide-order {
width: 20px;
height: 21px;
color: #dcdcdc;
display: inline-block;
vertical-align: middle;
background-size: 16px;
cursor: pointer;
background-color: #ccc;
position: absolute;
right: 5px;
top: 1px;
}
.orders .hide-order {
-webkit-mask: url(../image/hide.svg) no-repeat center;
mask: url(../image/hide.svg) no-repeat center;
-webkit-mask-size: 16px;
mask-size: 16px;
}
.logo {
display: inline-block;
}
.order_time {
margin-top: .77em;
margin-bottom: .3em;
padding-left: 15px;
padding-right: 15px;
color: #999;
font-size: 12px;
padding-top: 5px;
}
#orders .weui-cell:before, .contents-box.weui-cells:before, .contents-box.weui-cells:after {
border-top: none;
content: none;
}
.orders .good_title {
height: 55px;
}
.good_title {
font-size: 12px;
height: 80px;
display: block;
clear: both;
width: 98%;
}
.orders .good_title p {
margin-left: 65px;
}
.self-recommendation p.tips {
font-size: 12px;
text-align: center;
padding: 1em;
color: #ccc;
}
.good_title p {
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
max-height: 78px;
-webkit-line-clamp: 3;
-moz-box-orient: vertical;
-webkit-box-orient: vertical;
padding-bottom: 5px;
line-height: 18px;
margin-left: 85px;
}
.orders .good_title img {
width: 55px;
height: 55px;
}
.good_title img {
display: inline-block;
position: absolute;
left: 15px;
top: 10px;
padding-right: 10px;
width: 75px;
height: 75px;
overflow: hidden;
}
.good_title .description {
font-size: 12px;
color: #666;
max-height: 35px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-moz-box-orient: vertical;
-webkit-box-orient: vertical;
}
.good_title span.count {
color: #949494;
}
img.promotion_title {
display: inline-block;
padding-right: 10px;
width: 55px;
height: 55px;
overflow: hidden;
float: left;
position: unset;
}
.good_title a {
font-size: 13px;
}
.promotion_price {
font-size: 14px;
color: #1aad19;
display: block;
font-weight: 500;
padding-bottom: 5px;
}
.changelog .time {
font-size: 12px;
color: #666;
}
.changelog .blog {
float: right;
color: #888;
font-size: 12px;
text-decoration: none;
line-height: 24px;
}
.go-buy {
background: #1aad19;
color: #fff;
font-size: 14px;
padding: .3em .5em;
border-radius: 2px;
}
.orders .dismiss {
float: right;
padding: 2px;
position: absolute;
top: -2px;
right: 6px;
font-size: 14px;
cursor: pointer;
}
.weui-cell.promotion {
background: #feedba54;
}
.buy-btn {
padding: 2px 5px;
line-height: 1.3;
font-size: 12px;
margin-top: 4px;
}
a.buy-btn:hover {
color: #efefef;
}
.good {
background: #fdfdfd;
border-bottom: 1px solid #f3f3f3;
border-top: 1px solid #f3f3f3;
}
.good+.good {
border-top: none;
}
.success_log {
font-size: 12px;
margin: 10px;
color: #690;
}
.order_price {
font-size: 12px;
color: #666;
display: block;
}
#dialogs, #changeLogs{
display: none;
}
.zaoshu-icon {
width: auto;
height: 15px;
margin-top: -4px;
vertical-align: middle;
}
.guide .weui-dialog__bd {
line-height: 1.6;
max-height: 300px;
overflow-x: hidden;
}
.guide .weui-dialog__bd p {
margin-bottom: 1em;
}
.testbox p {
text-align: left;
}
.new_price {
font-size: 14px;
color: #333;
}
.new_price.up {
color: #690;
}
.new_price.down {
color: #ea2222;
}
.time {
text-align: right;
}
.alipay_pay {
display: none;
}
.alipay_pay img {
padding: 24px;
background: #fff;
}
.weui-dialog__ft a {
cursor: pointer;
}
.weui-dialog .segmented-control {
width: 80%;
margin: 0 auto;
border: 1px solid #eee;
border-radius: 4px;
}
.segmented-control {
display: table;
width: 100%;
margin: 2em 0;
padding: 0;
background: #fff;
}
.segmented-control__item:first-child {
float: left;
}
.segmented-control__item:last-child {
float: right;
}
.segmented-control__item:first-child .segmented-control__label {
border-radius: 4px 0 0 4px;
}
.segmented-control__item:last-child .segmented-control__label {
border-radius: 0 4px 4px 0;
}
.segmented-control__item {
width: 49.5%;
display: inline-block;
margin: 0;
padding: 0;
list-style-type: none;
}
.segmented-control__input {
position: absolute;
visibility: hidden;
}
.segmented-control__label {
display: block;
margin: 0 -1px -1px 0;
/* -1px margin removes double-thickness borders between items */
padding: .45em .25em;
font: 14px/1.5 sans-serif;
text-align: center;
cursor: pointer;
}
.segmented-control__label:hover {
background: #fafafa;
}
.checked .segmented-control__label {
background: #1aad19;
color: #fff;
}
.auto_login {
font-size: 14px;
margin: 10px 5px;
}
.weui-navbar__item {
padding: 7px 0;
cursor: pointer;
font-size: 15px;
color: var(--weui-TAG-TEXT-BLACK);
}
.recommendedLink p {
text-align: center;
margin-top: 5px;
}
.iframe-loading {
z-index: 0 !important;
}
.js-close {
position: absolute;
right: 10px;
top: 1px;
padding: 4px;
font-size: 18px;
cursor: pointer;
}
.settings_box {
overflow-y: auto;
overflow-x: hidden;
height: 515px;
}
.settings .page__desc {
font-size: 12px;
height: 31px;
line-height: 18px;
color: #666;
}
.settings .weui-tab {
height: auto;
}
.tips .page__desc {
padding: 2px 5px;
}
.bottom {
height: 44px;
font-size: 12px;
position: relative;
}
img.weui-tabbar__icon {
width: 20px;
height: 20px;
cursor: pointer;
}
.changelogs {
font-size: 14px;
padding: 10px;
text-align: left;
line-height: 2.2;
max-height: 300px;
overflow-y: auto;
}
.loginNotice {
font-size: 14px;
padding: 10px 15px;
text-align: left;
line-height: 2.2;
max-height: 300px;
color: #5a5a5a;
overflow-y: auto;
}
.loginNotice b {
color: #2b902f;
}
#loginNotice .title {
background: #fbf3a5;
color: #ce7f66;
padding-top: 0.8em;
}
#loginNotice a {
color: #126700;
}
#loginNotice a:hover {
color: #0c4600;
}
#loginNotice a.failed {
color: #de4545;
}
#loginNotice.state-alive .title {
background: #b9e684ad;
color: #2b902f;
padding-top: 0.8em;
}
#faqDialags .weui-dialog, #feedbackDialags .weui-dialog {
background-color: #f8f8f8;
height: 460px;
}
#specialEventDialags iframe, #faqDialags iframe, #feedbackDialags iframe {
width: 90%;
height: 450px;
}
#feedbackResult, #wechatDialags, #feedbackDialags, #faqDialags, #specialEventDialags, #loginNotice, #listenAudio {
display: none;
}
.contents-box {
margin-top: 0;
}
.listenVoice {
text-align: left;
}
.messages-header {
display: flex;
border-bottom: 1px solid #ebeef5;
height: 33px;
padding-top: 0px;
position: fixed;
width: 54%;
background: #fafafa;
z-index: 10;
}
#order.weui-cells:after {
border-bottom: none;
}
.messages-tab {
position: relative;
flex: 1;
height: 48px;
cursor: pointer;
}
.messages-tab span {
width: 22px;
height: 22px;
background: #ccc;
display: block;
margin: 0 auto;
}
.messages-tab.selectedTab span {
background: #4bc2ff;
}
.messages-tab span.notice {
-webkit-mask: url(../image/notice.svg) no-repeat center;
mask: url(../image/notice.svg) no-repeat center;
-webkit-mask-size: 20px;
mask-size: 20px;
}
.messages-tab span.coupon {
-webkit-mask: url(../image/coupon.svg) no-repeat center;
mask: url(../image/coupon.svg) no-repeat center;
-webkit-mask-size: 22px;
mask-size: 22px;
}
.messages-tab span.checkin {
-webkit-mask: url(../image/checkin.svg) no-repeat center;
mask: url(../image/checkin.svg) no-repeat center;
-webkit-mask-size: 20px;
mask-size: 20px;
}
.message-items {
margin-top: 20px;
}
.message-items .weui-media-box {
padding: 5px 15px;
border-bottom: 1px solid #ebeef5;
}
.Button--link, .Button--plain {
height: auto;
padding: 0;
line-height: inherit;
border: none;
border-radius: 0;
}
.messages-tabIcon {
fill: #c2cfde;
}
.selectedTab .messages-tabIcon {
fill: #0f88eb;
}
button.Button.messages-tab.Button--plain:focus {
outline: none;
}
.selectedTab {
background: #f5f5f5;
border: 1px solid #e6e6e6;
border-bottom: none;
border-top: none;
background-image: linear-gradient(0deg, #ffffff, #e6e6e64a);
}
.message i {
padding-right: 5px;
}
.message .checkin_notice {
background: url(../image/mileage.png) no-repeat;
width: 20px;
height: 20px;
background-size: 20px;
display: inline-block;
margin-bottom: -3px;
}
.message .checkin_notice.coin {
background-image: url(../image/coin.png);
}
.message .notice {
background: url(../image/notice.png) no-repeat;
width: 20px;
height: 20px;
background-size: 20px;
display: inline-block;
margin-bottom: -3px;
}
.coupon-box {
position: relative;
height: 50px;
border: 1px solid #f2f2f2;
background: #fff;
display: inline-block;
display: block;
padding: 10px;
}
.coupon-box .price {
padding: 1px 5px;
color: #f23030;
font-size: 15px;
background: #fff4ec;
display: inline-block;
}
.coupon-box a {
font-size: 14px;
color: #555;
padding: 4px;
}
.reward {
cursor: pointer;
display: block;
color: #d29737;
}
.reward h4 {
padding-top: 10px;
font-size: 22px;
color: #4e4c4c;
}
.reward .qrcode {
width: 210px;
padding: 10px;
}
.reward .switch-tips {
font-size: 14px;
color: #ccc;
}
.other_actions .tips {
color: #ccc;
}
.switch {
cursor: pointer;
color: #f54e4d;
}
.switch p {
margin-top: -10px;
}
.switch .icon {
margin-bottom: -5px;
}
#unreadCount {
display: none;
}
.alipay_pay .redpack img {
padding: 10px;
}
.switch-paymethod i {
font-size: 21px;
margin-top: -4px;
}
.weui-cell__bd .weui-icon-info-circle {
margin-top: -4px;
}
#listenAudio .weui-cells {
margin-bottom: .8em;
}
#listenAudio .weui-cell_access {
cursor: pointer;
}
#changeLogs b {
color: #4CAF50;
}
.text-tips {
font-size: 12px;
text-align: center;
color: #666;
padding-top: 5px;
padding-bottom: 10px;
}
.recommendServices {
text-align: center;
}
.recommendServices .el-tag {
margin-right: 2px;
}
.openMobilePage {
cursor: pointer;
}
.el-tag {
background-color: rgba(64, 158, 255, .1);
display: inline-block;
padding: 0 10px;
height: 32px;
line-height: 30px;
font-size: 12px;
color: #409eff;
border-radius: 4px;
box-sizing: border-box;
border: 1px solid rgba(64, 158, 255, .2);
white-space: nowrap;
}
.el-tag a {
color: #2196F3;
}
.el-tag--success {
background-color: rgba(103, 194, 58, .1);
border-color: rgba(103, 194, 58, .2);
color: #67c23a;
}
.el-tag--success a {
color: #67c23a;
}
.el-tag--warning {
background-color: rgba(230, 162, 60, .1);
border-color: rgba(230, 162, 60, .2);
color: #e6a23c;
}
.el-tag--warning a {
color: #e6a23c;
}
.el-tag--danger {
background-color: hsla(0, 87%, 69%, .1);
border-color: hsla(0, 87%, 69%, .2);
color: #f56c6c;
}
.el-tag--danger a {
color: #ff1d1d;
}
.bottom-box {
height: 55px;
border-top: #d4d4d4;
background: #f7f7fa0a;
position: relative;
}
.bottom-box .avatar {
width: 30px;
position: absolute;
left: 10px;
bottom: 11px;
height: 30px;
}
.bottom-box .login-state {
-webkit-mask: url(../image/avatar.svg) no-repeat center;
-webkit-mask-size: 30px;
mask: url(../image/avatar.svg) no-repeat center;
mask-size: 30px;
width: 30px;
height: 30px;
color: #dcdcdc;
display: inline-block;
cursor: pointer;
background-color: #cecece;
}
.bottom-box .login-state.alive {
background-color: #41bd2a;
}
.bottom-box .login-state.failed {
background-color: #f56c6c;
}
.bottom-box .login-state.warning {
background-color: #f7aa4d;
}
.bottom-box .links {
right: 10px;
position: absolute;
bottom: 10px;
}
.links .text-tips {
color: #bbb;
padding-bottom: 5px;
}
.links .el-tag {
padding: 0 8px;
}
.tips .weui-btn {
display: none;
}
.tips a.weui-btn_primary.weui-btn:hover {
color: #ffffffd6;
}
.showChangeLog {
cursor: pointer;
}
.offline-icon {
-webkit-mask: url(../image/offline.svg) no-repeat center;
-webkit-mask-size: 22px;
mask: url(../image/offline.svg) no-repeat center;
mask-size: 22px;
width: 22px;
height: 22px;
display: inline-block;
background-color: #666;
padding-right: 5px;
margin-bottom: -4px;
}
.online-icon {
-webkit-mask: url(../image/online.svg) no-repeat center;
-webkit-mask-size: 22px;
mask: url(../image/online.svg) no-repeat center;
mask-size: 22px;
width: 22px;
height: 22px;
background-color: #2b902f;
padding-right: 5px;
margin-bottom: -4px;
display: none;
}
.request-permissions-icon.weui-icon-warn {
color: #FFC107;
font-size: 22px;
cursor: pointer;
}
.state-alive .online-icon {
display: inline-block;
}
.state-alive .offline-icon {
display: none;
}
.el-tag--plus {
border-color: #f7aa4d;
background-color: #f9d2a3;
}
.el-tag--plus a {
color: #da8d00;
}
.settings .weui-cells_form {
margin-top: .8em;
}
.help_btns .el-tag a {
font-size: 14px;
padding: 12px;
}
.text-tips.version {
padding-bottom: 0;
cursor: pointer;
}
.loginNotice .detail {
display: none;
padding-top: 20px;
}
.loginNotice .detail h3 {
text-align: center;
}
.unknown .status-icon {
background-color: #ccc;
}
.alive .status-icon {
background-color: #289e2d;
}
.alive .status-text {
color: #289e2d;
}
.alive .weui-cell {
background: #e6ffcad4;
}
.alive .weui-cell:hover {
background: #d8f9b4;
}
.failed .weui-cell {
background: #ffd3d3b5;
}
.failed .weui-cell:hover {
background: #ffd9d9;
}
.failed .status-text {
color: #de4545;
}
#login i {
margin-top: -3px;
}
#know_more {
text-align: center;
padding-top: 10px;
color: #999;
cursor: pointer;
}
.update .weui-dialog .weui-dialog__bd {
white-space: pre-line;
text-align: left;
padding-top: 1em;
}
.update .dismiss {
cursor: pointer;
float: right;
margin-top: -13px;
padding: 5px 10px;
font-size: 22px;
margin-right: -7px;
}
.showApplyAlipayCode {
cursor: pointer;
margin-top: 10px;
color: #10aeff;
}
.apply-alipay-code .weui-dialog {
border: 5px solid #10aeff;
min-height: 400px;
}
.apply-alipay-code .weui-dialog__title {
color: #10aeff;
}
.reward-tips {
font-size: 12px;
}
.new-version {
margin-left: 5px;
margin-right: 5px;
margin-top: -2px;
padding: .2em .5em;
}
@media screen and (min-width: 352px) {
.weui-dialog {
width: 440px;
}
}
================================================
FILE: static/style/start.css
================================================
.start{
width: 640px;
margin: 0 auto;
}
.page, body {
background-color: var(--weui-BG-0);
}
.find-coupon{
width: 100%;
border: 1px solid #ccc;
margin: 12px 0;
}
================================================
FILE: static/style/style.css
================================================
#teaclub {
min-height: 80px;
background: #f1fde347;
padding: 10px;
margin-bottom: 1em;
}
#teaclub .information-from{
font-size: 12px;
color: #ccc;
height: 12px;
display: block;
text-align: right;
padding-top: 4px;
padding-bottom: 4px;
}
#teaclub dt.metatit{
text-align: left;
float: left;
width: 66px;
}
.tb-wrap-newshop #teaclub dt.metatit{
width: 60px;
}
#teaclub .prop dd{
width: 420px;
float: left;
position: relative;
overflow: visible;
height: auto;
z-index: 1;
}
.tb-wrap-newshop #teaclub .prop dd{
width: 400px;
}
#teaclub .clear:after {
content: '\20';
display: block;
height: 0;
clear: both;
}
#Coupon-box{
margin-bottom: 1em;
}
a.teaclub-coupon:hover {
text-decoration: none;
}
#teaclub .loading, #teaclub .coupon-not-found {
background: #fff;
font-size: 16px;
text-align: center;
color: #ccc;
}
#teaclub .loading img, #teaclub .coupon-not-found img {
width: 80px;
vertical-align: middle;
}
#teaclub .qrcode{
width: 60px;
display: block;
float: left;
padding: 5px;
}
.teaclub-discount {
padding: 10px;
background-color: #FFF2E8;
}
.coupon-bonus-item {
width: 100%;
background-color: #ffe2e0;
border-radius: 6px;
display: block;
max-width: 400px;
}
.coupon-bonus-item .coupon-item-left {
width: 69%;
color: #fff;
background-color: #de1d3d;
border-radius: 6px 0 0 6px;
display: inline-block;
}
.coupon-bonus-item .coupon-item-right {
text-align: right;
padding-left: 7px;
margin-top: 7px;
color: #df4c47;
display: inline-block;
}
.coupon-bonus-item .coupon-item-rmb {
font-family: arial;
font-size: 14px;
line-height: 18px;
text-align: right;
padding-right: 12px;
margin-top: 14px;
}
.coupon-bonus-item .coupon-item-surplus {
font-size: 12px;
line-height: 18px;
text-align: right;
padding-right: 12px;
margin-top: 6px;
}
.coupon-bonus-item .coupon-item-rmb .rmb {
font-family: tahoma;
font-size: 28px;
line-height: 18px;
}
#J_OtherDiscount .coupon-bonus-item {
line-height: 1;
margin-top: 20px;
}
#teaclub .PDD-card {
-webkit-box-align: stretch;
-ms-flex-align: stretch;
align-items: stretch;
overflow: hidden;
position: relative;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
min-height: 128px;
max-width: 400px;
border-radius: 6px;
background-color: #f5f5f5;
text-decoration: none;
margin-bottom: 0.5em;
}
#teaclub .PDD-cardContainer {
-webkit-box-align: stretch;
-ms-flex-align: stretch;
align-items: stretch;
padding: 14px;
box-sizing: border-box;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
position: relative;
width: 100%;
z-index: 2;
}
#teaclub .PDD-imageContainer {
border-radius: 0px;
height: 100px;
width: 100px;
flex-shrink: 0;
overflow: hidden;
position: relative;
}
#teaclub .PDD-image {
height: 100%;
width: 100%;
}
#teaclub .PDD-qrcode {
position: absolute;
}
#teaclub .PDD-cardContainer:hover .PDD-qrcode {
z-index: 10;
}
#teaclub .PDD-info {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-direction: normal;
-ms-flex-direction: column;
flex-direction: column;
-webkit-box-flex: 1;
-ms-flex-positive: 1;
flex-grow: 1;
margin-left: 12px;
}
#teaclub .PDD-titleText {
line-height: 20px;
max-height: 40px;
color: #1a1a1a;
font-size: 16px;
line-height: 19px;
font-weight: 600;
font-synthesis: style;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
#teaclub .PDD-tool {
margin-top: auto;
-webkit-box-align: end;
-ms-flex-align: end;
align-items: flex-end;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
}
#teaclub .PDD-toolLeft {
margin-right: auto;
}
#teaclub .PDD-price {
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
color: #FF0036;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
font-size: 16px;
font-weight: 500;
line-height: 18px;
margin-right: auto;
}
#teaclub .PDD-button--plain.PDD-button--orange {
color: #FF0036;
}
#teaclub .PDD-button--plain {
-ms-flex-item-align: start;
align-self: flex-start;
font-size: 13px;
height: 18px;
font-weight: 600;
font-synthesis: style;
}
#teaclub .PDD-button {
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-ms-flex-negative: 0;
flex-shrink: 0;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
}
#teaclub-slides .slideshow-container {
max-width: 1000px;
position: relative;
margin: auto;
}
#teaclub-slides .teaClubSlides {
display: none;
}
/* Next & previous buttons */
#teaclub .prev, #teaclub .next {
cursor: pointer;
position: absolute;
top: 50%;
width: auto;
margin-top: -22px;
padding: 16px;
color: white;
font-weight: bold;
font-size: 18px;
transition: 0.6s ease;
border-radius: 0 3px 3px 0;
user-select: none;
}
/* Position the "next button" to the right */
#teaclub .next {
right: 0;
border-radius: 3px 0 0 3px;
}
/* On hover, add a black background color with a little bit see-through */
#teaclub .prev:hover, #teaclub .next:hover {
background-color: rgba(0,0,0,0.8);
}
/* Caption text */
#teaclub .text {
color: #f2f2f2;
font-size: 15px;
padding: 8px 12px;
position: absolute;
bottom: 8px;
width: 100%;
text-align: center;
}
/* Number text (1/3 etc) */
#teaclub .number-text {
color: #f2f2f2;
font-size: 12px;
padding: 8px 12px;
position: absolute;
top: 0;
}
/* The dots/bullets/indicators */
#teaclub .dot {
cursor: pointer;
height: 15px;
width: 15px;
margin: 0 2px;
background-color: #bbb;
border-radius: 50%;
display: inline-block;
transition: background-color 0.6s ease;
}
#teaclub .active,#teaclub .dot:hover {
background-color: #717171;
}
/* Fading animation */
#teaclub .fade {
-webkit-animation-name: fade;
-webkit-animation-duration: 1.5s;
animation-name: fade;
animation-duration: 1.5s;
}
@-webkit-keyframes fade {
from {opacity: .4}
to {opacity: 1}
}
@keyframes fade {
from {opacity: .4}
to {opacity: 1}
}
================================================
FILE: webpack.config.js
================================================
const path = require('path');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const CopyPlugin = require('copy-webpack-plugin');
const { EnvironmentPlugin } = require('webpack')
function modifyManifest(buffer) {
let manifest = JSON.parse(buffer.toString());
// make any modifications you like, such as
if (process.env.VERSION) {
manifest.version = process.env.VERSION;
}
// pretty print to JSON with two spaces
manifest_JSON = JSON.stringify(manifest, null, 2);
return manifest_JSON;
}
module.exports = {
mode: process.env.NODE_ENV || 'development',
entry: {
background: './src/background.js',
content_script: './src/content_script.js',
start: './src/start.js',
popup: './src/popup.js'
},
output: {
filename: 'static/[name].js',
path: path.resolve(__dirname, 'dist')
},
resolve: {
alias: {
vue: 'vue/dist/vue.runtime.esm.js'
}
},
node: {
fs: 'empty'
},
module: {
rules: [
{
test: /\.(png|jpg|gif)$/i,
use: [
{
loader: 'url-loader',
loader: 'file-loader',
options: {
limit: 8192,
outputPath: 'images',
esModule: false
},
},
],
},
{
test: /\.svg$/,
use: [
'svg-url-loader',
]
},
{
test: /\.less$/,
use: [
'vue-style-loader',
'css-loader',
'less-loader'
]
},
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
},
{
test: /\.vue$/,
loader: 'vue-loader'
}
]
},
plugins: [
new CleanWebpackPlugin(),
new CopyPlugin([
{
from: "public/manifest.json",
to: "./manifest.json",
transform(content, path) {
return modifyManifest(content)
}
},
{ from: 'public', to: '.' },
{
from: 'static/image/icon',
to: 'static/image'
},
{
from: 'node_modules/@sunoj/touchemulator/touch-emulator.js',
to: 'static'
},
{
from: 'node_modules/zepto/dist/zepto.min.js',
to: 'static'
}
]),
new VueLoaderPlugin(),
new EnvironmentPlugin({
NODE_ENV: 'development',
BROWSER: 'chrome',
VERSION: '0.1.1',
BUILDID: 0
})
]
};
gitextract_q6d88nwp/ ├── .babelrc ├── .vscode/ │ └── settings.json ├── package.json ├── public/ │ ├── background.html │ ├── manifest.json │ ├── popup.html │ └── start.html ├── readme.md ├── src/ │ ├── account.js │ ├── background.js │ ├── components/ │ │ ├── app.vue │ │ ├── discounts.vue │ │ ├── events.vue │ │ ├── guide.vue │ │ ├── links.vue │ │ ├── loading.vue │ │ ├── popup.vue │ │ └── report.vue │ ├── content_script.js │ ├── popup.js │ ├── start.js │ ├── tasks.js │ ├── utils.js │ └── variables.js ├── static/ │ └── style/ │ ├── popup.css │ ├── start.css │ └── style.css └── webpack.config.js
SYMBOL INDEX (53 symbols across 5 files)
FILE: src/background.js
function newMessage (line 13) | async function newMessage(messageId, data) {
function updateMessages (line 22) | async function updateMessages() {
function saveJobStack (line 159) | function saveJobStack(jobStack) {
function scheduleJob (line 166) | function scheduleJob(task) {
function findJobs (line 190) | function findJobs() {
function log (line 233) | function log(type, message, details) {
function resetIframe (line 241) | function resetIframe(domId) {
function incrementUsage (line 247) | function incrementUsage(task) {
function runJob (line 256) | function runJob(taskId, force = false) {
function openByIframe (line 310) | function openByIframe(src, type, delayTimes = 0) {
function updateUnreadCount (line 334) | function updateUnreadCount(change = 0) {
function openWebPageAsMobile (line 378) | function openWebPageAsMobile(url) {
function updateIcon (line 408) | function updateIcon() {
function openLoginPage (line 471) | function openLoginPage() {
function saveLoginState (line 478) | function saveLoginState(loginState) {
function sendChromeNotification (line 503) | function sendChromeNotification(id, content) {
function runTask (line 515) | function runTask(msg, sendResponse) {
function markCheckinStatus (line 547) | function markCheckinStatus(msg) {
function updateRunStatus (line 573) | function updateRunStatus(msg) {
function loadSettingsToLocalStorage (line 592) | function loadSettingsToLocalStorage(key) {
function loadRecommendSettingsToLocalStorage (line 599) | function loadRecommendSettingsToLocalStorage() {
function sendMessageToPage (line 628) | function sendMessageToPage(targetPage, data) {
function timeoutPromise (line 634) | function timeoutPromise(promise, ms) {
function searchCoupon (line 644) | async function searchCoupon(params) {
FILE: src/content_script.js
function mockTap (line 33) | function mockTap(element) {
function simulateClick (line 40) | function simulateClick(domNode, mouseEvent) {
function mockClick (line 53) | function mockClick(element) {
function sendTouchEvent (line 68) | function sendTouchEvent(x, y, element, eventType) {
function injectScript (line 94) | function injectScript(file, node) {
function injectScriptCode (line 103) | function injectScriptCode(code, node = 'body') {
function escapeSpecialChars (line 119) | function escapeSpecialChars(jsonString) {
function getSetting (line 126) | function getSetting(name, cb) {
function createElementFromHTML (line 136) | function createElementFromHTML(htmlString) {
function addDiscountElement (line 143) | function addDiscountElement() {
function addCouponElement (line 187) | function addCouponElement(coupon) {
function buildGoodsBatch (line 221) | function buildGoodsBatch(goodsBatch) {
function buildGoodCard (line 292) | function buildGoodCard(good) {
function findCoupon (line 333) | async function findCoupon(disable_find_coupon) {
function markCheckinStatus (line 350) | function markCheckinStatus(task, data, cb) {
function markFliggyCheckin (line 368) | function markFliggyCheckin(task, orderId) {
function fliggyCheckin (line 405) | function fliggyCheckin(setting) {
function fliggyCheckin2 (line 432) | function fliggyCheckin2(setting) {
function fliggyCheckin3 (line 460) | function fliggyCheckin3(setting) {
function fliggyCheckin6 (line 501) | function fliggyCheckin6(setting) {
function fliggyCheckin7 (line 524) | function fliggyCheckin7(setting) {
function accountAlive (line 552) | function accountAlive(type, message) {
function CheckDom (line 572) | function CheckDom() {
function checkLoginState (line 670) | function checkLoginState() {
function dealWithSearchRes (line 704) | function dealWithSearchRes(content) {
FILE: src/popup.js
function readMessage (line 28) | function readMessage() {
FILE: src/utils.js
function isValidPart (line 35) | function isValidPart(x) {
FILE: webpack.config.js
function modifyManifest (line 7) | function modifyManifest(buffer) {
method transform (line 86) | transform(content, path) {
Condensed preview — 28 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (159K chars).
[
{
"path": ".babelrc",
"chars": 286,
"preview": "{\n \"presets\": [\n [\"env\", {\n \"targets\": {\n \"browsers\": [\"last 1 Chrome version\"]\n "
},
{
"path": ".vscode/settings.json",
"chars": 84,
"preview": "{\n \"cSpell.words\": [\n \"Lazyload\",\n \"fliggy\",\n \"metatit\",\n \"tmall\"\n ]\n}"
},
{
"path": "package.json",
"chars": 1659,
"preview": "{\n \"name\": \"teaclub\",\n \"version\": \"0.0.1\",\n \"author\": \"Ming\",\n \"private\": true,\n \"scripts\": {\n \"start\": \"npx web"
},
{
"path": "public/background.html",
"chars": 209,
"preview": "<!DOCTYPE html>\n<html>\n <head>\n <meta charset=\"UTF-8\">\n <script src=\"static/background.js\"></script>\n </head>\n "
},
{
"path": "public/manifest.json",
"chars": 1246,
"preview": "{\n \"manifest_version\": 2,\n \"name\": \"茶友会 - 淘宝查券助手\",\n \"short_name\": \"茶友会\",\n \"description\": \"茶友会是自动为你查找淘宝优惠券,自动签到领飞猪里程的"
},
{
"path": "public/popup.html",
"chars": 315,
"preview": "<!DOCTYPE html>\n<html>\n\n<head>\n <meta charset=\"UTF-8\">\n <title>茶友会</title>\n <meta name=\"viewport\" content=\"user-scala"
},
{
"path": "public/start.html",
"chars": 1484,
"preview": "<!DOCTYPE html>\n<html>\n <head>\n <meta charset=\"UTF-8\">\n <title>茶友会安装成功!</title>\n </head>\n <body>\n <div class"
},
{
"path": "readme.md",
"chars": 2050,
"preview": "# 茶友会 - 淘宝查券助手\n\n茶友会是自动为你查找淘宝优惠券,自动签到领飞猪里程的多功能购物助手。\n\n**强烈推荐使用 Chrome 商店安装**(这样才能获得自动更新):\n\n<a target='_blank' rel='nofollo"
},
{
"path": "src/account.js",
"chars": 748,
"preview": "import { getSetting } from './utils'\n\nexport const getLoginState = function () {\n let loginState = {\n pc: getSetting"
},
{
"path": "src/background.js",
"chars": 22893,
"preview": "$ = window.$ = window.jQuery = require('jquery')\nimport * as _ from \"lodash\"\nimport Logline from 'logline'\nimport Dexie "
},
{
"path": "src/components/app.vue",
"chars": 21016,
"preview": "<template>\n <div>\n <div class=\"main-container\">\n <div class=\"settings\">\n <div class=\"weui-tab\">\n "
},
{
"path": "src/components/discounts.vue",
"chars": 12975,
"preview": "<template>\n <div id=\"discounts\" class=\"contents-box discounts\">\n <div class=\"top-bar\">\n <div class=\"tabs\">\n "
},
{
"path": "src/components/events.vue",
"chars": 1165,
"preview": "<template>\n <hooper>\n <slide v-for=\"(event, index) in events\" :key=\"event.id\" :index=\"index\">\n <a :href=\"`${ev"
},
{
"path": "src/components/guide.vue",
"chars": 2725,
"preview": "<template>\n <div class=\"guide\">\n <div class=\"js_dialog\" style=\"opacity: 1;\" v-if=\"step > 0\">\n <div class=\"weui-"
},
{
"path": "src/components/links.vue",
"chars": 2272,
"preview": "<template>\n <div class=\"links\">\n <span :class=\"action.class\" v-for=\"(action, index) in actionLinks\" :key=\"action.id"
},
{
"path": "src/components/loading.vue",
"chars": 3274,
"preview": "<template>\n <div class=\"loading-masker\">\n <div class=\"white-widget grey-bg author-area\" v-for=\"(masker, index) in nu"
},
{
"path": "src/components/popup.vue",
"chars": 3275,
"preview": "<template>\n <div v-if=\"events && events.length > 0 && loadEvents\">\n <div class=\"popup-show\" v-if=\"showPopup\">\n "
},
{
"path": "src/components/report.vue",
"chars": 4259,
"preview": "<template>\n<div class=\"reprot\">\n <div class=\"report-mask\" @click=\"hide\" v-if=\"show\"></div>\n <div class=\"report-problem"
},
{
"path": "src/content_script.js",
"chars": 23449,
"preview": "import 'weui';\nimport weui from 'weui.js';\nimport QRCode from \"qrcode-svg\";\n\nimport '../static/style/style.css'\n\nvar obs"
},
{
"path": "src/popup.js",
"chars": 3544,
"preview": "import * as _ from \"lodash\"\n$ = window.$ = window.jQuery = require('jquery')\nimport 'weui'\nimport weui from 'weui.js'\nim"
},
{
"path": "src/start.js",
"chars": 49,
"preview": "import 'weui'\nimport '../static/style/start.css'\n"
},
{
"path": "src/tasks.js",
"chars": 5092,
"preview": "import {DateTime} from 'luxon'\nimport { getLoginState } from './account'\nimport { getSetting, readableTime } from './uti"
},
{
"path": "src/utils.js",
"chars": 1965,
"preview": "import { DateTime } from 'luxon'\n\nexport const rand = function (n) {\n return (Math.floor(Math.random() * n + 1));\n}\nexp"
},
{
"path": "src/variables.js",
"chars": 545,
"preview": "\nmodule.exports = {\n stateText: {\n \"failed\": \"失败\",\n \"alive\": \"有效\",\n \"unknown\": \"未知\"\n },\n recommendServices: "
},
{
"path": "static/style/popup.css",
"chars": 17956,
"preview": "\n@media (prefers-color-scheme:dark) {\n body:not([data-weui-theme='light']) {\n background-color: #1a1919;\n color: "
},
{
"path": "static/style/start.css",
"chars": 186,
"preview": ".start{\n width: 640px;\n margin: 0 auto;\n}\n\n.page, body {\n background-color: var(--weui-BG-0);\n}\n\n.find-coupon{\n"
},
{
"path": "static/style/style.css",
"chars": 6218,
"preview": "#teaclub {\n min-height: 80px;\n background: #f1fde347;\n padding: 10px;\n margin-bottom: 1em;\n}\n\n#teaclub .information-"
},
{
"path": "webpack.config.js",
"chars": 2464,
"preview": "const path = require('path');\nconst VueLoaderPlugin = require('vue-loader/lib/plugin');\nconst { CleanWebpackPlugin } = r"
}
]
About this extraction
This page contains the full source code of the sunoj/teaclub GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 28 files (140.0 KB), approximately 41.8k tokens, and a symbol index with 53 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.