微信、知乎、掘金、简书,贴吧,文章页宽屏,贴吧签到

微信、知乎、掘金、简书,贴吧,文章页宽屏,贴吧签到(模拟客户端获得更多经验)

As of 2020-09-06. See the latest version.

// ==UserScript==
// @name         微信、知乎、掘金、简书,贴吧,文章页宽屏,贴吧签到
// @namespace    http://tampermonkey.net/
// @version      1.1.1
// @description  微信、知乎、掘金、简书,贴吧,文章页宽屏,贴吧签到(模拟客户端获得更多经验)
// @author       sakura-flutter
// @match        https://mp.weixin.qq.com/s*
// @match        https://zhuanlan.zhihu.com/p/*
// @match        https://www.zhihu.com/question/*
// @match        https://juejin.im/post/*
// @match        https://www.jianshu.com/p/*
// @match        https://tieba.baidu.com/p/*
// @match        https://tieba.baidu.com
// @grant        unsafeWindow
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @connect      tieba.baidu.com
// @require      https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.min.js
// @require      https://cdn.jsdelivr.net/npm/md5/dist/md5.min.js
// @compatible   chrome >= 80  firefox >= 75
// @homepage     https://gist.github.com/sakura-flutter/116338dcd68ab99e50322aa0058a35a1
// ==/UserScript==

/* global Vue MD5 */

