ApiDocsPage.vue 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  1. <script setup>
  2. import { ref, onMounted } from 'vue';
  3. import { useI18n } from 'vue-i18n';
  4. import { Modal, message } from 'ant-design-vue';
  5. import {
  6. KeyOutlined,
  7. ReloadOutlined,
  8. CopyOutlined,
  9. EyeOutlined,
  10. EyeInvisibleOutlined,
  11. } from '@ant-design/icons-vue';
  12. import { theme as themeState, antdThemeConfig } from '@/composables/useTheme.js';
  13. import AppSidebar from '@/components/AppSidebar.vue';
  14. import { HttpUtil, ClipboardManager } from '@/utils/index.js';
  15. import { sections } from './endpoints.js';
  16. import EndpointSection from './EndpointSection.vue';
  17. const { t } = useI18n();
  18. const basePath = window.X_UI_BASE_PATH || '';
  19. const requestUri = window.location.pathname;
  20. const apiToken = ref('');
  21. const tokenLoading = ref(false);
  22. const tokenRotating = ref(false);
  23. const tokenVisible = ref(false);
  24. const curlExample = `curl -X GET \\
  25. -H "Authorization: Bearer YOUR_API_TOKEN" \\
  26. -H "Accept: application/json" \\
  27. https://your-panel.example.com/panel/api/inbounds/list`;
  28. async function loadApiToken() {
  29. tokenLoading.value = true;
  30. try {
  31. const msg = await HttpUtil.get('/panel/setting/getApiToken');
  32. if (msg?.success) apiToken.value = msg.obj || '';
  33. } finally {
  34. tokenLoading.value = false;
  35. }
  36. }
  37. function regenerateApiToken() {
  38. Modal.confirm({
  39. title: t('pages.nodes.regenerateConfirm'),
  40. okText: t('confirm'),
  41. cancelText: t('cancel'),
  42. okType: 'danger',
  43. onOk: async () => {
  44. tokenRotating.value = true;
  45. try {
  46. const msg = await HttpUtil.post('/panel/setting/regenerateApiToken');
  47. if (msg?.success) {
  48. apiToken.value = msg.obj || '';
  49. message.success(t('success'));
  50. }
  51. } finally {
  52. tokenRotating.value = false;
  53. }
  54. },
  55. });
  56. }
  57. async function copyApiToken() {
  58. if (!apiToken.value) return;
  59. const ok = await ClipboardManager.copy(apiToken.value);
  60. if (ok) message.success(t('success'));
  61. }
  62. function scrollToSection(id) {
  63. const el = document.getElementById(id);
  64. if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
  65. }
  66. onMounted(() => {
  67. loadApiToken();
  68. });
  69. </script>
  70. <template>
  71. <a-config-provider :theme="antdThemeConfig">
  72. <a-layout class="api-docs-page" :class="{ 'is-dark': themeState.isDark, 'is-ultra': themeState.isUltra }">
  73. <AppSidebar :base-path="basePath" :request-uri="requestUri" />
  74. <a-layout class="content-shell">
  75. <a-layout-content class="content-area">
  76. <div class="docs-wrapper">
  77. <header class="docs-header">
  78. <h1 class="docs-title">API Documentation</h1>
  79. <p class="docs-lead">
  80. The 3x-ui panel exposes a REST API under <code>/panel/api/</code>. Authenticate with the panel session
  81. cookie, or with the <code>Authorization: Bearer &lt;token&gt;</code> header below. Every endpoint
  82. returns a uniform <code>{ success, msg, obj }</code> envelope unless otherwise noted.
  83. </p>
  84. </header>
  85. <a-card class="token-card" size="small">
  86. <div class="token-card-head">
  87. <div class="token-card-title">
  88. <KeyOutlined />
  89. <span>API Token</span>
  90. </div>
  91. <a-space size="small" wrap>
  92. <a-button size="small" @click="tokenVisible = !tokenVisible">
  93. <template #icon>
  94. <EyeInvisibleOutlined v-if="tokenVisible" />
  95. <EyeOutlined v-else />
  96. </template>
  97. {{ tokenVisible ? 'Hide' : 'Show' }}
  98. </a-button>
  99. <a-button size="small" :disabled="!apiToken" @click="copyApiToken">
  100. <template #icon>
  101. <CopyOutlined />
  102. </template>
  103. Copy
  104. </a-button>
  105. <a-button size="small" danger :loading="tokenRotating" @click="regenerateApiToken">
  106. <template #icon>
  107. <ReloadOutlined />
  108. </template>
  109. Regenerate
  110. </a-button>
  111. </a-space>
  112. </div>
  113. <a-spin :spinning="tokenLoading" size="small">
  114. <pre
  115. class="token-value">{{ tokenVisible ? (apiToken || '—') : (apiToken ? '••••••••••••••••••••••••••••' : '—') }}</pre>
  116. </a-spin>
  117. <p class="token-hint">
  118. Send it on every request as <code>Authorization: Bearer &lt;token&gt;</code>. Token-authenticated
  119. callers skip CSRF and don't need a session cookie. Regenerating rotates the secret immediately —
  120. running bots will need the new value.
  121. </p>
  122. </a-card>
  123. <a-card class="curl-card" size="small" title="Quick example">
  124. <pre class="code-block">{{ curlExample }}</pre>
  125. </a-card>
  126. <nav class="toc-nav">
  127. <span class="toc-label">On this page:</span>
  128. <a v-for="s in sections" :key="s.id" class="toc-link" :href="`#${s.id}`"
  129. @click.prevent="scrollToSection(s.id)">
  130. {{ s.title }}
  131. </a>
  132. </nav>
  133. <EndpointSection v-for="s in sections" :key="s.id" :section="s" />
  134. </div>
  135. </a-layout-content>
  136. </a-layout>
  137. </a-layout>
  138. </a-config-provider>
  139. </template>
  140. <style scoped>
  141. .api-docs-page {
  142. --bg-page: #e6e8ec;
  143. --bg-card: #ffffff;
  144. min-height: 100vh;
  145. background: var(--bg-page);
  146. }
  147. .api-docs-page.is-dark {
  148. --bg-page: #1e1e1e;
  149. --bg-card: #252526;
  150. }
  151. .api-docs-page.is-dark.is-ultra {
  152. --bg-page: #000;
  153. --bg-card: #0a0a0a;
  154. }
  155. .content-shell {
  156. background: var(--bg-page);
  157. }
  158. .content-area {
  159. padding: 24px;
  160. max-width: 100%;
  161. }
  162. @media (max-width: 768px) {
  163. .content-area {
  164. padding: 16px 12px 12px;
  165. padding-top: 64px;
  166. }
  167. }
  168. .docs-wrapper {
  169. max-width: 1100px;
  170. margin: 0 auto;
  171. }
  172. .docs-header {
  173. margin-bottom: 18px;
  174. }
  175. .docs-title {
  176. font-size: 26px;
  177. font-weight: 700;
  178. margin: 0 0 8px;
  179. color: rgba(0, 0, 0, 0.88);
  180. }
  181. .docs-lead {
  182. margin: 0;
  183. color: rgba(0, 0, 0, 0.65);
  184. line-height: 1.6;
  185. font-size: 14px;
  186. }
  187. .docs-lead code,
  188. .token-hint code {
  189. background: rgba(128, 128, 128, 0.12);
  190. padding: 1px 6px;
  191. border-radius: 4px;
  192. font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  193. font-size: 12.5px;
  194. }
  195. .token-card,
  196. .curl-card {
  197. margin-bottom: 16px;
  198. }
  199. .token-card-head {
  200. display: flex;
  201. align-items: center;
  202. justify-content: space-between;
  203. gap: 12px;
  204. flex-wrap: wrap;
  205. margin-bottom: 8px;
  206. }
  207. .token-card-title {
  208. display: inline-flex;
  209. align-items: center;
  210. gap: 8px;
  211. font-weight: 600;
  212. font-size: 14px;
  213. }
  214. .token-value {
  215. background: rgba(128, 128, 128, 0.08);
  216. border: 1px solid rgba(128, 128, 128, 0.15);
  217. border-radius: 6px;
  218. padding: 10px 12px;
  219. font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  220. font-size: 13px;
  221. margin: 0;
  222. word-break: break-all;
  223. white-space: pre-wrap;
  224. }
  225. .token-hint {
  226. margin: 10px 0 0;
  227. color: rgba(0, 0, 0, 0.55);
  228. font-size: 12.5px;
  229. line-height: 1.55;
  230. }
  231. .code-block {
  232. background: rgba(128, 128, 128, 0.08);
  233. border: 1px solid rgba(128, 128, 128, 0.15);
  234. border-radius: 6px;
  235. padding: 10px 12px;
  236. font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  237. font-size: 12.5px;
  238. line-height: 1.55;
  239. margin: 0;
  240. white-space: pre-wrap;
  241. word-break: break-word;
  242. overflow-x: auto;
  243. }
  244. .toc-nav {
  245. display: flex;
  246. flex-wrap: wrap;
  247. align-items: center;
  248. gap: 8px 14px;
  249. padding: 12px 16px;
  250. background: rgba(128, 128, 128, 0.08);
  251. border-radius: 6px;
  252. margin-bottom: 16px;
  253. }
  254. .toc-label {
  255. font-size: 12px;
  256. font-weight: 600;
  257. text-transform: uppercase;
  258. letter-spacing: 0.5px;
  259. color: rgba(0, 0, 0, 0.5);
  260. }
  261. .toc-link {
  262. color: #1677ff;
  263. text-decoration: none;
  264. cursor: pointer;
  265. font-size: 13px;
  266. }
  267. .toc-link:hover {
  268. color: #4096ff;
  269. text-decoration: underline;
  270. }
  271. </style>
  272. <style>
  273. body.dark .docs-title {
  274. color: rgba(255, 255, 255, 0.92);
  275. }
  276. body.dark .docs-lead,
  277. body.dark .token-hint {
  278. color: rgba(255, 255, 255, 0.7);
  279. }
  280. body.dark .docs-lead code,
  281. body.dark .token-hint code {
  282. background: rgba(255, 255, 255, 0.1);
  283. }
  284. body.dark .token-value,
  285. body.dark .code-block {
  286. background: rgba(255, 255, 255, 0.04);
  287. border-color: rgba(255, 255, 255, 0.1);
  288. color: rgba(255, 255, 255, 0.88);
  289. }
  290. body.dark .toc-nav {
  291. background: rgba(255, 255, 255, 0.04);
  292. }
  293. body.dark .toc-label {
  294. color: rgba(255, 255, 255, 0.55);
  295. }
  296. </style>