按最近更新顺序排列 apifox 接口列表

2024/11/25 11:09:18

目前为 2024-11-25 提交的版本。查看 最新版本

// ==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;

QingJ © 2025

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