|
|
@@ -1,37 +1,49 @@
|
|
|
<script setup>
|
|
|
-import { ref, computed, onMounted } from 'vue';
|
|
|
-import { useI18n } from 'vue-i18n';
|
|
|
-import { Modal, message } from 'ant-design-vue';
|
|
|
+import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
|
|
|
import {
|
|
|
KeyOutlined,
|
|
|
- ReloadOutlined,
|
|
|
- CopyOutlined,
|
|
|
- EyeOutlined,
|
|
|
- EyeInvisibleOutlined,
|
|
|
SearchOutlined,
|
|
|
ExpandOutlined,
|
|
|
CompressOutlined,
|
|
|
+ ApiOutlined,
|
|
|
+ SafetyCertificateOutlined,
|
|
|
+ CloudServerOutlined,
|
|
|
+ ClusterOutlined,
|
|
|
+ GlobalOutlined,
|
|
|
+ SaveOutlined,
|
|
|
+ SettingOutlined,
|
|
|
+ WifiOutlined,
|
|
|
+ LinkOutlined,
|
|
|
+ NodeIndexOutlined,
|
|
|
} from '@ant-design/icons-vue';
|
|
|
|
|
|
import { theme as themeState, antdThemeConfig } from '@/composables/useTheme.js';
|
|
|
import AppSidebar from '@/components/AppSidebar.vue';
|
|
|
-import { HttpUtil, ClipboardManager } from '@/utils/index.js';
|
|
|
import { sections as allSections } from './endpoints.js';
|
|
|
import EndpointSection from './EndpointSection.vue';
|
|
|
import CodeBlock from './CodeBlock.vue';
|
|
|
|
|
|
-const { t } = useI18n();
|
|
|
-
|
|
|
const basePath = window.X_UI_BASE_PATH || '';
|
|
|
const requestUri = window.location.pathname;
|
|
|
-
|
|
|
-const apiToken = ref('');
|
|
|
-const tokenLoading = ref(false);
|
|
|
-const tokenRotating = ref(false);
|
|
|
-const tokenVisible = ref(false);
|
|
|
+const settingsHref = `${basePath}panel/settings#security`;
|
|
|
|
|
|
const searchQuery = ref('');
|
|
|
const collapsedSections = ref(new Set());
|
|
|
+const activeSection = ref('');
|
|
|
+
|
|
|
+const sectionIcons = {
|
|
|
+ authentication: SafetyCertificateOutlined,
|
|
|
+ inbounds: NodeIndexOutlined,
|
|
|
+ server: CloudServerOutlined,
|
|
|
+ nodes: ClusterOutlined,
|
|
|
+ 'custom-geo': GlobalOutlined,
|
|
|
+ backup: SaveOutlined,
|
|
|
+ settings: SettingOutlined,
|
|
|
+ 'api-tokens': KeyOutlined,
|
|
|
+ 'xray-settings': WifiOutlined,
|
|
|
+ subscription: LinkOutlined,
|
|
|
+ websocket: ApiOutlined,
|
|
|
+};
|
|
|
|
|
|
const curlExample = `curl -X GET \\
|
|
|
-H "Authorization: Bearer YOUR_API_TOKEN" \\
|
|
|
@@ -57,7 +69,7 @@ const endpointCount = computed(() =>
|
|
|
allSections.reduce((sum, s) => sum + s.endpoints.length, 0)
|
|
|
);
|
|
|
|
|
|
-const visibleSections = computed(() =>
|
|
|
+const visibleEndpoints = computed(() =>
|
|
|
sections.value.reduce((sum, s) => sum + s.endpoints.length, 0)
|
|
|
);
|
|
|
|
|
|
@@ -79,50 +91,53 @@ function collapseAll() {
|
|
|
collapsedSections.value = new Set(allSections.map(s => s.id));
|
|
|
}
|
|
|
|
|
|
-async function loadApiToken() {
|
|
|
- tokenLoading.value = true;
|
|
|
- try {
|
|
|
- const msg = await HttpUtil.get('/panel/setting/getApiToken');
|
|
|
- if (msg?.success) apiToken.value = msg.obj || '';
|
|
|
- } finally {
|
|
|
- tokenLoading.value = false;
|
|
|
+function scrollToSection(id) {
|
|
|
+ const el = document.getElementById(id);
|
|
|
+ if (!el) return;
|
|
|
+ el.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
|
+ if (window.location.hash !== `#${id}`) {
|
|
|
+ history.replaceState(null, '', `#${id}`);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-function regenerateApiToken() {
|
|
|
- Modal.confirm({
|
|
|
- title: t('pages.nodes.regenerateConfirm'),
|
|
|
- okText: t('confirm'),
|
|
|
- cancelText: t('cancel'),
|
|
|
- okType: 'danger',
|
|
|
- onOk: async () => {
|
|
|
- tokenRotating.value = true;
|
|
|
- try {
|
|
|
- const msg = await HttpUtil.post('/panel/setting/regenerateApiToken');
|
|
|
- if (msg?.success) {
|
|
|
- apiToken.value = msg.obj || '';
|
|
|
- message.success(t('success'));
|
|
|
- }
|
|
|
- } finally {
|
|
|
- tokenRotating.value = false;
|
|
|
- }
|
|
|
- },
|
|
|
- });
|
|
|
-}
|
|
|
-
|
|
|
-async function copyApiToken() {
|
|
|
- if (!apiToken.value) return;
|
|
|
- const ok = await ClipboardManager.copyText(apiToken.value);
|
|
|
- if (ok) message.success(t('success'));
|
|
|
-}
|
|
|
-
|
|
|
-function scrollToSection(id) {
|
|
|
+function scrollToHash() {
|
|
|
+ const id = window.location.hash.slice(1);
|
|
|
+ if (!id) return;
|
|
|
const el = document.getElementById(id);
|
|
|
- if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
|
+ if (el) el.scrollIntoView({ behavior: 'auto', block: 'start' });
|
|
|
+}
|
|
|
+
|
|
|
+let scrollObserver = null;
|
|
|
+function onScroll() {
|
|
|
+ const toc = document.querySelector('.toc-nav');
|
|
|
+ const tocHeight = toc ? toc.offsetHeight : 56;
|
|
|
+ let current = '';
|
|
|
+ for (const s of sections.value) {
|
|
|
+ const el = document.getElementById(s.id);
|
|
|
+ if (!el) continue;
|
|
|
+ const rect = el.getBoundingClientRect();
|
|
|
+ if (rect.top <= tocHeight + 20) {
|
|
|
+ current = s.id;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ activeSection.value = current;
|
|
|
}
|
|
|
|
|
|
onMounted(() => {
|
|
|
- loadApiToken();
|
|
|
+ scrollObserver = onScroll;
|
|
|
+ window.addEventListener('scroll', scrollObserver, { passive: true });
|
|
|
+ window.addEventListener('hashchange', scrollToHash);
|
|
|
+ requestAnimationFrame(() => {
|
|
|
+ scrollToHash();
|
|
|
+ onScroll();
|
|
|
+ });
|
|
|
+});
|
|
|
+
|
|
|
+onBeforeUnmount(() => {
|
|
|
+ if (scrollObserver) {
|
|
|
+ window.removeEventListener('scroll', scrollObserver);
|
|
|
+ }
|
|
|
+ window.removeEventListener('hashchange', scrollToHash);
|
|
|
});
|
|
|
</script>
|
|
|
|
|
|
@@ -148,38 +163,17 @@ onMounted(() => {
|
|
|
<div class="token-card-head">
|
|
|
<div class="token-card-title">
|
|
|
<KeyOutlined />
|
|
|
- <span>API Token</span>
|
|
|
+ <span>API Tokens</span>
|
|
|
</div>
|
|
|
- <a-space size="small" wrap>
|
|
|
- <a-button size="small" @click="tokenVisible = !tokenVisible">
|
|
|
- <template #icon>
|
|
|
- <EyeInvisibleOutlined v-if="tokenVisible" />
|
|
|
- <EyeOutlined v-else />
|
|
|
- </template>
|
|
|
- {{ tokenVisible ? 'Hide' : 'Show' }}
|
|
|
- </a-button>
|
|
|
- <a-button size="small" :disabled="!apiToken" @click="copyApiToken">
|
|
|
- <template #icon>
|
|
|
- <CopyOutlined />
|
|
|
- </template>
|
|
|
- Copy
|
|
|
- </a-button>
|
|
|
- <a-button size="small" danger :loading="tokenRotating" @click="regenerateApiToken">
|
|
|
- <template #icon>
|
|
|
- <ReloadOutlined />
|
|
|
- </template>
|
|
|
- Regenerate
|
|
|
- </a-button>
|
|
|
- </a-space>
|
|
|
+ <a-button type="primary" size="small" :href="settingsHref">
|
|
|
+ Manage tokens
|
|
|
+ </a-button>
|
|
|
</div>
|
|
|
- <a-spin :spinning="tokenLoading" size="small">
|
|
|
- <pre
|
|
|
- class="token-value">{{ tokenVisible ? (apiToken || '—') : (apiToken ? '••••••••••••••••••••••••••••' : '—') }}</pre>
|
|
|
- </a-spin>
|
|
|
<p class="token-hint">
|
|
|
- Send it on every request as <code>Authorization: Bearer <token></code>. Token-authenticated
|
|
|
- callers skip CSRF and don't need a session cookie. Regenerating rotates the secret immediately —
|
|
|
- running bots will need the new value.
|
|
|
+ Create, enable, or revoke named Bearer tokens in
|
|
|
+ <a :href="settingsHref">Settings → Security</a>. Send each request as
|
|
|
+ <code>Authorization: Bearer <token></code>. Token-authenticated callers skip CSRF and don't
|
|
|
+ need a session cookie. Deleting a token revokes it immediately — running bots will need a new one.
|
|
|
</p>
|
|
|
</a-card>
|
|
|
|
|
|
@@ -197,7 +191,7 @@ onMounted(() => {
|
|
|
<template #prefix><SearchOutlined /></template>
|
|
|
</a-input-search>
|
|
|
<span class="match-count" v-if="searchQuery">
|
|
|
- {{ visibleSections }} / {{ endpointCount }} endpoints
|
|
|
+ {{ visibleEndpoints }} / {{ endpointCount }} endpoints
|
|
|
</span>
|
|
|
<a-space size="small">
|
|
|
<a-button size="small" @click="expandAll">
|
|
|
@@ -213,16 +207,27 @@ onMounted(() => {
|
|
|
|
|
|
<nav class="toc-nav">
|
|
|
<span class="toc-label">On this page:</span>
|
|
|
- <a v-for="s in sections" :key="s.id" class="toc-link" :href="`#${s.id}`"
|
|
|
- @click.prevent="scrollToSection(s.id)">
|
|
|
- {{ s.title }} ({{ s.endpoints.length }})
|
|
|
- </a>
|
|
|
+ <div class="toc-links">
|
|
|
+ <a
|
|
|
+ v-for="s in sections"
|
|
|
+ :key="s.id"
|
|
|
+ class="toc-link"
|
|
|
+ :class="{ active: activeSection === s.id }"
|
|
|
+ :href="`#${s.id}`"
|
|
|
+ @click.prevent="scrollToSection(s.id)"
|
|
|
+ >
|
|
|
+ <component :is="sectionIcons[s.id]" class="toc-icon" />
|
|
|
+ <span class="toc-text">{{ s.title }}</span>
|
|
|
+ <span class="toc-badge">{{ s.endpoints.length }}</span>
|
|
|
+ </a>
|
|
|
+ </div>
|
|
|
</nav>
|
|
|
|
|
|
<EndpointSection
|
|
|
v-for="s in sections"
|
|
|
:key="s.id"
|
|
|
:section="s"
|
|
|
+ :icon="sectionIcons[s.id]"
|
|
|
:collapsed="isCollapsed(s.id)"
|
|
|
@toggle="toggleSection(s.id)"
|
|
|
/>
|
|
|
@@ -273,20 +278,25 @@ onMounted(() => {
|
|
|
}
|
|
|
|
|
|
.docs-header {
|
|
|
- margin-bottom: 18px;
|
|
|
+ margin-bottom: 20px;
|
|
|
+ padding: 24px;
|
|
|
+ background: var(--bg-card);
|
|
|
+ border: 1px solid rgba(128, 128, 128, 0.12);
|
|
|
+ border-radius: 10px;
|
|
|
}
|
|
|
|
|
|
.docs-title {
|
|
|
- font-size: 26px;
|
|
|
- font-weight: 700;
|
|
|
+ font-size: 28px;
|
|
|
+ font-weight: 800;
|
|
|
margin: 0 0 8px;
|
|
|
color: rgba(0, 0, 0, 0.88);
|
|
|
+ letter-spacing: -0.3px;
|
|
|
}
|
|
|
|
|
|
.docs-lead {
|
|
|
margin: 0;
|
|
|
color: rgba(0, 0, 0, 0.65);
|
|
|
- line-height: 1.6;
|
|
|
+ line-height: 1.65;
|
|
|
font-size: 14px;
|
|
|
}
|
|
|
|
|
|
@@ -310,7 +320,8 @@ onMounted(() => {
|
|
|
justify-content: space-between;
|
|
|
gap: 12px;
|
|
|
flex-wrap: wrap;
|
|
|
- margin-bottom: 8px;
|
|
|
+ margin-bottom: 10px;
|
|
|
+ min-height: 32px;
|
|
|
}
|
|
|
|
|
|
.token-card-title {
|
|
|
@@ -321,18 +332,6 @@ onMounted(() => {
|
|
|
font-size: 14px;
|
|
|
}
|
|
|
|
|
|
-.token-value {
|
|
|
- background: rgba(128, 128, 128, 0.08);
|
|
|
- border: 1px solid rgba(128, 128, 128, 0.15);
|
|
|
- border-radius: 6px;
|
|
|
- padding: 10px 12px;
|
|
|
- font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
|
- font-size: 13px;
|
|
|
- margin: 0;
|
|
|
- word-break: break-all;
|
|
|
- white-space: pre-wrap;
|
|
|
-}
|
|
|
-
|
|
|
.token-hint {
|
|
|
margin: 10px 0 0;
|
|
|
color: rgba(0, 0, 0, 0.55);
|
|
|
@@ -377,32 +376,87 @@ onMounted(() => {
|
|
|
.toc-nav {
|
|
|
display: flex;
|
|
|
flex-wrap: wrap;
|
|
|
- align-items: center;
|
|
|
- gap: 8px 14px;
|
|
|
+ align-items: flex-start;
|
|
|
+ gap: 8px 12px;
|
|
|
padding: 12px 16px;
|
|
|
- background: rgba(128, 128, 128, 0.08);
|
|
|
- border-radius: 6px;
|
|
|
+ background: var(--bg-card);
|
|
|
+ border: 1px solid rgba(128, 128, 128, 0.12);
|
|
|
+ border-radius: 8px;
|
|
|
margin-bottom: 16px;
|
|
|
}
|
|
|
|
|
|
.toc-label {
|
|
|
- font-size: 12px;
|
|
|
+ font-size: 11px;
|
|
|
font-weight: 600;
|
|
|
text-transform: uppercase;
|
|
|
- letter-spacing: 0.5px;
|
|
|
+ letter-spacing: 0.6px;
|
|
|
color: rgba(0, 0, 0, 0.5);
|
|
|
+ padding-top: 3px;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.toc-links {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 6px;
|
|
|
}
|
|
|
|
|
|
.toc-link {
|
|
|
- color: #1677ff;
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 5px;
|
|
|
+ padding: 4px 10px;
|
|
|
+ border-radius: 20px;
|
|
|
+ font-size: 12.5px;
|
|
|
+ color: rgba(0, 0, 0, 0.65);
|
|
|
+ background: rgba(128, 128, 128, 0.06);
|
|
|
+ border: 1px solid transparent;
|
|
|
text-decoration: none;
|
|
|
cursor: pointer;
|
|
|
- font-size: 13px;
|
|
|
+ transition: all 0.2s;
|
|
|
+ white-space: nowrap;
|
|
|
}
|
|
|
|
|
|
.toc-link:hover {
|
|
|
- color: #4096ff;
|
|
|
- text-decoration: underline;
|
|
|
+ background: rgba(22, 119, 255, 0.08);
|
|
|
+ color: #1677ff;
|
|
|
+ border-color: rgba(22, 119, 255, 0.2);
|
|
|
+}
|
|
|
+
|
|
|
+.toc-link.active {
|
|
|
+ background: rgba(22, 119, 255, 0.12);
|
|
|
+ color: #1677ff;
|
|
|
+ border-color: rgba(22, 119, 255, 0.3);
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
+
|
|
|
+.toc-icon {
|
|
|
+ font-size: 13px;
|
|
|
+ opacity: 0.8;
|
|
|
+}
|
|
|
+
|
|
|
+.toc-text {
|
|
|
+ font-size: 12.5px;
|
|
|
+}
|
|
|
+
|
|
|
+.toc-badge {
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ min-width: 18px;
|
|
|
+ height: 18px;
|
|
|
+ padding: 0 5px;
|
|
|
+ border-radius: 9px;
|
|
|
+ font-size: 10.5px;
|
|
|
+ font-weight: 700;
|
|
|
+ background: rgba(22, 119, 255, 0.12);
|
|
|
+ color: #1677ff;
|
|
|
+ line-height: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.toc-link.active .toc-badge {
|
|
|
+ background: #1677ff;
|
|
|
+ color: #fff;
|
|
|
}
|
|
|
</style>
|
|
|
|
|
|
@@ -411,28 +465,97 @@ body.dark .docs-title {
|
|
|
color: rgba(255, 255, 255, 0.92);
|
|
|
}
|
|
|
|
|
|
+html[data-theme='ultra-dark'] .docs-title {
|
|
|
+ color: rgba(255, 255, 255, 0.95);
|
|
|
+}
|
|
|
+
|
|
|
+body.dark .docs-header {
|
|
|
+ background: #252526;
|
|
|
+ border-color: rgba(255, 255, 255, 0.08);
|
|
|
+}
|
|
|
+
|
|
|
+html[data-theme='ultra-dark'] .docs-header {
|
|
|
+ background: #0a0a0a;
|
|
|
+ border-color: rgba(255, 255, 255, 0.06);
|
|
|
+}
|
|
|
+
|
|
|
body.dark .docs-lead,
|
|
|
body.dark .token-hint {
|
|
|
color: rgba(255, 255, 255, 0.7);
|
|
|
}
|
|
|
|
|
|
+html[data-theme='ultra-dark'] .docs-lead,
|
|
|
+html[data-theme='ultra-dark'] .token-hint {
|
|
|
+ color: rgba(255, 255, 255, 0.75);
|
|
|
+}
|
|
|
+
|
|
|
body.dark .docs-lead code,
|
|
|
body.dark .token-hint code {
|
|
|
background: rgba(255, 255, 255, 0.1);
|
|
|
}
|
|
|
|
|
|
-body.dark .token-value,
|
|
|
+html[data-theme='ultra-dark'] .docs-lead code,
|
|
|
+html[data-theme='ultra-dark'] .token-hint code {
|
|
|
+ background: rgba(255, 255, 255, 0.12);
|
|
|
+}
|
|
|
+
|
|
|
body.dark .code-block {
|
|
|
background: rgba(255, 255, 255, 0.04);
|
|
|
border-color: rgba(255, 255, 255, 0.1);
|
|
|
color: rgba(255, 255, 255, 0.88);
|
|
|
}
|
|
|
|
|
|
+html[data-theme='ultra-dark'] .code-block {
|
|
|
+ background: rgba(255, 255, 255, 0.02);
|
|
|
+ border-color: rgba(255, 255, 255, 0.08);
|
|
|
+}
|
|
|
+
|
|
|
body.dark .toc-nav {
|
|
|
- background: rgba(255, 255, 255, 0.04);
|
|
|
+ background: #252526;
|
|
|
+ border-color: rgba(255, 255, 255, 0.08);
|
|
|
+}
|
|
|
+
|
|
|
+html[data-theme='ultra-dark'] .toc-nav {
|
|
|
+ background: #0a0a0a;
|
|
|
+ border-color: rgba(255, 255, 255, 0.06);
|
|
|
}
|
|
|
|
|
|
body.dark .toc-label {
|
|
|
color: rgba(255, 255, 255, 0.55);
|
|
|
}
|
|
|
+
|
|
|
+html[data-theme='ultra-dark'] .toc-label {
|
|
|
+ color: rgba(255, 255, 255, 0.6);
|
|
|
+}
|
|
|
+
|
|
|
+body.dark .toc-link {
|
|
|
+ color: rgba(255, 255, 255, 0.65);
|
|
|
+ background: rgba(255, 255, 255, 0.06);
|
|
|
+}
|
|
|
+
|
|
|
+html[data-theme='ultra-dark'] .toc-link {
|
|
|
+ background: rgba(255, 255, 255, 0.04);
|
|
|
+}
|
|
|
+
|
|
|
+body.dark .toc-link:hover {
|
|
|
+ background: rgba(88, 166, 255, 0.12);
|
|
|
+ color: #58a6ff;
|
|
|
+ border-color: rgba(88, 166, 255, 0.25);
|
|
|
+}
|
|
|
+
|
|
|
+body.dark .toc-link.active {
|
|
|
+ background: rgba(88, 166, 255, 0.15);
|
|
|
+ color: #58a6ff;
|
|
|
+ border-color: rgba(88, 166, 255, 0.35);
|
|
|
+}
|
|
|
+
|
|
|
+body.dark .toc-badge {
|
|
|
+ background: rgba(88, 166, 255, 0.15);
|
|
|
+ color: #58a6ff;
|
|
|
+}
|
|
|
+
|
|
|
+body.dark .toc-link.active .toc-badge {
|
|
|
+ background: #58a6ff;
|
|
|
+ color: #0d1117;
|
|
|
+}
|
|
|
</style>
|