(function() {
    'use strict'
    // true|false 开启后会打开日志
    const isDebug = false


    const $ = document.querySelector.bind(document)
    const $$ = document.querySelectorAll.bind(document)

    function log(...args) {
        if (!isDebug) return
        console.log(...args)
    }

    // 主函数
    function main() {
        const sites = checkWebsites()
        sites.forEach(site => {
            const hanlder = handlers.get(site)
            log(site)
            hanlder && hanlder()
        })
    }

    // 检查网站
    function checkWebsites() {
        const { origin, pathname } = location
        const url = origin + pathname
        // 格式[ ['xx', true|false], ]
        const sites = [
            ['mpWeixin', /mp.weixin.qq.com\/s/.test(url)],
            ['zhihu', /zhuanlan.zhihu.com\/p\//.test(url) || /zhihu.com\/question\//.test(url)],
            ['juejin', /juejin.im\/post\//.test(url)],
            ['jianshu', /jianshu.com\/p\//.test(url)],
            ['tieba', /tieba.baidu.com\/p\//.test(url)],
            ['tiebaMain', url.endsWith('tieba.baidu.com/')],
        ]
        // 返回匹配的页面
        return sites
            .filter(item => item[1])
            .map(item => item[0])
    }

    // 对应网页要执行的操作操作
    const handlers = new Map()

    /* ===微信文章===start */
    handlers.set('mpWeixin', function() {
        const store = createStore('mpWeixin')
        function execute() {
            GM_addStyle(`
              /* 文章宽屏 */
              .rich_media_area_primary_inner { max-width: 100vw !important; }
              /* 二维码位置 */
              #js_pc_qr_code .qr_code_pc { position: fixed; top: 25vh; right: 3vw; }

              @media screen and (min-width: 1024px) {
                .rich_media_area_primary_inner { max-width: 75vw !important; }
                #js_pc_qr_code .qr_code_pc { position: fixed; top: 25vh; right: 3vw; }
              }
            `)

            // 文章图片宽高(仅对大图处理)
            const imgEls = $$('.rich_media_area_primary_inner img')
            imgEls.forEach(img => {
                img.addEventListener('load', () => {
                    // 页面本身对图片有宽高处理,延时后再处理
                    setTimeout(() => {
                        const width = parseFloat(getComputedStyle(img).width)
                        if (width >= 400) {
                            img.style.cssText += 'width: auto !important; height: auto !important;'
                        }
                    },16)
                })
            })
            Toast.info('已宽屏处理')
        }

        createWidescreenControl({ store, execute })
    })
    /* ===微信文章===end */

    /* ===知乎===start */
    handlers.set('zhihu', function() {
        const store = createStore('zhihu')
        function execute() {
            GM_addStyle(`
              /* 知乎专栏 */
              .Post-NormalMain .Post-Header, .Post-NormalMain>div, .Post-NormalSub>div {
                 width: 65vw;
                 min-width: 690px;
              }
              .Post-SideActions { left: calc((100vw - 82vw)/2); }

              /* 知乎问答 */
              .QuestionHeader-content, .QuestionHeader-footer {
                 width: 75vw;
                 min-width: 1000px;
                 margin-left: auto;
                 margin-right: auto;
              }
              .QuestionHeader-footer-inner {
                 width: auto;
              }
              .QuestionHeader-footer-main {
                 padding-left: 0;
              }
              .QuestionHeader-main {
                 width: auto;
                 flex: 1;
              }
              .Question-main {
                 width: 75vw;
                 min-width: 1000px;
              }
              .Question-main .ListShortcut {
                 flex: 1;
              }
              .Question-mainColumn {
                 flex: 1;
                 width: auto;
                 padding-right: 10px;
              }
            `)
            Toast.info('已宽屏处理')
        }

        createWidescreenControl({ store, execute })
    })
    /* ===知乎===end */

    /* ===掘金===start */
    handlers.set('juejin', function() {
        const store = createStore('juejin')
        function execute() {
            GM_addStyle(`
              /* 掘金文章 */
              @media screen and (min-width: 1300px) {
                .main-container {
                   max-width: 75vw;
                }
                .main-container .main-area {
                   width: calc(100% - 21rem);
                }
              }
            `)
            Toast.info('已宽屏处理')
        }

        createWidescreenControl({ store, execute })
    })
    /* ===掘金===end */

    /* ===简书===start */
    handlers.set('jianshu', function() {
        const store = createStore('jianshu')
        function execute() {
            GM_addStyle(`
              /* 简书文章 */
              @media screen and (min-width: 1250px) {
                [role=main] > div:first-child {
                   flex: 1;
                   width: auto;
                }
              }
              @media screen and (min-width: 1250px) {
                [role=main] {
                   width: 85vw;
                }
                #__next > div:last-child {
                   left: 30px;
                }
              }
              @media screen and (min-width: 1450px) {
                [role=main] {
                   width: 75vw;
                }
                #__next > div:last-child {
                   left: 7vw;
                }
              }
            `)
            Toast.info('已宽屏处理')
        }

        createWidescreenControl({ store, execute })
    })
    /* ===简书===end */

    /* ===贴吧===start */
    handlers.set('tieba', function() {
        const store = createStore('tieba')
        function execute() {
            GM_addStyle(`
              /* 帖子 */
              @media screen and (min-width: 1390px) {
                #container {
                   width: 70vw;
                }
                #container > .content {
                   width: 100%;
                }
                .nav_wrap, .p_thread, .pb_content, .core_title_wrap_bright, .l_post_bright, .core_reply_wrapper, .l_post_bright .core_reply_wrapper, .pb_footer {
                   width: 100%;
                }
                /* 内容区域 */
                .pb_content {
                   display: flex;
                   background-size: 100%;
                }
                .pb_content::after {
                   content: none;
                }
                /* 楼区域 */
                .left_section {
                   flex: 1;
                   border-right: 2px solid #e4e6eb;
                }
                /* 楼层 */
                .l_post_bright {
                   display: flex;
                }
                .l_post_bright .d_post_content_main{
                   width: auto;
                   flex: 1;
                }
                /* 右侧悬浮按钮 */
                .tbui_aside_float_bar {
                   left: auto;
                   right: 11vw;
                   margin-left: 0;
                }
              }
            `)
            Toast.info('已宽屏处理')
        }

        createWidescreenControl({ store, execute })
    })
    /* ===贴吧===end */

    /* ===贴吧主页===start */
    handlers.set('tiebaMain', function() {
        const store = createStore('tiebaMain')
        const jQuery = unsafeWindow.jQuery
        const $moreforumEl = jQuery('#moreforum')
        // 模拟的app版本
        const fakeVersion = '11.8.8.0'
        // 未登录时删除已有的BDUSS
        if (!$moreforumEl.length) {
            delete store.BDUSS
            return
        }

        const ui = new Vue({
            template: `
               <div>
                 <div style="position:fixed; z-index:500; top:80px; right:150px;">
                   <button
                      style="padding:10px; font-size:14px; color:#fff; background:#3385ff; box-shadow:0 1px 6px rgba(0,0,0,.2);"
                      :disabled="loading"
                      @click="run"
                   >
                     一键签到
                   </button>
                   <p style="margin-top:10px; text-align:center;" title="模拟APP签到可以获得与APP相同的经验,比网页签到经验更多,也提供更多功能,但需要BDUSS,重新登录后需要再次输入,请网上搜索获得方法,不勾选则通过网页签到,此时不需要BDUSS">
                     <input style="vertical-align:text-top;" v-model="isSimulate" type="checkbox" @change="simulateChange" /> 模拟APP
                   </p>
                   <p style="text-align:center;" title="下次进入贴吧时自动签到,建议同时勾选模拟APP">
                     <input style="vertical-align:text-top;" v-model="isComplete" type="checkbox" /> 自动签到
                   </p>
                 </div>
                 <div style="position:fixed; z-index:2; top:200px; right:10px; width:19vw; min-width:280px; box-shadow:0 1px 6px rgba(0,0,0,.2); background:#fafafa; padding:5px;" v-if="likeForums.length">
                    <button style="display:block; text-align:center; width:100%;" @click="reverseChange">{{isReverse ? '已倒序' : '普通'}}</button>
                    <ul style="max-height:65vh; overflow-x:hidden;">
                      <li style="display:flex; border-bottom:1px solid rgba(221, 221, 221, .5);" v-for="item in diaplayForums" :key="item.forum_id">
                        <span style="width:56px;" :title="item.level_name">{{item.user_level}}级{{item.is_sign ? ' √' : ''}}{{item.sign_bonus_point ? ('+' + item.sign_bonus_point) : ''}}</span>
                        <a style="flex:1; overflow:hidden; white-space:nowrap; text-overflow:ellipsis;" :href="'/f?kw=' + item.forum_name" :title="item.forum_name" target="_blank">{{item.forum_name}}</a>
                        <span style="width:80px" :title="'距离升级' + (item.levelup_score - item.user_exp)">{{item.user_exp}}/{{item.levelup_score}}</span>
                      </li>
                    </ul>
                  </div>
               </div>
             `,
            data() {
                return {
                    loading: false,
                    isSimulate: false,
                    isReverse: store.is_reverse || false,
                    likeForums: [],
                }
            },
            computed: {
                isComplete: {
                    get(){
                        return store.is_complete || false
                    },
                    set(val) {
                        store.is_complete = val
                    },
                },
                diaplayForums() {
                    const { isReverse, likeForums } = this
                    return isReverse ? Object.freeze([...likeForums].reverse()) : likeForums
                },
            },
            created() {
                if (store.is_simulate && store.BDUSS) {
                    this.isSimulate = true
                }
                if (this.isComplete) {
                    this.run()
                }
            },
            methods:{
                run() {
                    this.loading = true
                    ;(this.isSimulate ? runByBDUSS : runByWeb)(this).finally(() => {
                        this.loading = false
                    })
                },
                simulateChange({ target: { checked } }) {
                    store.is_simulate = checked
                    if (!checked) return

                    const { BDUSS } = store
                    const result = window.prompt("请输入F12->Application->Cookies中的BDUSS", BDUSS ? BDUSS : undefined)
                    if (result) {
                        store.BDUSS = result
                    } else {
                        this.$nextTick(() => {
                            this.isSimulate = false
                            store.is_simulate = false
                        })
                    }
                },
                reverseChange() {
                    this.isReverse = !this.isReverse
                    store.is_reverse = this.isReverse
                },
                setLikeForums(forums) {
                    this.likeForums = Object.freeze([...forums])
                },
                updateLikeForum(fid, forum) {
                    const { likeForums } = this
                    const index = likeForums.findIndex(item => +fid === +item.forum_id)
                    if (index === -1) return
                    const target = {
                        ...likeForums[index],
                        ...forum,
                        is_sign: true,
                    }
                    if (forum.sign_bonus_point) {
                        target.user_exp = Number(target.user_exp) + Number(forum.sign_bonus_point)
                    }
                    const ectype = [...likeForums]
                    ectype.splice(index, 1, target)
                    this.likeForums = Object.freeze(ectype)
                },
                // 未签到的靠前
                checkUnsign() {
                    const ectype = [...this.likeForums]
                    ectype.sort((a, b) => {
                        if (!a.is_sign && b.is_sign) return -1
                        return 0
                    })
                    this.likeForums = Object.freeze(ectype)
                },
            },
        }).$mount()
        document.body.appendChild(ui.$el)

        // 模拟APP参数
        function makeFakeParams(obj) {
            return Object.assign({
                // 以下可选参数 为了模拟更加真实
                _client_type: 4, // 不要更改
                _client_version: fakeVersion,
                _phone_imei: '0'.repeat(15),
                model: 'HUAWEI P40', // HUAWEI加油 ヾ(◍°∇°◍)ノ゙
                net_type: 1,
                stErrorNums: 1,
                stMethod: 1,
                stMode: 1,
                stSize: 320,
                stTime: 117,
                stTimesNum: 1,
                timestamp: Date.now(),
            }, obj)
        }

        // 贴吧参数签名函数 isFake true时会加入模拟APP参数
        function signature(payload, isFake = true) {
            if (isFake) {
                payload = makeFakeParams(payload)
            }
            // 提交内容所有name-value按照name的字典序升序排列
            const sortKeys = Object.keys(payload).sort()
            // 所有内容按照key=value拼接
            let str = sortKeys.reduce((acc, key) => (acc += `${key}=${payload[key]}`), '')
            // 拼接后补充
            str += 'tiebaclient!!!'
            // 最后以UTF-8编码进行MD5
            return MD5(str)
        }

        // 界面上无法获得失效的贴吧,这里调用接口获取所有关注的贴吧
        async function getLikeForums() {
            const { BDUSS } = store
            const tbs = unsafeWindow.PageData && unsafeWindow.PageData.tbs
            const req2 = makeFakeParams({
                BDUSS,
                tbs,
            })
            const [ like1, like2Map ] = await Promise.all([
                request.post('/mo/q/newmoindex').then(response => response.json()).then(data => data.data.like_forum),
                GMRequest.post('http://c.tieba.baidu.com/c/f/forum/like', utils.URL.stringify({
                    ...req2,
                    sign: signature(req2),
                }), {
                    headers: {
                        'User-agent': `bdtb for Android ${fakeVersion}`,
                        'Accept': '',
                        'Content-Type': 'application/x-www-form-urlencoded',
                        'Accept-Encoding': 'gzip',
                        'Cookie': 'ka=open',
                    }
                }).then(data => data.forum_list).then(forum_list => forum_list.reduce((acc, val) => (acc[val.id] = val, acc), {})),
            ])

            // 融合数据
            like1.forEach(forum => {
                const { forum_id } = forum
                const like2Forum = like2Map[forum_id]
                if (!like2Forum) return
                Object.assign(forum, {
                    levelup_score: like2Forum.levelup_score,
                    level_name: like2Forum.level_name,
                    slogan: like2Forum.slogan,
                })
            })
            // 经验降序
            like1.sort((a, b) => b.user_exp - a.user_exp)
            return like1
        }

        if (store.BDUSS) {
            getLikeForums().then(ui.setLikeForums).then(ui.checkUnsign)
        }

        // 通过BDUSS签到 获得经验与客户端签到相同
        async function runByBDUSS(ui) {
            // 贴吧必须先触发才能获取剩下贴吧
            $moreforumEl.trigger(new MouseEvent('mouseenter'))
            // 侧边元素
            const likeUnsignEls = $$('#likeforumwraper .unsign')
            // 查看更多元素
            const alwayUnsignEls = $$('#alwayforum-wraper .unsign')
            // 关闭面板
            $moreforumEl.trigger(new Event('click'))
            const allUnsignEls = [...likeUnsignEls, ...alwayUnsignEls]
            // 需要重新签到元素(失败时尝试重签)
            const resignEls = []
            if (!allUnsignEls.length) {
                Toast.success('所有贴吧已经签到')
                return
            }
            const toast = Toast.info({
                content: '开始签到,请等待',
                duration: 0,
            })

            // 签到
            function doSign(data) {
                const { BDUSS } = store
                const { tbs, fid, kw } = data
                const params = makeFakeParams({
                    // 以下4个参数 + 下面sign参数 是必选的
                    BDUSS,
                    tbs,
                    fid,
                    kw,
                })

                return GMRequest.post('http://c.tieba.baidu.com/c/c/forum/sign', utils.URL.stringify({
                    ...params,
                    sign: signature(params),
                }), {
                    headers: {
                        'User-agent': `bdtb for Android ${fakeVersion}`,
                        'Accept': '',
                        'Content-Type': 'application/x-www-form-urlencoded',
                        'Accept-Encoding': 'gzip',
                        'Cookie': 'ka=open',
                    }
                })
            }

            const tbs = unsafeWindow.PageData && unsafeWindow.PageData.tbs
            while(allUnsignEls.length) {
                const current = allUnsignEls.shift()
                const { kw } = utils.URL.parse(current.href)
                const { fid } = current.dataset
                const { error_code, error, error_msg, user_info } = await doSign({ tbs, kw, fid })
                // 贴吧成功码为0 还会出现code为0但error的情况
                if (error_code === '0' && !error) {
                    ui.updateLikeForum(fid, user_info)
                    // 替换已签到样式
                    current.classList.replace('unsign', 'sign')
                } else {
                    toast.close()
                    Toast.error(`签到终止,${error_msg || (error ? (error.errmsg || error.usermsg) : '')}`)
                    // 重签
                    resignEls.push(current)
                }
                // 客户端签到可以将延时缩短,垃圾贴吧,随机延时一下 50ms以上
                const ms = parseInt(Math.random() * 50 + 50)
                await utils.sleep(ms)
            }

            let failCount = 0

            // 重签
            while(resignEls.length) {
                const current = resignEls.shift()
                const { kw } = utils.URL.parse(current.href)
                const { fid } = current.dataset
                const { error_code, error, user_info } = await doSign({ tbs, kw, fid })
                if (error_code === '0' && !error) {
                    ui.updateLikeForum(fid, user_info)
                    current.classList.replace('unsign', 'sign')
                } else {
                    failCount++
                    Toast.error(`${decodeURIComponent(kw)} 签到失败`)
                }
                await utils.sleep(500)
            }

            toast.close()
            failCount
                ? Toast.warning({
                content: `签到成功,失败${failCount}个`,
                duration: 0,
            })
            : Toast.success('签到成功')
            ui.checkUnsign()
        }


        // 网页签到 经验没客户端那么多 但不需要获得BDUSS只需贴吧已登录即可
        async function runByWeb() {
            // 贴吧必须先触发才能获取剩下贴吧
            $moreforumEl.trigger(new MouseEvent('mouseenter'))
            // 侧边元素
            const likeUnsignEls = $$('#likeforumwraper .unsign')
            // 查看更多元素
            const alwayUnsignEls = $$('#alwayforum-wraper .unsign')
            // 关闭面板
            $moreforumEl.trigger(new Event('click'))
            const allUnsignEls = [...likeUnsignEls, ...alwayUnsignEls]
            // 需要重新签到元素(失败时尝试重签)
            const resignEls = []
            if (!allUnsignEls.length) {
                Toast.success('所有贴吧已经签到')
                return
            }
            const toast = Toast.info({
                content: '开始签到,请等待',
                duration: 0,
            })
            // 签到
            function doSign(data) {
                return request.post('/sign/add', {
                    ie: 'utf-8',
                    ...data,
                }, {
                    headers: {
                        'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
                    }
                }).then(response => response.json())
            }
            while(allUnsignEls.length) {
                const current = allUnsignEls.shift()
                const { kw } = utils.URL.parse(current.href)
                const { no } = await doSign({ kw })
                // 贴吧成功码为0
                if (no === 0) {
                    // 替换已签到样式
                    current.classList.replace('unsign', 'sign')
                } else {
                    // 重签
                    resignEls.push(current)
                }
                // 网页签到不能太短,否则很容易出现验证码(ಥ﹏ಥ) 验证码2150040
                const ms = parseInt(Math.random() * 500 + 500)
                await utils.sleep(ms)
            }

            let failCount = 0
            // 重签
            while(resignEls.length) {
                const current = resignEls.shift()
                const { kw } = utils.URL.parse(current.href)
                const { no } = await doSign({ kw })
                if (no === 0) {
                    current.classList.replace('unsign', 'sign')
                } else {
                    failCount++
                    Toast.error(`${decodeURIComponent(kw)} 签到失败`)
                }
                await utils.sleep(500)
            }

            toast.close()
            failCount
                ? Toast.warning({
                content: `签到成功,失败${failCount}个`,
                duration: 0,
            })
            : Toast.success('签到成功')
        }
    })
    /* ===贴吧主页===end */

    // GM请求
    function GMRequest(url, options) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                ...options,
                url,
                onload(res) {
                    try {
                        resolve(JSON.parse(res.response))
                    } catch (e) {
                        resolve(res.response)
                    }
                },
                onerror: reject,
            });
        })
    }
    GMRequest.post = function(url, data, options) {
        return GMRequest(url, {
            ...options,
            data,
            method: 'POST',
        })
    }

    // 请求
    function request(url, options) {
        return fetch(url, {
            ...options,
        })
    }
    request.post = function(url, data, options = {}) {
        options.headers = Object.assign({}, options.headers)
        if (data) {
            let body = data
            if (options.headers['Content-Type'].includes('application/x-www-form-urlencoded') && Object.prototype.toString.call(data) === '[object Object]') {
                body = utils.URL.stringify(data)
            }
            if (options.headers['Content-Type'].includes('application/json') && Object.prototype.toString.call(data) === '[object Object]') {
                body = JSON.stringify(data)
            }
            options.body = body
        }

        return request(url, {
            ...options,
            method: 'POST',
        })
    }

    // 存储 以网站作为模块
    function createStore(sitename) {
        if (!sitename) throw new TypeError('缺少sitename,期望<string>')
        const getRealProp = property => `${sitename}_${property}`
        const target = {}
        const handler = {
            get(target, property) {
                const realProp = getRealProp(property)
                let value = target[realProp]
                if (value == null) {
                    value = GM_getValue(realProp)
                    target[realProp] = value
                }
                return value
            },
            set(target, property, value) {
                const realProp = getRealProp(property)
                target[realProp] = value
                GM_setValue(realProp, value)
                return true
            },
            deleteProperty(target, property) {
                const realProp = getRealProp(property)
                const deleted = delete target[realProp]
                GM_deleteValue(realProp)
                return deleted
            },
        }
        const store = new Proxy(target, handler)
        return store
    }

    // 工具
    const utils = {
        // url解析
        URL: {
            parse() {},
            stringify() {},
        },
        // 转formdata
        toFormData() {},
        // 延时
        async sleep() {},
    }

    utils.URL.parse = function(string) {
        const url = new URL(string)
        const searchParams = new URLSearchParams(url.search)
        return [...searchParams.entries()].reduce((acc, [key, value]) => (acc[key] = value, acc), {})
    }
    utils.URL.stringify = function(obj) {
        return Object.entries(obj).map(([key, value]) => `${key}=${value}`).join('&')
    }
    utils.toFormData = function(params = {}) {
        const formData = new FormData()
        for (const [key, value] of Object.entries(params)) {
            formData.append(key, value)
        }
        return formData
    }
    utils.sleep = ms => new Promise(resolve => setTimeout(resolve, ms))

    // Toast 可以直接Toast[type] 调用
    function Toast(options) {
        if (typeof options === 'string') {
            options = { content: options }
        }
        // 参数
        options = Object.assign({
            content: '',
            type: 'info',
            duration: 3000, // 0不会自动关闭
        }, options)

        const toast = new Vue({
            template: `
               <transition name="fade" appear @before-enter="beforeEnter" @enter="enter" @leave="leave" @after-leave="afterLeave">
                 <div
                   :style="colour"
                   style="position:fixed; z-index:99999; top:80px; left:50%; padding:8px 16px; font-size:14px; box-shadow:0 2px 3px rgba(0,0,0,.1); transition:all .3s ease-in-out;"
                   v-if="visible"
                   v-html="content"
                 >
                 </div>
               </transition>
             `,
            data() {
                return {
                    content: options.content,
                    type: options.type,
                    visible: true,
                }
            },
            computed: {
                // 颜色
                colour() {
                    switch(this.type) {
                        case 'info':
                            return {
                                color: '#2e8bf0',
                                background: '#f0faff',
                                border: '1px solid #d4eeff',
                            }
                        case 'success':
                            return {
                                color: '#19bf6c',
                                background: '#edfff3',
                                border: '1px solid #bbf2cf',
                            }
                        case 'warning':
                            return {
                                color: '#f90',
                                background: '#fff9e6',
                                border: '1px solid #ffe7a3',
                            }
                        case 'error':
                            return {
                                color: '#ed3f13',
                                background: '#ffefe6',
                                border: '1px solid #ffcfb8',
                            }
                    }
                }
            },
            methods: {
                // export-api
                // 关闭
                close() {
                    this.visible = false
                },
                beforeEnter(el) {
                    el.style.opacity = 0
                    el.style.transform = 'translate(-50%, -10%)'
                },
                enter(el, done) {
                    setTimeout(() => {
                        el.style.opacity = 1
                        el.style.transform = 'translate(-50%, 0)'
                    })
                },
                leave(el, done) {
                    setTimeout(() => {
                        el.style.opacity = 0
                        el.style.transform = 'translate(-50%, 30%)'
                    })
                },
                afterLeave () {
                    this.$destroy()
                    this.$el.parentNode.removeChild(this.$el)
                },
            },
        }).$mount()
        document.body.appendChild(toast.$el)

        if (options.duration > 0) {
            setTimeout(() => {
                toast.visible = false
            }, options.duration)
        }

        return {
            // 关闭
            close: toast.close,
        }
    }
    ['info', 'success', 'warning', 'error'].forEach(type => {
        Toast[type] = function(options) {
            if (typeof options === 'string') {
                options = { content: options }
            }
            options = {
                ...options,
                type,
            }
            return Toast(options)
        }
    })

    // 宽屏开关 options: store<store>, execute要执行的函数
    function createWidescreenControl(options) {
        const { store, execute } = options
        const buttonComponent = new Vue({
            template: `
              <button
                style="position:fixed; z-index:99; top:150px; right:150px; border:none; color:#fff; padding:6px 12px; font-size:14px; background:#3385ff; box-shadow:0 1px 6px rgba(0,0,0,.2);"
                title="注意:页面会被刷新"
                @click="toggle"
              >
               {{ isOpen ? '已开启' : '关闭' }}
              </button>
            `,
            data() {
                return {
                    isOpen: store.is_open || false,
                }
            },
            beforeCreate() {
                store.is_open && execute()
            },
            methods: {
                async toggle() {
                    store.is_open = !this.isOpen
                    location.reload()
                }
            },
        }).$mount()
        document.body.appendChild(buttonComponent.$el)
    }

    main()

})();