// ==UserScript== // @name 蓝湖 lanhu // @version 1.7.0 // @description 自动填充填写过的产品密码(不是蓝湖账户);查看打开过的项目;查看产品页面窗口改变后帮助侧边栏更新高度 // @author sakura-flutter // @namespace https://github.com/sakura-flutter/tampermonkey-scripts/commits/master/src/lanhu/index.js // @license GPL-3.0 // @compatible chrome >= Latest // @compatible firefox >= Latest // @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://cdn.jsdelivr.net/npm/vue@3.0.2/dist/vue.runtime.global.js // @require https://greasyfork.org/scripts/411093-toast/code/Toast.js?version=862073 // @downloadURL none // ==/UserScript== /******/ (() => { // webpackBootstrap /******/ "use strict"; // CONCATENATED MODULE: external "Vue" const external_Vue_namespaceObject = Vue; // CONCATENATED MODULE: ./src/utils/index.js /** * 解析url上的参数 * @param {string} href 或 带有参数格式的string;有search则不再hash * @return {object} */ function parseURL(href = location.href) { if (!href) return {}; let search; try { // 链接 const url = new URL(href); ({ search } = url); // 主要处理对hash的search if (!search && url.hash.includes('?')) { search = url.hash.split('?')[1]; } } catch { // 非链接,如:a=1&b=2、?a=1、/foo?a=1、/foo#bar?a=1 if (href.includes('?')) { search = href.split('?')[1]; } else { search = href; } } const searchParams = new URLSearchParams(search); return [...searchParams.entries()].reduce((acc, [key, value]) => (acc[key] = value, acc), {}); } function stringifyURL(obj) { return Object.entries(obj).map(([key, value]) => `${key}=${value}`).join('&'); } 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); } }; } function once(fn) { let called = false; return function (...args) { if (called === false) { called = true; return fn.apply(this, args); } }; } /** * 有些脚本是在document-start执行的,安全地获得document * @param {fn} cb */ function documentLoaded(cb) { document.body ? cb() : window.addEventListener('DOMContentLoaded', cb); } /** * 延时 * @param {number} ms 毫秒数 */ const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); function toFormData(params = {}) { const formData = new FormData(); for (const [key, value] of Object.entries(params)) { formData.append(key, value); } return formData; } // CONCATENATED MODULE: ./src/composition/use-gm-value.js /** * 同GM_getValue且在生命周期内自动GM_addValueChangeListener与GM_removeValueChangeListener,亦提供GM_setValue * 暂不提供GM_deleteValue * @param {string} name * @param {any} defaultValue */ function useGMvalue(name, defaultValue) { const state = (0,external_Vue_namespaceObject.reactive)({ value: GM_getValue(name, defaultValue), old: undefined, name }); (0,external_Vue_namespaceObject.onUnmounted)(() => { GM_removeValueChangeListener(id); }); const id = GM_addValueChangeListener(name, (name, oldVal, newVal) => { state.value = newVal; state.old = oldVal; }); function setValue(val) { GM_setValue(name, val); } return { ...(0,external_Vue_namespaceObject.toRefs)(state), setValue }; } // CONCATENATED MODULE: ./src/lanhu/index.js const $ = document.querySelector.bind(document); const marks = new WeakSet(); function main() { updateStorage(); fixBarHeight(); const app = $('.whole').__vue__; if (!app) { console.warn('蓝湖脚本:获取vue失败'); return; } const recorder = createRecorder(); app.$watch('$route', function (to, from) { autofillPassword(); // 蓝湖title是动态获取的,可能有延时,延时处理 setTimeout(recorder.record, 500); }, { immediate: true }); } /* 填充密码 */ function autofillPassword() { // 停止上次观察 autofillPassword.observer?.disconnect(); if (!location.hash.startsWith('#/item/project/door')) return; const { pid } = parseURL(); if (!pid) return; // 确认登录按钮 let confirmEl = null; // 密码框 let passwordEl = null; function savePassword() { const savedPassword = GM_getValue('passwords', {}); const password = passwordEl.value; GM_setValue('passwords', { ...savedPassword, [pid]: password }); } const observer = autofillPassword.observer = new MutationObserver((mutationsList, observer) => { let filled = false; // eslint-disable-next-line no-unused-vars for (const _ of mutationsList) { const [hasConfirmEl, hasPasswordEl] = [$('#project-door .mu-raised-button-wrapper'), $('#project-door .pass input')]; if (!hasConfirmEl || !hasPasswordEl) continue; observer.disconnect(); confirmEl = hasConfirmEl; passwordEl = hasPasswordEl; const pidPassword = GM_getValue('passwords', {})[pid]; // 确保本次内只进行一次操作 if (filled === false && pidPassword) { filled = true; passwordEl.value = pidPassword; Toast('密码已填写'); confirmEl.click(); } // 标记已添加事件的元素 if (marks.has(confirmEl)) break; marks.add(confirmEl); // 点击后保存密码 confirmEl.addEventListener('mousedown', savePassword); // 回车键保存密码 passwordEl.addEventListener('keydown', event => { if (event.keyCode !== 13) return; savePassword(); }); } }); observer.observe(document.body, { childList: true, subtree: true }); } /* 更新侧边栏高度 */ 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() { // eslint-disable-next-line no-unused-vars const { Transition, TransitionGroup } = Vue; const app = Vue.createApp({ render() { const { reversed, recordsVisible, unhidden, moreActionsVisible, toggle, toggleMoreActions, deleteItem, copy, onUnhiddenChange } = this; return (0,external_Vue_namespaceObject.createVNode)("article", { "id": "inject-recorder-ui", "onMouseenter": () => { toggle(true); }, "onMouseleave": () => { toggle(false); toggleMoreActions(false); } }, [(0,external_Vue_namespaceObject.createVNode)(Transition, { "name": "inject-slide-fade" }, { default: () => [(0,external_Vue_namespaceObject.withDirectives)((0,external_Vue_namespaceObject.createVNode)("div", null, [(0,external_Vue_namespaceObject.createVNode)(TransitionGroup, { "class": { 'more-actions': moreActionsVisible }, "tag": "ul", "name": "inject-slide-hor-fade", "appear": true }, { default: () => [reversed.map(item => (0,external_Vue_namespaceObject.createVNode)("li", { "key": item.pid }, [(0,external_Vue_namespaceObject.createVNode)("a", { "href": item.href, "title": item.title, "target": "_blank" }, [item.title]), (0,external_Vue_namespaceObject.createVNode)("div", { "class": "actions", "onMouseenter": () => { toggleMoreActions(true); } }, [(0,external_Vue_namespaceObject.createVNode)("button", { "title": "移除", "onClick": () => { deleteItem(item); } }, [(0,external_Vue_namespaceObject.createTextVNode)("\xD7")]), (0,external_Vue_namespaceObject.withDirectives)((0,external_Vue_namespaceObject.createVNode)("button", { "title": "左击复制链接和密码;右击复制密码", "onClick": () => { copy('all', item); }, "onContextmenu": event => { event.preventDefault(); copy('pwd', item); } }, [(0,external_Vue_namespaceObject.createVNode)("svg", { "t": "1602929080634", "viewBox": "0 0 1024 1024", "version": "1.1", "xmlns": "http://www.w3.org/2000/svg", "p-id": "4117", "width": "10", "height": "10" }, [(0,external_Vue_namespaceObject.createVNode)("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" }, null)])]), [[external_Vue_namespaceObject.vShow, moreActionsVisible]])])]))] })]), [[external_Vue_namespaceObject.vShow, reversed.length && (unhidden || recordsVisible)]])] }), (0,external_Vue_namespaceObject.createVNode)("div", { "class": "control" }, [(0,external_Vue_namespaceObject.createVNode)("button", { "class": "view-btn" }, [(0,external_Vue_namespaceObject.createTextVNode)("\u6253\u5F00\u6700\u8FD1\u9879\u76EE")]), (0,external_Vue_namespaceObject.createVNode)("input", { "checked": this.unhidden, "type": "checkbox", "title": "固定显示", "onChange": onUnhiddenChange }, null)])]); }, setup() { const { toRefs, reactive, computed } = Vue; const state = reactive({ recordsVisible: false, moreActionsVisible: false }); const { value: records, setValue: setRecords } = useGMvalue('records', []); const { value: unhidden, setValue: setUnhidden } = useGMvalue('unhidden', false); const reversed = computed(() => [...records.value].reverse()); function deleteItem(item) { const newRecords = [...records.value]; newRecords.find((record, index) => { if (record.pid === item.pid) { newRecords.splice(index, 1); return true; } }); setRecords(newRecords); } function copy(action, item) { let copyString = ''; const password = GM_getValue('passwords', {})[item.pid]; if (action === 'all') { copyString += `${item.title}`; password && (copyString += ` (密码:${password})`); copyString += `\n${item.href}`; } else if (action === 'pwd') { if (password) { copyString += password; } else { Toast.warning('没有密码!'); } } if (!copyString) return; GM_setClipboard(copyString, 'text'); Toast.success('复制成功'); } function toggle(visible) { state.recordsVisible = visible; } function toggleMoreActions(visible) { state.moreActionsVisible = visible; } function onUnhiddenChange(event) { setUnhidden(event.target.checked); } return { ...toRefs(state), records, unhidden, reversed, deleteItem, copy, toggle, toggleMoreActions, onUnhiddenChange }; } }); const rootContainer = document.createElement('div'); app.mount(rootContainer); document.body.appendChild(rootContainer); /* 记录函数 */ function record() { const { pid } = parseURL(); 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; } |> li button:nth-of-type(n+2) { margin-left: 4px; } |> .control { display: flex; justify-content: center; align-items: center; padding-top: 8px; } |> .control input { margin-left: 6px; } |> .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-from, |> .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-from, |> .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 updateStorage() { const records = GM_getValue('records'); if (!records) return; let hasDiff = false; const newRecords = records.map(record => { if (record.href) return record; hasDiff = true; const PATH = 'https://lanhuapp.com/web/'; const { hash, queryString, ...rest } = record; return { href: PATH + hash + '?' + queryString, ...rest }; }); if (hasDiff) { GM_setValue('records', newRecords); } } main(); /******/ })() ;