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