蓝湖 lanhu

自动填充填写过的产品密码(不是蓝湖账户);查看打开过的项目;查看产品页面窗口改变后帮助侧边栏更新高度

目前为 2020-10-17 提交的版本。查看 最新版本

// ==UserScript==
// @name         蓝湖 lanhu
// @version      1.5.0
// @description  自动填充填写过的产品密码(不是蓝湖账户);查看打开过的项目;查看产品页面窗口改变后帮助侧边栏更新高度
// @author       sakura-flutter
// @namespace    https://github.com/sakura-flutter/tampermonkey-scripts/commits/master/lanhu/index.js
// @license      GPL-3.0
// @compatible   chrome >= 80
// @compatible   firefox >= 75
// @noframes
// @match        https://lanhuapp.com/web/
// @grant        unsafeWindow
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addValueChangeListener
// @grant        GM_addStyle
// @grant        GM_setClipboard
// @require      https://gf.qytechs.cn/scripts/411093-toast/code/Toast.js?version=846237
// ==/UserScript==

/* global Vue Toast */

(function() {
  'use strict'
  const $ = document.querySelector.bind(document)
  const marks = new WeakSet()

  function main() {
    const app = $('.whole').__vue__
    const recorder = createRecorder()

    fixBarHeight()
    app.$watch('$route', function(to, from) {
      // 无法知道页面是否渲染完毕,延时处理
      setTimeout(autofillPassword, 500)
      // 蓝湖title是动态获取的,可能有延时,延时处理
      setTimeout(recorder.record, 500)
    }, { immediate: true })
  }

  /* 填充密码 */
  function autofillPassword() {
    if (!location.hash.startsWith('#/item/project/door')) return
    const queryString = location.hash.includes('?') ? location.hash.split('?')[1] : ''
    if (!queryString) return
    const pid = new URLSearchParams(queryString).get('pid')
    // 确认登录(不可用)按钮   密码框
    const [confirmEl, passwordEl] = [
      $('#project-door .mu-raised-button-wrapper'),
      $('#project-door .pass input'),
    ]
    if (!pid || !confirmEl || !passwordEl) return

    const pidPassword = GM_getValue('passwords', {})[pid]
    if (pidPassword) {
      passwordEl.value = pidPassword
      Toast('密码已填写')
      confirmEl.click()
    }

    // 标记已添加事件的元素
    if (marks.has(confirmEl)) return
    marks.add(confirmEl)

    // 点击后保存密码
    confirmEl.addEventListener('mousedown', savePassword)
    // 回车键保存密码
    passwordEl.addEventListener('keydown', event => {
      if (event.keyCode !== 13) return
      savePassword()
    })

    function savePassword() {
      const savedPassword = GM_getValue('passwords', {})
      const password = passwordEl.value
      GM_setValue('passwords', {
        ...savedPassword,
        [pid]: password,
      })
    }
  }

  /* 更新侧边栏高度 */
  function fixBarHeight() {
    window.addEventListener('resize', throttle(function() {
      if (!location.hash.startsWith('#/item/project/product')) return
      const barEl = $('.flexible-bar')
      const modalEl = $('.flexible-modal')
      if (!barEl || !modalEl) return
      barEl.dispatchEvent(new MouseEvent('mousedown'))
      modalEl.dispatchEvent(new MouseEvent('mouseup'))
    }, 150))
  }

  /* 记录看过的产品 */
  function createRecorder() {
    const ui = new Vue({
      template: `
        <article id="inject-recorder-ui" @mouseenter="toggle(true)" @mouseleave="toggle(false);toggleMoreActions(false)">
          <transition name="inject-slide-fade">
            <transition-group v-show="reversed.length && (unhidden || recordsVisible)" :class="{'more-actions': moreActionsVisible}" tag="ul" name="inject-slide-hor-fade">
              <li v-for="item in reversed" :key="item.pid">
                <a :href="getHref(item)" :title="item.title" target="_blank">{{item.title}}</a>
                <div class="actions" @mouseenter="toggleMoreActions(true)">
                  <button title="移除" @click="deleteItem(item)">×</button>
                  <button v-show="moreActionsVisible" title="左击复制链接和密码;右击复制密码" @click="copy('all', item)" @contextmenu.prevent="copy('pwd', item)">
                    <svg t="1602929080634" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4117" width="10" height="10"><path d="M877.714286 0H265.142857c-5.028571 0-9.142857 4.114286-9.142857 9.142857v64c0 5.028571 4.114286 9.142857 9.142857 9.142857h566.857143v786.285715c0 5.028571 4.114286 9.142857 9.142857 9.142857h64c5.028571 0 9.142857-4.114286 9.142857-9.142857V36.571429c0-20.228571-16.342857-36.571429-36.571428-36.571429zM731.428571 146.285714H146.285714c-20.228571 0-36.571429 16.342857-36.571428 36.571429v606.514286c0 9.714286 3.885714 18.971429 10.742857 25.828571l198.057143 198.057143c2.514286 2.514286 5.371429 4.571429 8.457143 6.285714v2.171429h4.8c4 1.485714 8.228571 2.285714 12.571428 2.285714H731.428571c20.228571 0 36.571429-16.342857 36.571429-36.571429V182.857143c0-20.228571-16.342857-36.571429-36.571429-36.571429zM326.857143 905.371429L228.457143 806.857143H326.857143v98.514286zM685.714286 941.714286H400V779.428571c0-25.257143-20.457143-45.714286-45.714286-45.714285H192V228.571429h493.714286v713.142857z" p-id="4118"></path></svg>
                  </button>
                </div>
              </li>
            </transition-group>
          </transition>
          <div style="text-align: center; padding-top: 8px;">
            <button class="view-btn">打开最近项目</button>
            <input style="margin-left: 6px;vertical-align:text-top;" v-model="unhidden" type="checkbox" title="固定显示" @change="unhiddenChange" />
          </div>
        </article>
      `,
      data() {
        return {
          records: GM_getValue('records', []),
          recordsVisible: false,
          moreActionsVisible: false,
          unhidden: GM_getValue('unhidden', false),
        }
      },
      computed: {
        reversed() {
          return [...this.records].reverse()
        },
      },
      created() {
        GM_addValueChangeListener('records', (name, oldVal, newVal) => {
          this.records = Object.freeze(newVal)
        })
        GM_addValueChangeListener('unhidden', (name, oldVal, newVal) => {
          this.unhidden = newVal
        })
      },
      methods: {
        getHref(item) {
          if (item.href) return item.href
          // 兼容旧版本
          const PATH = 'https://lanhuapp.com/web/'
          return PATH + item.hash + '?' + item.queryString
        },
        deleteItem(item) {
          const newRecords = [...this.records]
          newRecords.find((record, index) => {
            if (record.pid === item.pid) {
              newRecords.splice(index, 1)
              return true
            }
          })
          GM_setValue('records', newRecords)
        },
        copy(action, item) {
          let copyString = ''
          const password = GM_getValue('passwords', {})[item.pid]
          if (action === 'all') {
            const href = this.getHref(item)
            copyString += `${item.title}`
            password && (copyString += ` (密码:${password})`)
            copyString += `\n${href}`
          } else if (action === 'pwd') {
            if (password) {
              copyString += password
            } else {
              Toast.warning('没有密码!')
            }
          }

          if (!copyString) return
          GM_setClipboard(copyString, 'text')
          Toast.success('复制成功')
        },
        toggle(visible) {
          this.recordsVisible = visible
        },
        toggleMoreActions(visible) {
          this.moreActionsVisible = visible
        },
        unhiddenChange() {
          GM_setValue('unhidden', this.unhidden)
        },
      },
    }).$mount()
    document.body.appendChild(ui.$el)

    /* 记录函数 */
    function record() {
      const queryString = location.hash.includes('?') ? location.hash.split('?')[1] : ''
      if (!queryString) return
      const pid = new URLSearchParams(queryString).get('pid')
      if (!pid) return

      const records = GM_getValue('records', [])
      let oldTitle = null
      records.find((item, index) => {
        if (item.pid === pid) {
          oldTitle = item.title
          records.splice(index, 1)
          return true
        }
      })
      // 优化标题显示:当前是无意义标题且有旧标题时优先使用旧标题
      const title = (['蓝湖', '...'].includes(document.title) && oldTitle) ? oldTitle : document.title
      records.push({
        pid,
        title,
        href: location.href,
      })
      GM_setValue('records', records)
    }

    // 添加样式
    GM_addStyle(`
      |> {
          position: fixed;
          right: 1.5vw;
          bottom: 8vh;
          z-index: 1000;
          width: 240px;
          padding: 30px 30px 10px;
          opacity: .5;
          transition: opacity .1s;
      }
      |>:hover {
          opacity: 1;
      }
      |> ul::-webkit-scrollbar {
          width: 8px;
          height: 8px;
          background: #f2f2f2;
          padding-right: 2px;
      }
      |> ul::-webkit-scrollbar-thumb {
          border-radius: 3px;
          border: 0;
          background: #b4bbc5;
      }
      |> ul.more-actions {
          width: 204px;
      }
      |> ul {
          width: 180px;
          padding: 5px;
          max-height: 40vh;
          overflow-x: hidden;
          background: rgb(251, 251, 251);
          box-shadow: 0 1px 6px rgba(0,0,0,.15);
          transition: width .1s;
      }
      |> li {
          display: flex;
          align-items: center;
          padding: 0 5px;
          transition: all .3s, background 0.1s ease-out;
      }
      |> li:hover {
          background: rgba(220, 237, 251, 0.64);
      }
      |> li a {
          width: 132px;
          flex: none;
          line-height: 30px;
          overflow: hidden;
          text-overflow: ellipsis;
          white-space: nowrap;
      }
      |> li .actions {
          flex: 1 0 auto;
      }
      |> li button {
          width: 20px;
          line-height: 20px;
          border: none;
          border-radius: 50%;
          color: #ababab;
          box-shadow: 0 1px 1px rgba(0,0,0,.15);
          background: #fff;
          cursor: pointer;
      }
      |> .view-btn {
          padding: 4px 12px;
          color: #fff;
          background: #3385ff;
          box-shadow:0 1px 6px rgba(0,0,0,.2);
          border: none;
          border-radius: 2px;
      }
      |> svg {
          fill: currentColor;
      }

      /* 动画1 */
      |> .inject-slide-fade-enter-active, |> .inject-slide-fade-leave-active {
          transition: all .1s;
      }
      |> .inject-slide-fade-enter, |> .inject-slide-fade-leave-to {
          transform: translateY(5px);
          opacity: 0;
      }
      /* 动画2 group */
      |> .inject-slide-hor-fade-move {
          transition: all .8s;
      }
      |> .inject-slide-hor-fade-enter, |> .inject-slide-hor-fade-leave-to {
          opacity: 0;
          transform: translateX(30px);
      }
      |> .inject-slide-hor-fade-active {
          position: absolute;
      }
    `.replace(/\|>/g, '#inject-recorder-ui'))

    return {
      record,
    }
  }

  function throttle(fn, delay) {
    var t = null
    var begin = new Date().getTime()

    return function(...args) {
      var _self = this
      var cur = new Date().getTime()

      clearTimeout(t)

      if (cur - begin >= delay) {
        fn.apply(_self, args)
        begin = cur
      } else {
        t = setTimeout(function() {
          fn.apply(_self, args)
        }, delay)
      }
    }
  }

  main()
})()

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址