feat: restructure docs page to left-center-right 3-column layout with page TOC sidebar
This commit is contained in:
parent
e028f6f93a
commit
0ab319143b
|
|
@ -5690,6 +5690,7 @@
|
|||
"语音 & 音频": "Audio & Speech",
|
||||
"支持模型": "Supported Models",
|
||||
"速率限制": "Rate Limits",
|
||||
"常见问题": "FAQ"
|
||||
"常见问题": "FAQ",
|
||||
"本页目录": "On this page"
|
||||
}
|
||||
}
|
||||
|
|
@ -5549,6 +5549,7 @@
|
|||
"语音 & 音频": "语音 & 音频",
|
||||
"支持模型": "支持模型",
|
||||
"速率限制": "速率限制",
|
||||
"常见问题": "常见问题"
|
||||
"常见问题": "常见问题",
|
||||
"本页目录": "本页目录"
|
||||
}
|
||||
}
|
||||
|
|
@ -38,6 +38,7 @@ import {
|
|||
Music,
|
||||
Video,
|
||||
FileText,
|
||||
List,
|
||||
} from 'lucide-react';
|
||||
|
||||
/* ─── DOCS SECTIONS ─── */
|
||||
|
|
@ -95,8 +96,8 @@ function SectionTitle({ id, icon: Icon, children }) {
|
|||
}
|
||||
|
||||
/* ─── SubSection ─── */
|
||||
function SubSection({ children, className = '' }) {
|
||||
return <h3 className={`text-lg font-semibold text-gray-800 dark:text-gray-200 mt-8 mb-3 ${className}`}>{children}</h3>;
|
||||
function SubSection({ id, children, className = '' }) {
|
||||
return <h3 id={id} className={`text-lg font-semibold text-gray-800 dark:text-gray-200 mt-8 mb-3 scroll-mt-[72px] ${className}`}>{children}</h3>;
|
||||
}
|
||||
|
||||
/* ─── Paragraph ─── */
|
||||
|
|
@ -145,6 +146,90 @@ function InfoCard({ icon: Icon, title, children, className = '' }) {
|
|||
);
|
||||
}
|
||||
|
||||
/* ─── Right Page TOC ─── */
|
||||
function RightToc({ contentRef, HEADER_OFFSET, activeSection }) {
|
||||
const { t } = useTranslation();
|
||||
const [tocItems, setTocItems] = useState([]);
|
||||
const [activeTocId, setActiveTocId] = useState('');
|
||||
|
||||
// Build TOC from all h2/h3 headings in the content area
|
||||
useEffect(() => {
|
||||
if (!contentRef.current) return;
|
||||
const timer = setTimeout(() => {
|
||||
const headings = contentRef.current.querySelectorAll('h2[id], h3[id]');
|
||||
const items = [];
|
||||
headings.forEach((h) => {
|
||||
items.push({
|
||||
id: h.id,
|
||||
text: h.textContent.trim(),
|
||||
level: h.tagName === 'H2' ? 2 : 3,
|
||||
});
|
||||
});
|
||||
setTocItems(items);
|
||||
}, 100);
|
||||
return () => clearTimeout(timer);
|
||||
}, [contentRef, activeSection]);
|
||||
|
||||
// TOC scroll-spy
|
||||
useEffect(() => {
|
||||
if (tocItems.length === 0) return;
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
for (const entry of entries) {
|
||||
if (entry.isIntersecting) {
|
||||
setActiveTocId(entry.target.id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
{ rootMargin: `-${HEADER_OFFSET}px 0px -80% 0px`, threshold: 0 }
|
||||
);
|
||||
|
||||
tocItems.forEach((item) => {
|
||||
const el = document.getElementById(item.id);
|
||||
if (el) observer.observe(el);
|
||||
});
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, [tocItems, HEADER_OFFSET]);
|
||||
|
||||
const scrollToHeading = (id) => {
|
||||
const el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
const top = el.getBoundingClientRect().top + window.scrollY - HEADER_OFFSET;
|
||||
window.scrollTo({ top, behavior: 'smooth' });
|
||||
};
|
||||
|
||||
if (tocItems.length === 0) return null;
|
||||
|
||||
return (
|
||||
<aside className="hidden xl:block w-[200px] flex-shrink-0">
|
||||
<div className="sticky top-[72px] max-h-[calc(100vh-5rem)] overflow-y-auto py-8">
|
||||
<h4 className="flex items-center gap-2 text-xs font-semibold text-gray-400 dark:text-gray-500 uppercase tracking-wider mb-3 px-1">
|
||||
<List size={13} />
|
||||
{t('本页目录')}
|
||||
</h4>
|
||||
<nav className="space-y-0.5 border-l border-gray-200 dark:border-gray-700">
|
||||
{tocItems.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={() => scrollToHeading(item.id)}
|
||||
className={`block w-full text-left text-sm py-1.5 truncate transition-all duration-150
|
||||
${item.level === 3 ? 'pl-5' : 'pl-3'}
|
||||
${activeTocId === item.id
|
||||
? 'text-violet-600 dark:text-violet-400 font-medium border-l-2 -ml-px border-violet-500'
|
||||
: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 border-l-2 -ml-px border-transparent'
|
||||
}`}
|
||||
>
|
||||
{item.text}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
/* ─── MAIN DOCS PAGE ─── */
|
||||
export default function Docs() {
|
||||
const { t } = useTranslation();
|
||||
|
|
@ -256,7 +341,7 @@ export default function Docs() {
|
|||
)}
|
||||
|
||||
{/* ── Content ── */}
|
||||
<main ref={contentRef} className="flex-1 min-w-0 py-8 px-6 lg:px-10 xl:px-16 max-w-4xl">
|
||||
<main ref={contentRef} className="flex-1 min-w-0 py-8 px-6 lg:px-10 xl:px-16 max-w-3xl">
|
||||
|
||||
{/* ── Section: 概述 ── */}
|
||||
<SectionTitle id="overview" icon={BookOpen}>{t('概述')}</SectionTitle>
|
||||
|
|
@ -321,13 +406,13 @@ export default function Docs() {
|
|||
API 密钥是与 TuringToken 通信的凭证。每个密钥可以设置额度上限、有效期和 IP 白名单。
|
||||
</P>
|
||||
|
||||
<SubSection>创建密钥</SubSection>
|
||||
<SubSection id="apikey-create">创建密钥</SubSection>
|
||||
<P>
|
||||
登录后进入 <a href="/console/token" className="text-violet-600 dark:text-violet-400 hover:underline font-medium">控制台 → API Key</a>,
|
||||
点击"新建 API Key",填写名称后即可生成。请务必妥善保存密钥,关闭页面后将无法再次查看完整密钥。
|
||||
</P>
|
||||
|
||||
<SubSection>使用密钥</SubSection>
|
||||
<SubSection id="apikey-usage">使用密钥</SubSection>
|
||||
<P>在所有 API 请求中,通过 Bearer Token 方式传递密钥:</P>
|
||||
<CodeBlock
|
||||
lang="http"
|
||||
|
|
@ -345,7 +430,7 @@ export default function Docs() {
|
|||
兼容 OpenAI Chat Completions API 格式。
|
||||
</P>
|
||||
|
||||
<SubSection>基础请求</SubSection>
|
||||
<SubSection id="chat-basic">基础请求</SubSection>
|
||||
<CodeBlock
|
||||
lang="bash"
|
||||
code={`curl ${BASE_HOST}/v1/chat/completions \\
|
||||
|
|
@ -360,7 +445,7 @@ export default function Docs() {
|
|||
}'`}
|
||||
/>
|
||||
|
||||
<SubSection>流式输出</SubSection>
|
||||
<SubSection id="chat-stream">流式输出</SubSection>
|
||||
<P>设置 <code className="font-mono text-sm bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 rounded text-violet-600 dark:text-violet-400">stream: true</code> 即可启用 SSE 流式响应:</P>
|
||||
<CodeBlock
|
||||
lang="bash"
|
||||
|
|
@ -394,7 +479,7 @@ response = client.chat.completions.create(
|
|||
print(response.choices[0].message.content)`}
|
||||
/>
|
||||
|
||||
<SubSection>Node.js SDK 示例</SubSection>
|
||||
<SubSection id="chat-nodejs">Node.js SDK 示例</SubSection>
|
||||
<CodeBlock
|
||||
lang="javascript"
|
||||
code={`import OpenAI from 'openai';
|
||||
|
|
@ -438,7 +523,7 @@ console.log(completion.choices[0].message.content);`}
|
|||
<SectionTitle id="audio" icon={Music}>{t('语音 & 音频')}</SectionTitle>
|
||||
<P>支持语音转文字(STT)和文字转语音(TTS),兼容 OpenAI Audio API 格式。</P>
|
||||
|
||||
<SubSection>语音转文字 (STT)</SubSection>
|
||||
<SubSection id="audio-stt">语音转文字 (STT)</SubSection>
|
||||
<CodeBlock
|
||||
lang="bash"
|
||||
code={`curl ${BASE_HOST}/v1/audio/transcriptions \\
|
||||
|
|
@ -447,7 +532,7 @@ console.log(completion.choices[0].message.content);`}
|
|||
-F "model=whisper-1"`}
|
||||
/>
|
||||
|
||||
<SubSection>文字转语音 (TTS)</SubSection>
|
||||
<SubSection id="audio-tts">文字转语音 (TTS)</SubSection>
|
||||
<CodeBlock
|
||||
lang="bash"
|
||||
code={`curl ${BASE_HOST}/v1/audio/speech \\
|
||||
|
|
@ -573,6 +658,9 @@ console.log(completion.choices[0].message.content);`}
|
|||
</P>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* ── Right Page TOC ── */}
|
||||
<RightToc contentRef={contentRef} HEADER_OFFSET={HEADER_OFFSET} activeSection={activeSection} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in New Issue