Commit 058c3bd8 authored by 陈曦's avatar 陈曦
Browse files

添加支持模型的调用API文档

parent f9944efd
Pipeline #82018 failed with stage
in 1 minute and 2 seconds
This diff is collapsed.
......@@ -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,
......
......@@ -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',
......
<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\u4e00-\u9fff-]/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>
This diff is collapsed.
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment