ApiDocsPage.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561
  1. <script setup>
  2. import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
  3. import {
  4. KeyOutlined,
  5. SearchOutlined,
  6. ExpandOutlined,
  7. CompressOutlined,
  8. ApiOutlined,
  9. SafetyCertificateOutlined,
  10. CloudServerOutlined,
  11. ClusterOutlined,
  12. GlobalOutlined,
  13. SaveOutlined,
  14. SettingOutlined,
  15. WifiOutlined,
  16. LinkOutlined,
  17. NodeIndexOutlined,
  18. } from '@ant-design/icons-vue';
  19. import { theme as themeState, antdThemeConfig } from '@/composables/useTheme.js';
  20. import AppSidebar from '@/components/AppSidebar.vue';
  21. import { sections as allSections } from './endpoints.js';
  22. import EndpointSection from './EndpointSection.vue';
  23. import CodeBlock from './CodeBlock.vue';
  24. const basePath = window.X_UI_BASE_PATH || '';
  25. const requestUri = window.location.pathname;
  26. const settingsHref = `${basePath}panel/settings#security`;
  27. const searchQuery = ref('');
  28. const collapsedSections = ref(new Set());
  29. const activeSection = ref('');
  30. const sectionIcons = {
  31. authentication: SafetyCertificateOutlined,
  32. inbounds: NodeIndexOutlined,
  33. server: CloudServerOutlined,
  34. nodes: ClusterOutlined,
  35. 'custom-geo': GlobalOutlined,
  36. backup: SaveOutlined,
  37. settings: SettingOutlined,
  38. 'api-tokens': KeyOutlined,
  39. 'xray-settings': WifiOutlined,
  40. subscription: LinkOutlined,
  41. websocket: ApiOutlined,
  42. };
  43. const curlExample = `curl -X GET \\
  44. -H "Authorization: Bearer YOUR_API_TOKEN" \\
  45. -H "Accept: application/json" \\
  46. https://your-panel.example.com/panel/api/inbounds/list`;
  47. const sections = computed(() => {
  48. const q = searchQuery.value.toLowerCase().trim();
  49. if (!q) return allSections;
  50. return allSections
  51. .map(s => {
  52. const matching = s.endpoints.filter(e =>
  53. e.path.toLowerCase().includes(q) ||
  54. e.summary?.toLowerCase().includes(q) ||
  55. e.method.toLowerCase().includes(q)
  56. );
  57. return { ...s, endpoints: matching };
  58. })
  59. .filter(s => s.endpoints.length > 0);
  60. });
  61. const endpointCount = computed(() =>
  62. allSections.reduce((sum, s) => sum + s.endpoints.length, 0)
  63. );
  64. const visibleEndpoints = computed(() =>
  65. sections.value.reduce((sum, s) => sum + s.endpoints.length, 0)
  66. );
  67. function isCollapsed(id) {
  68. return collapsedSections.value.has(id);
  69. }
  70. function toggleSection(id) {
  71. const s = new Set(collapsedSections.value);
  72. if (s.has(id)) s.delete(id); else s.add(id);
  73. collapsedSections.value = s;
  74. }
  75. function expandAll() {
  76. collapsedSections.value = new Set();
  77. }
  78. function collapseAll() {
  79. collapsedSections.value = new Set(allSections.map(s => s.id));
  80. }
  81. function scrollToSection(id) {
  82. const el = document.getElementById(id);
  83. if (!el) return;
  84. el.scrollIntoView({ behavior: 'smooth', block: 'start' });
  85. if (window.location.hash !== `#${id}`) {
  86. history.replaceState(null, '', `#${id}`);
  87. }
  88. }
  89. function scrollToHash() {
  90. const id = window.location.hash.slice(1);
  91. if (!id) return;
  92. const el = document.getElementById(id);
  93. if (el) el.scrollIntoView({ behavior: 'auto', block: 'start' });
  94. }
  95. let scrollObserver = null;
  96. function onScroll() {
  97. const toc = document.querySelector('.toc-nav');
  98. const tocHeight = toc ? toc.offsetHeight : 56;
  99. let current = '';
  100. for (const s of sections.value) {
  101. const el = document.getElementById(s.id);
  102. if (!el) continue;
  103. const rect = el.getBoundingClientRect();
  104. if (rect.top <= tocHeight + 20) {
  105. current = s.id;
  106. }
  107. }
  108. activeSection.value = current;
  109. }
  110. onMounted(() => {
  111. scrollObserver = onScroll;
  112. window.addEventListener('scroll', scrollObserver, { passive: true });
  113. window.addEventListener('hashchange', scrollToHash);
  114. requestAnimationFrame(() => {
  115. scrollToHash();
  116. onScroll();
  117. });
  118. });
  119. onBeforeUnmount(() => {
  120. if (scrollObserver) {
  121. window.removeEventListener('scroll', scrollObserver);
  122. }
  123. window.removeEventListener('hashchange', scrollToHash);
  124. });
  125. </script>
  126. <template>
  127. <a-config-provider :theme="antdThemeConfig">
  128. <a-layout class="api-docs-page" :class="{ 'is-dark': themeState.isDark, 'is-ultra': themeState.isUltra }">
  129. <AppSidebar :base-path="basePath" :request-uri="requestUri" />
  130. <a-layout class="content-shell">
  131. <a-layout-content class="content-area">
  132. <div class="docs-wrapper">
  133. <header class="docs-header">
  134. <h1 class="docs-title">API Documentation</h1>
  135. <p class="docs-lead">
  136. The 3x-ui panel exposes a REST API under <code>/panel/api/</code>. Authenticate with the panel session
  137. cookie, or with the <code>Authorization: Bearer &lt;token&gt;</code> header below. Every endpoint
  138. returns a uniform <code>{ success, msg, obj }</code> envelope unless otherwise noted.
  139. </p>
  140. </header>
  141. <a-card class="token-card" size="small">
  142. <div class="token-card-head">
  143. <div class="token-card-title">
  144. <KeyOutlined />
  145. <span>API Tokens</span>
  146. </div>
  147. <a-button type="primary" size="small" :href="settingsHref">
  148. Manage tokens
  149. </a-button>
  150. </div>
  151. <p class="token-hint">
  152. Create, enable, or revoke named Bearer tokens in
  153. <a :href="settingsHref">Settings → Security</a>. Send each request as
  154. <code>Authorization: Bearer &lt;token&gt;</code>. Token-authenticated callers skip CSRF and don't
  155. need a session cookie. Deleting a token revokes it immediately — running bots will need a new one.
  156. </p>
  157. </a-card>
  158. <a-card class="curl-card" size="small" title="Quick example">
  159. <CodeBlock :code="curlExample" lang="text" />
  160. </a-card>
  161. <div class="toolbar">
  162. <a-input-search
  163. v-model:value="searchQuery"
  164. placeholder="Search endpoints by path, method, or description…"
  165. allow-clear
  166. class="search-bar"
  167. >
  168. <template #prefix><SearchOutlined /></template>
  169. </a-input-search>
  170. <span class="match-count" v-if="searchQuery">
  171. {{ visibleEndpoints }} / {{ endpointCount }} endpoints
  172. </span>
  173. <a-space size="small">
  174. <a-button size="small" @click="expandAll">
  175. <template #icon><ExpandOutlined /></template>
  176. Expand all
  177. </a-button>
  178. <a-button size="small" @click="collapseAll">
  179. <template #icon><CompressOutlined /></template>
  180. Collapse all
  181. </a-button>
  182. </a-space>
  183. </div>
  184. <nav class="toc-nav">
  185. <span class="toc-label">On this page:</span>
  186. <div class="toc-links">
  187. <a
  188. v-for="s in sections"
  189. :key="s.id"
  190. class="toc-link"
  191. :class="{ active: activeSection === s.id }"
  192. :href="`#${s.id}`"
  193. @click.prevent="scrollToSection(s.id)"
  194. >
  195. <component :is="sectionIcons[s.id]" class="toc-icon" />
  196. <span class="toc-text">{{ s.title }}</span>
  197. <span class="toc-badge">{{ s.endpoints.length }}</span>
  198. </a>
  199. </div>
  200. </nav>
  201. <EndpointSection
  202. v-for="s in sections"
  203. :key="s.id"
  204. :section="s"
  205. :icon="sectionIcons[s.id]"
  206. :collapsed="isCollapsed(s.id)"
  207. @toggle="toggleSection(s.id)"
  208. />
  209. </div>
  210. </a-layout-content>
  211. </a-layout>
  212. </a-layout>
  213. </a-config-provider>
  214. </template>
  215. <style scoped>
  216. .api-docs-page {
  217. --bg-page: #e6e8ec;
  218. --bg-card: #ffffff;
  219. min-height: 100vh;
  220. background: var(--bg-page);
  221. }
  222. .api-docs-page.is-dark {
  223. --bg-page: #1e1e1e;
  224. --bg-card: #252526;
  225. }
  226. .api-docs-page.is-dark.is-ultra {
  227. --bg-page: #000;
  228. --bg-card: #0a0a0a;
  229. }
  230. .content-shell {
  231. background: var(--bg-page);
  232. }
  233. .content-area {
  234. padding: 24px;
  235. max-width: 100%;
  236. }
  237. @media (max-width: 768px) {
  238. .content-area {
  239. padding: 16px 12px 12px;
  240. padding-top: 64px;
  241. }
  242. }
  243. .docs-wrapper {
  244. max-width: 1100px;
  245. margin: 0 auto;
  246. }
  247. .docs-header {
  248. margin-bottom: 20px;
  249. padding: 24px;
  250. background: var(--bg-card);
  251. border: 1px solid rgba(128, 128, 128, 0.12);
  252. border-radius: 10px;
  253. }
  254. .docs-title {
  255. font-size: 28px;
  256. font-weight: 800;
  257. margin: 0 0 8px;
  258. color: rgba(0, 0, 0, 0.88);
  259. letter-spacing: -0.3px;
  260. }
  261. .docs-lead {
  262. margin: 0;
  263. color: rgba(0, 0, 0, 0.65);
  264. line-height: 1.65;
  265. font-size: 14px;
  266. }
  267. .docs-lead code,
  268. .token-hint code {
  269. background: rgba(128, 128, 128, 0.12);
  270. padding: 1px 6px;
  271. border-radius: 4px;
  272. font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  273. font-size: 12.5px;
  274. }
  275. .token-card,
  276. .curl-card {
  277. margin-bottom: 16px;
  278. }
  279. .token-card-head {
  280. display: flex;
  281. align-items: center;
  282. justify-content: space-between;
  283. gap: 12px;
  284. flex-wrap: wrap;
  285. margin-bottom: 10px;
  286. min-height: 32px;
  287. }
  288. .token-card-title {
  289. display: inline-flex;
  290. align-items: center;
  291. gap: 8px;
  292. font-weight: 600;
  293. font-size: 14px;
  294. }
  295. .token-hint {
  296. margin: 10px 0 0;
  297. color: rgba(0, 0, 0, 0.55);
  298. font-size: 12.5px;
  299. line-height: 1.55;
  300. }
  301. .code-block {
  302. background: rgba(128, 128, 128, 0.08);
  303. border: 1px solid rgba(128, 128, 128, 0.15);
  304. border-radius: 6px;
  305. padding: 10px 12px;
  306. font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  307. font-size: 12.5px;
  308. line-height: 1.55;
  309. margin: 0;
  310. white-space: pre-wrap;
  311. word-break: break-word;
  312. overflow-x: auto;
  313. }
  314. .toolbar {
  315. display: flex;
  316. align-items: center;
  317. gap: 12px;
  318. flex-wrap: wrap;
  319. margin-bottom: 16px;
  320. }
  321. .search-bar {
  322. flex: 1;
  323. min-width: 200px;
  324. max-width: 480px;
  325. }
  326. .match-count {
  327. font-size: 12px;
  328. color: rgba(0, 0, 0, 0.5);
  329. white-space: nowrap;
  330. }
  331. .toc-nav {
  332. display: flex;
  333. flex-wrap: wrap;
  334. align-items: flex-start;
  335. gap: 8px 12px;
  336. padding: 12px 16px;
  337. background: var(--bg-card);
  338. border: 1px solid rgba(128, 128, 128, 0.12);
  339. border-radius: 8px;
  340. margin-bottom: 16px;
  341. }
  342. .toc-label {
  343. font-size: 11px;
  344. font-weight: 600;
  345. text-transform: uppercase;
  346. letter-spacing: 0.6px;
  347. color: rgba(0, 0, 0, 0.5);
  348. padding-top: 3px;
  349. flex-shrink: 0;
  350. }
  351. .toc-links {
  352. display: flex;
  353. flex-wrap: wrap;
  354. gap: 6px;
  355. }
  356. .toc-link {
  357. display: inline-flex;
  358. align-items: center;
  359. gap: 5px;
  360. padding: 4px 10px;
  361. border-radius: 20px;
  362. font-size: 12.5px;
  363. color: rgba(0, 0, 0, 0.65);
  364. background: rgba(128, 128, 128, 0.06);
  365. border: 1px solid transparent;
  366. text-decoration: none;
  367. cursor: pointer;
  368. transition: all 0.2s;
  369. white-space: nowrap;
  370. }
  371. .toc-link:hover {
  372. background: rgba(22, 119, 255, 0.08);
  373. color: #1677ff;
  374. border-color: rgba(22, 119, 255, 0.2);
  375. }
  376. .toc-link.active {
  377. background: rgba(22, 119, 255, 0.12);
  378. color: #1677ff;
  379. border-color: rgba(22, 119, 255, 0.3);
  380. font-weight: 600;
  381. }
  382. .toc-icon {
  383. font-size: 13px;
  384. opacity: 0.8;
  385. }
  386. .toc-text {
  387. font-size: 12.5px;
  388. }
  389. .toc-badge {
  390. display: inline-flex;
  391. align-items: center;
  392. justify-content: center;
  393. min-width: 18px;
  394. height: 18px;
  395. padding: 0 5px;
  396. border-radius: 9px;
  397. font-size: 10.5px;
  398. font-weight: 700;
  399. background: rgba(22, 119, 255, 0.12);
  400. color: #1677ff;
  401. line-height: 1;
  402. }
  403. .toc-link.active .toc-badge {
  404. background: #1677ff;
  405. color: #fff;
  406. }
  407. </style>
  408. <style>
  409. body.dark .docs-title {
  410. color: rgba(255, 255, 255, 0.92);
  411. }
  412. html[data-theme='ultra-dark'] .docs-title {
  413. color: rgba(255, 255, 255, 0.95);
  414. }
  415. body.dark .docs-header {
  416. background: #252526;
  417. border-color: rgba(255, 255, 255, 0.08);
  418. }
  419. html[data-theme='ultra-dark'] .docs-header {
  420. background: #0a0a0a;
  421. border-color: rgba(255, 255, 255, 0.06);
  422. }
  423. body.dark .docs-lead,
  424. body.dark .token-hint {
  425. color: rgba(255, 255, 255, 0.7);
  426. }
  427. html[data-theme='ultra-dark'] .docs-lead,
  428. html[data-theme='ultra-dark'] .token-hint {
  429. color: rgba(255, 255, 255, 0.75);
  430. }
  431. body.dark .docs-lead code,
  432. body.dark .token-hint code {
  433. background: rgba(255, 255, 255, 0.1);
  434. }
  435. html[data-theme='ultra-dark'] .docs-lead code,
  436. html[data-theme='ultra-dark'] .token-hint code {
  437. background: rgba(255, 255, 255, 0.12);
  438. }
  439. body.dark .code-block {
  440. background: rgba(255, 255, 255, 0.04);
  441. border-color: rgba(255, 255, 255, 0.1);
  442. color: rgba(255, 255, 255, 0.88);
  443. }
  444. html[data-theme='ultra-dark'] .code-block {
  445. background: rgba(255, 255, 255, 0.02);
  446. border-color: rgba(255, 255, 255, 0.08);
  447. }
  448. body.dark .toc-nav {
  449. background: #252526;
  450. border-color: rgba(255, 255, 255, 0.08);
  451. }
  452. html[data-theme='ultra-dark'] .toc-nav {
  453. background: #0a0a0a;
  454. border-color: rgba(255, 255, 255, 0.06);
  455. }
  456. body.dark .toc-label {
  457. color: rgba(255, 255, 255, 0.55);
  458. }
  459. html[data-theme='ultra-dark'] .toc-label {
  460. color: rgba(255, 255, 255, 0.6);
  461. }
  462. body.dark .toc-link {
  463. color: rgba(255, 255, 255, 0.65);
  464. background: rgba(255, 255, 255, 0.06);
  465. }
  466. html[data-theme='ultra-dark'] .toc-link {
  467. background: rgba(255, 255, 255, 0.04);
  468. }
  469. body.dark .toc-link:hover {
  470. background: rgba(88, 166, 255, 0.12);
  471. color: #58a6ff;
  472. border-color: rgba(88, 166, 255, 0.25);
  473. }
  474. body.dark .toc-link.active {
  475. background: rgba(88, 166, 255, 0.15);
  476. color: #58a6ff;
  477. border-color: rgba(88, 166, 255, 0.35);
  478. }
  479. body.dark .toc-badge {
  480. background: rgba(88, 166, 255, 0.15);
  481. color: #58a6ff;
  482. }
  483. body.dark .toc-link.active .toc-badge {
  484. background: #58a6ff;
  485. color: #0d1117;
  486. }
  487. </style>