您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
2024/11/25 11:09:18
// ==UserScript== // @name 按最近更新顺序排列 apifox 接口列表 // @namespace Violentmonkey Scripts // @match *://app.apifox.com/* // @require https://unpkg.com/vue@3/dist/vue.global.prod.js // @require https://unpkg.com/[email protected]/dayjs.min.js // @grant none // @version 1.0.1 // @license GPL // @author - // @description 2024/11/25 11:09:18 // ==/UserScript== window.Vue = Vue const { createApp, ref, reactive, computed, watchEffect } = Vue const styleStr = ` .sort-trigger { width: 30px; height: 30px; background-color: var(--app-bg-200); border-radius: 50%; position: fixed; top: 50%; right: 20px; transform: translateY(-50%); box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1); border: 1px solid #e0e0e0; display: flex; justify-content: center; align-items: center; cursor: pointer; } .sort-content { position: fixed; right: 10px; width: 360px; overflow: hidden; background: var(--app-bg-200); box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1); z-index: 1000; display: flex; flex-direction: column; } .sort-input { padding: 10px 15px; display: flex; gap: 10px; } .sort-input input { background: var(--app-bg-200); width: 100%; border: 1px solid #e0e0e0; border-radius: 5px; padding: 5px 10px; outline: none; } .sort-list { flex: 1; overflow: auto; } .sort-item { padding: 5px 15px; cursor: pointer; } .sort-close { padding: 5px 15px; cursor: pointer; text-align: center; border: 1px solid #e0e0e0; color: #666; border-radius: 5px; background: var(--app-bg-400); white-space: nowrap; } .sort-item-split { height: 1px; text-align: center; display: flex; align-items: center; justify-content: center; white-space: nowrap; gap: 10px; opacity: 0.5; } .sort-item-split::before, .sort-item-split::after { content: ''; width: 100%; height: 1px; background: #e0e0e0; } ` const enums = { urls: { apiDetails: /api\/v1\/api-details/, folders: /api\/v1\/projects\/[^/]*\/api-detail-folders/ }, doms: { container: document.createElement('div'), searchInput: ".ui-input.ui-input-variant-default", contentHolder: ".ui-tabs-content-holder" } } const globalState = reactive({ sortList: [], folders: [] }) // 保存原始的fetch函数 let originalFetch = fetch; // 自定义的fetch函数 async function customFetch(url, options) { // 发送原始的fetch请求,并等待响应 let response = await originalFetch(url, options); setTimeout(() => { interceptResponse(response) }, 1000) return response; } // 自定义的拦截响应数据的方法 async function interceptResponse(response) { // 这里假设响应数据是JSON格式,先进行解析 const list = [ [enums.urls.apiDetails, resolveApiDetails], [enums.urls.folders, resolveFolders] ] list.forEach(([url, action]) => { const link = new URL(response.url); if (link.pathname.match(url)) { action(response); } }) } async function resolveApiDetails(response) { const { data } = await response.json(); globalState.sortList = data.map(item => { return { name: item.name, id: item.id, path: item.path, method: item.method, folderId: item.folderId, updatedAt: dayjs(item.updatedAt).unix() } }).sort((a, b) => b.updatedAt - a.updatedAt) } async function resolveFolders(response) { const { data } = await response.json(); globalState.folders = data } function init() { document.body.appendChild(enums.doms.container); initStyle() createApp({ name: 'apifox-sort', setup() { syncContentHolder() const state = reactive({ visible: false, search: '', contentHolderBounds: { left: 0, top: 0, height: 0 }, getFolderName(folderId) { return globalState.folders.find(folder => folder.id === folderId)?.name }, getItemClass(item) { if (item.method === 'post') { return 'pui-g-ui-kit-request-method-icon-index-container text-orange-6 text-left block' } if (item.method === 'get') { return 'pui-g-ui-kit-request-method-icon-index-container text-green-6 text-left block' } return 'pui-g-ui-kit-request-method-icon-index-container text-orange-6 text-left block' }, renderList: computed(() => { if (!state.search) return globalState.sortList return globalState.sortList.filter(item => { const nameFilter = item.name.includes(state.search) const pathFilter = item.path.includes(state.search) return nameFilter || pathFilter }) }), splitIndex: computed(() => { const list = { sevenDays: 0, oneMonth: 0, } state.renderList.forEach((item, index) => { const isRecentSevenDays = dayjs().diff(dayjs(item.updatedAt * 1000), 'day') <= 7; if (isRecentSevenDays) { list.sevenDays = index + 1; } const isRecentOneMonth = dayjs().diff(dayjs(item.updatedAt * 1000), 'month') <= 1; if (isRecentOneMonth) { list.oneMonth = index + 1; } }); return list }), handleItemClick(item) { navigator.clipboard.writeText(item.path) const input = document.querySelector(enums.doms.searchInput) input.focus() }, getTime(t) { return dayjs(t * 1000).format('YYYY-MM-DD HH:mm:ss') } }) watchEffect(() => { console.log(state.splitIndex) }) const toggleSort = () => { state.visible = !state.visible } async function syncContentHolder() { const contentHolder = await domFinder(enums.doms.contentHolder) const bounds = contentHolder.getBoundingClientRect() state.contentHolderBounds = { top: bounds.top + 'px', height: bounds.height + 'px' } } return { state, toggleSort, globalState } }, template: ` <div class="sort-container"> <div class="sort-trigger" v-if="!state.visible" @click="toggleSort">O</div> <div class="sort-content" :style="state.contentHolderBounds" v-else> <div class="sort-input"> <input type="text" v-model="state.search" placeholder="搜索接口" /> <div class="sort-close" @click="toggleSort">关闭</div> </div> <div class="sort-list"> <div class="sort-item" :title="state.getTime(item.updatedAt)" v-for="item in state.renderList.slice(0, state.splitIndex.sevenDays)" :key="item.id" @click="state.handleItemClick(item)" > <span :class="state.getItemClass(item)">{{ item.method.toUpperCase() }}</span> <span v-if="state.getFolderName(item.folderId)">{{ state.getFolderName(item.folderId) }}-</span> <span>{{ item.name }}</span> </div> <div class="sort-item-split">七天内</div> <div class="sort-item" :title="state.getTime(item.updatedAt)" v-for="item in state.renderList.slice(state.splitIndex.sevenDays, state.splitIndex.oneMonth)" :key="item.id" @click="state.handleItemClick(item)" > <span :class="state.getItemClass(item)">{{ item.method.toUpperCase() }}</span> <span v-if="state.getFolderName(item.folderId)">{{ state.getFolderName(item.folderId) }}-</span> <span>{{ item.name }}</span> </div> <div class="sort-item-split">一个月内</div> <div class="sort-item" :title="state.getTime(item.updatedAt)" v-for="item in state.renderList.slice(state.splitIndex.oneMonth)" :key="item.id" @click="state.handleItemClick(item)" > <span :class="state.getItemClass(item)">{{ item.method.toUpperCase() }}</span> <span v-if="state.getFolderName(item.folderId)">{{ state.getFolderName(item.folderId) }}-</span> <span>{{ item.name }}</span> </div> </div> </div> </div> ` }).mount(enums.doms.container) } function initStyle() { const style = document.createElement('style') style.innerHTML = styleStr document.head.appendChild(style) } async function domFinder(selector) { const res = document.querySelector(selector) if (!res) { await sleep(100) return domFinder(selector) } return res } function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)) } init() // 覆盖原生的fetch函数为自定义的函数 fetch = customFetch;