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

2024/11/25 11:09:18

  1. // ==UserScript==
  2. // @name 按最近更新顺序排列 apifox 接口列表
  3. // @namespace Violentmonkey Scripts
  4. // @match *://app.apifox.com/*
  5. // @require https://unpkg.com/vue@3/dist/vue.global.prod.js
  6. // @require https://unpkg.com/dayjs@1.11.11/dayjs.min.js
  7. // @grant none
  8. // @version 1.0.1
  9. // @license GPL
  10. // @author -
  11. // @description 2024/11/25 11:09:18
  12. // ==/UserScript==
  13. window.Vue = Vue
  14. const { createApp, ref, reactive, computed, watchEffect } = Vue
  15. const styleStr = `
  16. .sort-trigger {
  17. width: 30px;
  18. height: 30px;
  19. background-color: var(--app-bg-200);
  20. border-radius: 50%;
  21. position: fixed;
  22. top: 50%;
  23. right: 20px;
  24. transform: translateY(-50%);
  25. box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1);
  26. border: 1px solid #e0e0e0;
  27. display: flex;
  28. justify-content: center;
  29. align-items: center;
  30. cursor: pointer;
  31. }
  32. .sort-content {
  33. position: fixed;
  34. right: 10px;
  35. width: 360px;
  36. overflow: hidden;
  37. background: var(--app-bg-200);
  38. box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1);
  39. z-index: 1000;
  40. display: flex;
  41. flex-direction: column;
  42. }
  43. .sort-input {
  44. padding: 10px 15px;
  45. display: flex;
  46. gap: 10px;
  47. }
  48. .sort-input input {
  49. background: var(--app-bg-200);
  50. width: 100%;
  51. border: 1px solid #e0e0e0;
  52. border-radius: 5px;
  53. padding: 5px 10px;
  54. outline: none;
  55. }
  56. .sort-list {
  57. flex: 1;
  58. overflow: auto;
  59. }
  60. .sort-item {
  61. padding: 5px 15px;
  62. cursor: pointer;
  63. }
  64. .sort-close {
  65. padding: 5px 15px;
  66. cursor: pointer;
  67. text-align: center;
  68. border: 1px solid #e0e0e0;
  69. color: #666;
  70. border-radius: 5px;
  71. background: var(--app-bg-400);
  72. white-space: nowrap;
  73. }
  74. .sort-item-split {
  75. height: 1px;
  76. text-align: center;
  77. display: flex;
  78. align-items: center;
  79. justify-content: center;
  80. white-space: nowrap;
  81. gap: 10px;
  82. opacity: 0.5;
  83. }
  84. .sort-item-split::before, .sort-item-split::after {
  85. content: '';
  86. width: 100%;
  87. height: 1px;
  88. background: #e0e0e0;
  89. }
  90. `
  91. const enums = {
  92. urls: {
  93. apiDetails: /api\/v1\/api-details/,
  94. folders: /api\/v1\/projects\/[^/]*\/api-detail-folders/
  95. },
  96. doms: {
  97. container: document.createElement('div'),
  98. searchInput: ".ui-input.ui-input-variant-default",
  99. contentHolder: ".ui-tabs-content-holder"
  100. }
  101. }
  102. const globalState = reactive({
  103. sortList: [],
  104. folders: []
  105. })
  106.  
  107. // 保存原始的fetch函数
  108. let originalFetch = fetch;
  109.  
  110. // 自定义的fetch函数
  111. async function customFetch(url, options) {
  112. // 发送原始的fetch请求,并等待响应
  113. let response = await originalFetch(url, options);
  114. setTimeout(() => {
  115. interceptResponse(response)
  116. }, 1000)
  117. return response;
  118. }
  119.  
  120. // 自定义的拦截响应数据的方法
  121. async function interceptResponse(response) {
  122. // 这里假设响应数据是JSON格式,先进行解析
  123. const list = [
  124. [enums.urls.apiDetails, resolveApiDetails],
  125. [enums.urls.folders, resolveFolders]
  126. ]
  127. list.forEach(([url, action]) => {
  128. const link = new URL(response.url);
  129. if (link.pathname.match(url)) {
  130. action(response);
  131. }
  132. })
  133. }
  134.  
  135. async function resolveApiDetails(response) {
  136. const { data } = await response.json();
  137. globalState.sortList = data.map(item => {
  138. return {
  139. name: item.name,
  140. id: item.id,
  141. path: item.path,
  142. method: item.method,
  143. folderId: item.folderId,
  144. updatedAt: dayjs(item.updatedAt).unix()
  145. }
  146. }).sort((a, b) => b.updatedAt - a.updatedAt)
  147. }
  148. async function resolveFolders(response) {
  149. const { data } = await response.json();
  150. globalState.folders = data
  151. }
  152.  
  153. function init() {
  154. document.body.appendChild(enums.doms.container);
  155. initStyle()
  156. createApp({
  157. name: 'apifox-sort',
  158. setup() {
  159. syncContentHolder()
  160. const state = reactive({
  161. visible: false,
  162. search: '',
  163. contentHolderBounds: {
  164. left: 0,
  165. top: 0,
  166. height: 0
  167. },
  168. getFolderName(folderId) {
  169. return globalState.folders.find(folder => folder.id === folderId)?.name
  170. },
  171. getItemClass(item) {
  172. if (item.method === 'post') {
  173. return 'pui-g-ui-kit-request-method-icon-index-container text-orange-6 text-left block'
  174. }
  175. if (item.method === 'get') {
  176. return 'pui-g-ui-kit-request-method-icon-index-container text-green-6 text-left block'
  177. }
  178. return 'pui-g-ui-kit-request-method-icon-index-container text-orange-6 text-left block'
  179. },
  180. renderList: computed(() => {
  181. if (!state.search) return globalState.sortList
  182. return globalState.sortList.filter(item => {
  183. const nameFilter = item.name.includes(state.search)
  184. const pathFilter = item.path.includes(state.search)
  185. return nameFilter || pathFilter
  186. })
  187. }),
  188. splitIndex: computed(() => {
  189. const list = {
  190. sevenDays: 0,
  191. oneMonth: 0,
  192. }
  193. state.renderList.forEach((item, index) => {
  194. const isRecentSevenDays = dayjs().diff(dayjs(item.updatedAt * 1000), 'day') <= 7;
  195. if (isRecentSevenDays) {
  196. list.sevenDays = index + 1;
  197. }
  198. const isRecentOneMonth = dayjs().diff(dayjs(item.updatedAt * 1000), 'month') <= 1;
  199. if (isRecentOneMonth) {
  200. list.oneMonth = index + 1;
  201. }
  202. });
  203. return list
  204. }),
  205. handleItemClick(item) {
  206. navigator.clipboard.writeText(item.path)
  207. const input = document.querySelector(enums.doms.searchInput)
  208. input.focus()
  209. },
  210. getTime(t) {
  211. return dayjs(t * 1000).format('YYYY-MM-DD HH:mm:ss')
  212. }
  213. })
  214. watchEffect(() => {
  215. console.log(state.splitIndex)
  216. })
  217. const toggleSort = () => {
  218. state.visible = !state.visible
  219. }
  220. async function syncContentHolder() {
  221. const contentHolder = await domFinder(enums.doms.contentHolder)
  222. const bounds = contentHolder.getBoundingClientRect()
  223. state.contentHolderBounds = {
  224. top: bounds.top + 'px',
  225. height: bounds.height + 'px'
  226. }
  227. }
  228. return {
  229. state,
  230. toggleSort,
  231. globalState
  232. }
  233. },
  234. template: `
  235. <div class="sort-container">
  236. <div class="sort-trigger" v-if="!state.visible" @click="toggleSort">O</div>
  237. <div class="sort-content" :style="state.contentHolderBounds" v-else>
  238. <div class="sort-input">
  239. <input type="text" v-model="state.search" placeholder="搜索接口" />
  240. <div class="sort-close" @click="toggleSort">关闭</div>
  241. </div>
  242. <div class="sort-list">
  243. <div class="sort-item"
  244. :title="state.getTime(item.updatedAt)"
  245. v-for="item in state.renderList.slice(0, state.splitIndex.sevenDays)"
  246. :key="item.id" @click="state.handleItemClick(item)"
  247. >
  248. <span :class="state.getItemClass(item)">{{ item.method.toUpperCase() }}</span>
  249. <span v-if="state.getFolderName(item.folderId)">{{ state.getFolderName(item.folderId) }}-</span>
  250. <span>{{ item.name }}</span>
  251. </div>
  252. <div class="sort-item-split">七天内</div>
  253. <div class="sort-item"
  254. :title="state.getTime(item.updatedAt)"
  255. v-for="item in state.renderList.slice(state.splitIndex.sevenDays, state.splitIndex.oneMonth)"
  256. :key="item.id" @click="state.handleItemClick(item)"
  257. >
  258. <span :class="state.getItemClass(item)">{{ item.method.toUpperCase() }}</span>
  259. <span v-if="state.getFolderName(item.folderId)">{{ state.getFolderName(item.folderId) }}-</span>
  260. <span>{{ item.name }}</span>
  261. </div>
  262. <div class="sort-item-split">一个月内</div>
  263. <div class="sort-item"
  264. :title="state.getTime(item.updatedAt)"
  265. v-for="item in state.renderList.slice(state.splitIndex.oneMonth)"
  266. :key="item.id" @click="state.handleItemClick(item)"
  267. >
  268. <span :class="state.getItemClass(item)">{{ item.method.toUpperCase() }}</span>
  269. <span v-if="state.getFolderName(item.folderId)">{{ state.getFolderName(item.folderId) }}-</span>
  270. <span>{{ item.name }}</span>
  271. </div>
  272. </div>
  273. </div>
  274. </div>
  275. `
  276. }).mount(enums.doms.container)
  277. }
  278.  
  279. function initStyle() {
  280. const style = document.createElement('style')
  281. style.innerHTML = styleStr
  282. document.head.appendChild(style)
  283. }
  284.  
  285. async function domFinder(selector) {
  286. const res = document.querySelector(selector)
  287. if (!res) {
  288. await sleep(100)
  289. return domFinder(selector)
  290. }
  291. return res
  292. }
  293.  
  294. function sleep(ms) {
  295. return new Promise(resolve => setTimeout(resolve, ms))
  296. }
  297.  
  298. init()
  299. // 覆盖原生的fetch函数为自定义的函数
  300. fetch = customFetch;

QingJ © 2025

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