Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
058c3bd8
Commit
058c3bd8
authored
Apr 10, 2026
by
陈曦
Browse files
添加支持模型的调用API文档
parent
f9944efd
Pipeline
#82018
failed with stage
in 1 minute and 2 seconds
Changes
5
Pipelines
1
Expand all
Show whitespace changes
Inline
Side-by-side
TrafficAPI调用文档.md
0 → 100644
View file @
058c3bd8
This diff is collapsed.
Click to expand it.
frontend/src/components/layout/AppSidebar.vue
View file @
058c3bd8
...
...
@@ -385,6 +385,21 @@ const BellIcon = {
)
}
const
BookOpenIcon
=
{
render
:
()
=>
h
(
'
svg
'
,
{
fill
:
'
none
'
,
viewBox
:
'
0 0 24 24
'
,
stroke
:
'
currentColor
'
,
'
stroke-width
'
:
'
1.5
'
},
[
h
(
'
path
'
,
{
'
stroke-linecap
'
:
'
round
'
,
'
stroke-linejoin
'
:
'
round
'
,
d
:
'
M12 6.042A8.967 8.967 0 006 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 016 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 016-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0018 18a8.967 8.967 0 00-6 2.292m0-14.25v14.25
'
})
]
)
}
const
TicketIcon
=
{
render
:
()
=>
h
(
...
...
@@ -499,6 +514,7 @@ const userNavItems = computed((): NavItem[] => {
:
[]),
{
path
:
'
/redeem
'
,
label
:
t
(
'
nav.redeem
'
),
icon
:
GiftIcon
,
hideInSimpleMode
:
true
},
{
path
:
'
/profile
'
,
label
:
t
(
'
nav.profile
'
),
icon
:
UserIcon
},
{
path
:
'
/docs
'
,
label
:
t
(
'
nav.docs
'
),
icon
:
BookOpenIcon
},
...
customMenuItemsForUser
.
value
.
map
((
item
):
NavItem
=>
({
path
:
`/custom/
${
item
.
id
}
`
,
label
:
item
.
label
,
...
...
@@ -527,6 +543,7 @@ const personalNavItems = computed((): NavItem[] => {
:
[]),
{
path
:
'
/redeem
'
,
label
:
t
(
'
nav.redeem
'
),
icon
:
GiftIcon
,
hideInSimpleMode
:
true
},
{
path
:
'
/profile
'
,
label
:
t
(
'
nav.profile
'
),
icon
:
UserIcon
},
{
path
:
'
/docs
'
,
label
:
t
(
'
nav.docs
'
),
icon
:
BookOpenIcon
},
...
customMenuItemsForUser
.
value
.
map
((
item
):
NavItem
=>
({
path
:
`/custom/
${
item
.
id
}
`
,
label
:
item
.
label
,
...
...
frontend/src/router/index.ts
View file @
058c3bd8
...
...
@@ -201,6 +201,17 @@ const routes: RouteRecordRaw[] = [
descriptionKey
:
'
purchase.description
'
}
},
{
path
:
'
/docs
'
,
name
:
'
Docs
'
,
component
:
()
=>
import
(
'
@/views/user/DocsView.vue
'
),
meta
:
{
requiresAuth
:
true
,
requiresAdmin
:
false
,
title
:
'
API Docs
'
,
titleKey
:
'
nav.docs
'
}
},
{
path
:
'
/custom/:id
'
,
name
:
'
CustomPage
'
,
...
...
frontend/src/views/user/DocsView.vue
0 → 100644
View file @
058c3bd8
<
template
>
<AppLayout>
<div
class=
"docs-page-root"
>
<!-- TOC sidebar — sticky within the page -->
<aside
v-if=
"toc.length > 0"
class=
"docs-toc"
>
<div
class=
"docs-toc-inner"
>
<p
class=
"toc-title"
>
目录
</p>
<nav
class=
"space-y-0.5"
>
<a
v-for=
"item in toc"
:key=
"item.id"
:href=
"`#$
{item.id}`"
class="toc-link"
:class="[
item.level === 1 ? 'toc-h1' : item.level === 2 ? 'toc-h2' : 'toc-h3',
activeId === item.id ? 'toc-link-active' : 'toc-link-idle'
]"
@click.prevent="scrollTo(item.id)"
>
{{
item
.
text
}}
</a>
</nav>
</div>
</aside>
<!-- Doc content -->
<div
class=
"docs-scroll"
>
<article
ref=
"articleEl"
class=
"docs-content"
v-html=
"renderedContent"
></article>
</div>
</div>
</AppLayout>
</
template
>
<
script
setup
lang=
"ts"
>
import
{
onMounted
,
onUnmounted
,
ref
,
nextTick
}
from
'
vue
'
import
{
Marked
,
Renderer
}
from
'
marked
'
import
DOMPurify
from
'
dompurify
'
import
AppLayout
from
'
@/components/layout/AppLayout.vue
'
import
docRaw
from
'
../../../../trafficapi_ai_call_documentation.md?raw
'
// ─── Slug ─────────────────────────────────────────────────────────────────────
function
slugify
(
text
:
string
):
string
{
return
text
.
toLowerCase
()
.
replace
(
/
\s
+/g
,
'
-
'
)
.
replace
(
/
[^\w\u
4e00-
\u
9fff-
]
/g
,
''
)
.
replace
(
/-+/g
,
'
-
'
)
.
replace
(
/^-|-$/g
,
''
)
}
interface
TocItem
{
id
:
string
;
text
:
string
;
level
:
number
}
// ─── Parse once ───────────────────────────────────────────────────────────────
const
toc
:
TocItem
[]
=
[]
const
slugCount
:
Record
<
string
,
number
>
=
{}
const
renderer
=
new
Renderer
()
// eslint-disable-next-line @typescript-eslint/no-explicit-any
renderer
.
heading
=
function
(
token
:
any
)
{
const
text
:
string
=
token
.
text
??
''
const
depth
:
number
=
token
.
depth
??
1
const
raw
=
text
.
replace
(
/<
[^
>
]
+>/g
,
''
)
let
slug
=
slugify
(
raw
)
if
(
slugCount
[
slug
]
!==
undefined
)
{
slugCount
[
slug
]
++
;
slug
=
`
${
slug
}
-
${
slugCount
[
slug
]}
`
}
else
{
slugCount
[
slug
]
=
0
}
toc
.
push
({
id
:
slug
,
text
:
raw
,
level
:
depth
})
return
`<h
${
depth
}
id="
${
slug
}
">
${
text
}
</h
${
depth
}
>`
}
const
markedInstance
=
new
Marked
({
renderer
,
breaks
:
true
,
gfm
:
true
})
const
html
=
markedInstance
.
parse
(
docRaw
)
as
string
const
renderedContent
=
DOMPurify
.
sanitize
(
html
,
{
ADD_ATTR
:
[
'
id
'
]
})
// ─── Active heading — page-level scroll ───────────────────────────────────────
// AppLayout: header h-16 (64px) + main p-4~p-8. Use 88px as safe offset.
const
SCROLL_OFFSET
=
88
const
activeId
=
ref
(
toc
[
0
]?.
id
??
''
)
const
articleEl
=
ref
<
HTMLElement
|
null
>
(
null
)
let
observer
:
IntersectionObserver
|
null
=
null
function
setupObserver
()
{
if
(
observer
)
observer
.
disconnect
()
if
(
!
articleEl
.
value
)
return
const
headings
=
articleEl
.
value
.
querySelectorAll
(
'
h1[id], h2[id], h3[id], h4[id]
'
)
if
(
!
headings
.
length
)
return
// root: null → observe against the viewport (page scrolls, not an inner div)
observer
=
new
IntersectionObserver
(
(
entries
)
=>
{
const
visible
=
entries
.
filter
((
e
)
=>
e
.
isIntersecting
)
.
sort
((
a
,
b
)
=>
a
.
boundingClientRect
.
top
-
b
.
boundingClientRect
.
top
)
if
(
visible
.
length
>
0
)
activeId
.
value
=
visible
[
0
].
target
.
id
},
{
root
:
null
,
rootMargin
:
`-
${
SCROLL_OFFSET
}
px 0px -60% 0px`
,
threshold
:
0
}
)
headings
.
forEach
((
el
)
=>
observer
!
.
observe
(
el
))
}
function
scrollTo
(
id
:
string
)
{
const
el
=
document
.
getElementById
(
id
)
if
(
!
el
)
return
// Scroll at page (window) level, offset by sticky header height
const
top
=
el
.
getBoundingClientRect
().
top
+
window
.
scrollY
-
SCROLL_OFFSET
window
.
scrollTo
({
top
,
behavior
:
'
smooth
'
})
activeId
.
value
=
id
}
onMounted
(
async
()
=>
{
await
nextTick
();
setupObserver
()
})
onUnmounted
(()
=>
{
observer
?.
disconnect
()
})
</
script
>
<
style
scoped
>
/* ═══════════════════════════════════════════════════════════════
Layout — page scrolls naturally; TOC is sticky
═══════════════════════════════════════════════════════════════ */
.docs-page-root
{
display
:
flex
;
align-items
:
flex-start
;
border-radius
:
1rem
;
border
:
1px
solid
#e5e7eb
;
background
:
#ffffff
;
overflow
:
visible
;
/* let page scroll */
}
:global
(
.dark
)
.docs-page-root
{
background
:
#111827
;
border-color
:
rgba
(
255
,
255
,
255
,
0.08
);
}
.docs-toc
{
width
:
14rem
;
flex-shrink
:
0
;
border-right
:
1px
solid
#e5e7eb
;
display
:
none
;
background
:
#f9fafb
;
/* sticky: stays in view as page scrolls */
position
:
sticky
;
top
:
88px
;
/* header (64px) + main padding (24px) */
max-height
:
calc
(
100vh
-
100px
);
overflow-y
:
auto
;
}
@media
(
min-width
:
1280px
)
{
.docs-toc
{
display
:
block
;
}
}
:global
(
.dark
)
.docs-toc
{
border-right-color
:
rgba
(
255
,
255
,
255
,
0.07
);
background
:
#0d1525
;
}
.docs-toc-inner
{
padding
:
1rem
;
}
.docs-scroll
{
min-width
:
0
;
flex
:
1
;
}
.docs-content
{
max-width
:
56rem
;
margin-left
:
auto
;
margin-right
:
auto
;
padding
:
2rem
1.5rem
;
}
/* Give headings scroll-margin so they aren't hidden behind the sticky header */
.docs-content
:deep
(
h1
),
.docs-content
:deep
(
h2
),
.docs-content
:deep
(
h3
),
.docs-content
:deep
(
h4
)
{
scroll-margin-top
:
92px
;
}
/* ═══════════════════════════════════════════════════════════════
TOC
═══════════════════════════════════════════════════════════════ */
.toc-title
{
margin-bottom
:
0.75rem
;
font-size
:
0.7rem
;
font-weight
:
600
;
text-transform
:
uppercase
;
letter-spacing
:
0.08em
;
color
:
#9ca3af
;
}
.toc-link
{
display
:
block
;
white-space
:
nowrap
;
overflow
:
hidden
;
text-overflow
:
ellipsis
;
border-radius
:
0.375rem
;
padding
:
0.25rem
0.5rem
;
font-size
:
0.8rem
;
text-decoration
:
none
;
transition
:
background-color
0.1s
,
color
0.1s
;
}
.toc-h1
{
font-weight
:
600
;
}
.toc-h2
{
padding-left
:
1rem
;
}
.toc-h3
{
padding-left
:
1.5rem
;
font-size
:
0.72rem
;
}
.toc-link-idle
{
color
:
#6b7280
;
}
.toc-link-idle
:hover
{
background
:
#f3f4f6
;
color
:
#111827
;
}
:global
(
.dark
)
.toc-link-idle
{
color
:
#9ca3af
;
}
:global
(
.dark
)
.toc-link-idle
:hover
{
background
:
rgba
(
255
,
255
,
255
,
0.07
);
color
:
#e2e8f0
;
}
.toc-link-active
{
background
:
#ede9fe
;
color
:
#6d28d9
;
}
:global
(
.dark
)
.toc-link-active
{
background
:
rgba
(
109
,
40
,
217
,
0.2
);
color
:
#a78bfa
;
}
/* ═══════════════════════════════════════════════════════════════
Markdown typography — Light
═══════════════════════════════════════════════════════════════ */
.docs-content
:deep
(
h1
)
{
margin
:
2rem
0
1rem
;
font-size
:
1.875rem
;
font-weight
:
700
;
color
:
#111827
;
line-height
:
2.25rem
;
}
.docs-content
:deep
(
h2
)
{
margin
:
2rem
0
0.75rem
;
font-size
:
1.5rem
;
font-weight
:
600
;
color
:
#1f2937
;
padding-bottom
:
0.5rem
;
border-bottom
:
1px
solid
#e5e7eb
;
}
.docs-content
:deep
(
h3
)
{
margin
:
1.5rem
0
0.5rem
;
font-size
:
1.2rem
;
font-weight
:
600
;
color
:
#374151
;
}
.docs-content
:deep
(
h4
)
{
margin
:
1rem
0
0.5rem
;
font-size
:
1rem
;
font-weight
:
600
;
color
:
#4b5563
;
}
.docs-content
:deep
(
p
)
{
margin-bottom
:
1rem
;
line-height
:
1.75
;
color
:
#374151
;
}
.docs-content
:deep
(
ul
),
.docs-content
:deep
(
ol
)
{
margin-bottom
:
1rem
;
padding-left
:
1.5rem
;
color
:
#374151
;
}
.docs-content
:deep
(
ul
)
{
list-style-type
:
disc
;
}
.docs-content
:deep
(
ol
)
{
list-style-type
:
decimal
;
}
.docs-content
:deep
(
li
)
{
margin-bottom
:
0.25rem
;
line-height
:
1.75
;
}
.docs-content
:deep
(
a
)
{
color
:
#6d28d9
;
text-decoration
:
underline
;
}
.docs-content
:deep
(
a
:hover
)
{
color
:
#5b21b6
;
}
.docs-content
:deep
(
blockquote
)
{
margin-bottom
:
1rem
;
border-left
:
4px
solid
#c4b5fd
;
padding-left
:
1rem
;
font-style
:
italic
;
color
:
#6b7280
;
}
.docs-content
:deep
(
strong
)
{
font-weight
:
600
;
color
:
#111827
;
}
.docs-content
:deep
(
hr
)
{
margin
:
2rem
0
;
border
:
none
;
border-top
:
1px
solid
#e5e7eb
;
}
/* Table — light */
.docs-content
:deep
(
table
)
{
margin-bottom
:
1rem
;
width
:
100%
;
border-collapse
:
collapse
;
font-size
:
0.875rem
;
}
.docs-content
:deep
(
th
)
{
border
:
1px
solid
#d1d5db
;
background
:
#f3f4f6
;
padding
:
0.5rem
0.75rem
;
text-align
:
left
;
font-weight
:
600
;
color
:
#374151
;
}
.docs-content
:deep
(
td
)
{
border
:
1px
solid
#e5e7eb
;
padding
:
0.5rem
0.75rem
;
color
:
#374151
;
}
.docs-content
:deep
(
tr
:nth-child
(
even
)
td
)
{
background
:
#f9fafb
;
}
/* Inline code — light */
.docs-content
:deep
(
code
)
{
background
:
#fef2f2
;
color
:
#be123c
;
padding
:
0.125rem
0.375rem
;
border-radius
:
0.25rem
;
font-family
:
ui-monospace
,
SFMono-Regular
,
Menlo
,
Monaco
,
Consolas
,
monospace
;
font-size
:
0.85em
;
}
/* Code block — always dark bg */
.docs-content
:deep
(
pre
)
{
margin-bottom
:
1rem
;
overflow-x
:
auto
;
border-radius
:
0.75rem
;
background
:
#1e293b
;
border
:
1px
solid
rgba
(
255
,
255
,
255
,
0.12
);
padding
:
1rem
;
font-size
:
0.875rem
;
line-height
:
1.6
;
}
.docs-content
:deep
(
pre
code
)
{
background
:
transparent
!important
;
color
:
#e2e8f0
!important
;
padding
:
0
!important
;
font-size
:
inherit
;
border-radius
:
0
;
}
/* ═══════════════════════════════════════════════════════════════
Markdown typography — Dark
═══════════════════════════════════════════════════════════════ */
:global
(
.dark
)
.docs-content
:deep
(
h1
)
{
color
:
#f1f5f9
;
}
:global
(
.dark
)
.docs-content
:deep
(
h2
)
{
color
:
#e2e8f0
;
border-bottom-color
:
rgba
(
255
,
255
,
255
,
0.1
);
}
:global
(
.dark
)
.docs-content
:deep
(
h3
)
{
color
:
#cbd5e1
;
}
:global
(
.dark
)
.docs-content
:deep
(
h4
)
{
color
:
#94a3b8
;
}
:global
(
.dark
)
.docs-content
:deep
(
p
)
{
color
:
#cbd5e1
;
}
:global
(
.dark
)
.docs-content
:deep
(
ul
),
:global
(
.dark
)
.docs-content
:deep
(
ol
)
{
color
:
#cbd5e1
;
}
:global
(
.dark
)
.docs-content
:deep
(
li
)
{
color
:
#cbd5e1
;
}
:global
(
.dark
)
.docs-content
:deep
(
a
)
{
color
:
#a78bfa
;
}
:global
(
.dark
)
.docs-content
:deep
(
a
:hover
)
{
color
:
#c4b5fd
;
}
:global
(
.dark
)
.docs-content
:deep
(
blockquote
)
{
border-left-color
:
#7c3aed
;
color
:
#9ca3af
;
}
:global
(
.dark
)
.docs-content
:deep
(
strong
)
{
color
:
#f1f5f9
;
}
:global
(
.dark
)
.docs-content
:deep
(
hr
)
{
border-top-color
:
rgba
(
255
,
255
,
255
,
0.1
);
}
/* Table — dark */
:global
(
.dark
)
.docs-content
:deep
(
th
)
{
border-color
:
rgba
(
255
,
255
,
255
,
0.12
);
background
:
rgba
(
255
,
255
,
255
,
0.06
);
color
:
#e2e8f0
;
}
:global
(
.dark
)
.docs-content
:deep
(
td
)
{
border-color
:
rgba
(
255
,
255
,
255
,
0.08
);
color
:
#cbd5e1
;
}
:global
(
.dark
)
.docs-content
:deep
(
tr
:nth-child
(
even
)
td
)
{
background
:
rgba
(
255
,
255
,
255
,
0.03
);
}
/* Inline code — dark */
:global
(
.dark
)
.docs-content
:deep
(
code
)
{
background
:
rgba
(
239
,
68
,
68
,
0.12
);
color
:
#fca5a5
;
}
/* Code block dark — slightly lighter to be distinct from page */
:global
(
.dark
)
.docs-content
:deep
(
pre
)
{
background
:
#0f172a
;
border-color
:
rgba
(
255
,
255
,
255
,
0.1
);
}
</
style
>
trafficapi_ai_call_documentation.md
0 → 100644
View file @
058c3bd8
This diff is collapsed.
Click to expand it.
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment