mirror of
https://github.com/labring/FastGPT.git
synced 2026-05-10 01:08:08 +08:00
update doc (#6823)
This commit is contained in:
@@ -1,18 +1,45 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { useLocalizedRouter } from '@/lib/localized-navigation';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import { getLocalizedPath, i18n } from '@/lib/i18n';
|
||||
import { useCurrentLang } from '@/lib/localized-navigation';
|
||||
|
||||
interface RedirectProps {
|
||||
to: string;
|
||||
}
|
||||
|
||||
function removeLocalePrefix(pathname: string): string {
|
||||
const segments = pathname.split('/').filter(Boolean);
|
||||
|
||||
if (segments[0] && i18n.languages.includes(segments[0])) {
|
||||
return `/${segments.slice(1).join('/')}`;
|
||||
}
|
||||
|
||||
return pathname;
|
||||
}
|
||||
|
||||
function normalizeDocPath(to: string, pathname: string): string {
|
||||
const target = to.replace(/(?:\.[a-z]{2}(?:-[A-Z]{2})?)?\.mdx$/, '');
|
||||
|
||||
if (target.startsWith('/')) {
|
||||
return removeLocalePrefix(target);
|
||||
}
|
||||
|
||||
const currentPath = removeLocalePrefix(pathname);
|
||||
const basePath = currentPath.endsWith('/') ? currentPath : `${currentPath}/`;
|
||||
|
||||
return new URL(target, `http://localhost${basePath}`).pathname;
|
||||
}
|
||||
|
||||
export function Redirect({ to }: RedirectProps) {
|
||||
const router = useLocalizedRouter();
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const lang = useCurrentLang();
|
||||
|
||||
useEffect(() => {
|
||||
router.push(to);
|
||||
}, [to, router]);
|
||||
router.push(getLocalizedPath(normalizeDocPath(to, pathname), lang));
|
||||
}, [to, pathname, lang, router]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
'use client';
|
||||
|
||||
import { useMemo, type ReactNode } from 'react';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { SwitcherDropdown } from '@/components/docs/switcherDropdown';
|
||||
import { cn } from '@/lib/cn';
|
||||
|
||||
export type CategorySwitcherOption = {
|
||||
icon?: ReactNode;
|
||||
title: ReactNode;
|
||||
url: string;
|
||||
urls?: Set<string>;
|
||||
};
|
||||
|
||||
type CategorySwitcherProps = {
|
||||
options: CategorySwitcherOption[];
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const getCategoryKey = (path: string): string => {
|
||||
const segments = path.split('/').filter(Boolean);
|
||||
return segments[1] ?? '';
|
||||
};
|
||||
|
||||
export function CategorySwitcher({ options, className }: CategorySwitcherProps) {
|
||||
const pathname = usePathname();
|
||||
|
||||
const selected = useMemo(() => {
|
||||
const lookup = pathname.endsWith('/') ? pathname.slice(0, -1) : pathname;
|
||||
const currentCategory = getCategoryKey(lookup);
|
||||
return (
|
||||
options.find((item) => item.urls?.has(lookup)) ??
|
||||
options.find((item) => getCategoryKey(item.url) === currentCategory) ??
|
||||
options[0]
|
||||
);
|
||||
}, [options, pathname]);
|
||||
|
||||
return (
|
||||
<SwitcherDropdown
|
||||
className="w-full"
|
||||
triggerClassName={cn(
|
||||
'flex h-[42px] w-full max-w-none justify-between gap-2 rounded-xl border bg-fd-secondary/50 p-1.5 text-start text-sm text-fd-secondary-foreground hover:bg-fd-accent data-[open=true]:bg-fd-accent data-[open=true]:text-fd-accent-foreground',
|
||||
className
|
||||
)}
|
||||
contentClassName="w-(--radix-popover-trigger-width)"
|
||||
align="start"
|
||||
keepSidebarOpenOnSelect
|
||||
options={options.map((item) => ({
|
||||
key: item.url,
|
||||
label: item.title,
|
||||
icon: item.icon,
|
||||
href: item.url,
|
||||
active: item === selected
|
||||
}))}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
'use client';
|
||||
|
||||
import { useI18n } from 'fumadocs-ui/contexts/i18n';
|
||||
import { SwitcherDropdown } from '@/components/docs/switcherDropdown';
|
||||
|
||||
const localeFlags: Record<string, string> = {
|
||||
en: '🇺🇸',
|
||||
'zh-CN': '🇨🇳'
|
||||
};
|
||||
|
||||
type LanguageSwitcherProps = {
|
||||
className?: string;
|
||||
buttonClassName?: string;
|
||||
menuClassName?: string;
|
||||
};
|
||||
|
||||
export function LanguageSwitcher({
|
||||
className,
|
||||
buttonClassName,
|
||||
menuClassName
|
||||
}: LanguageSwitcherProps) {
|
||||
const { locale, locales = [], onChange } = useI18n();
|
||||
|
||||
return (
|
||||
<SwitcherDropdown
|
||||
className={className}
|
||||
triggerClassName={buttonClassName}
|
||||
contentClassName={menuClassName}
|
||||
options={locales.map((item) => ({
|
||||
key: item.locale,
|
||||
label: item.name,
|
||||
icon: <span className="text-base leading-none">{localeFlags[item.locale] ?? '🌐'}</span>,
|
||||
active: item.locale === locale,
|
||||
onSelect: () => onChange?.(item.locale)
|
||||
}))}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -12,7 +12,7 @@ export default function NotFound() {
|
||||
|
||||
useEffect(() => {
|
||||
// Redirect to introduction page
|
||||
window.location.replace(getLocalizedPath('/docs/introduction', lang));
|
||||
window.location.replace(getLocalizedPath('/introduction', lang));
|
||||
}, [lang]);
|
||||
|
||||
return null;
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { Check, ChevronDown } from 'lucide-react';
|
||||
import { useState, type ReactNode } from 'react';
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger
|
||||
} from 'fumadocs-ui/components/ui/popover';
|
||||
import { useSidebar } from 'fumadocs-ui/provider';
|
||||
import { cn } from '@/lib/cn';
|
||||
|
||||
export type SwitcherDropdownOption = {
|
||||
key: string;
|
||||
label: ReactNode;
|
||||
icon?: ReactNode;
|
||||
href?: string;
|
||||
active?: boolean;
|
||||
onSelect?: () => void;
|
||||
};
|
||||
|
||||
type SwitcherDropdownProps = {
|
||||
options: SwitcherDropdownOption[];
|
||||
className?: string;
|
||||
triggerClassName?: string;
|
||||
contentClassName?: string;
|
||||
optionClassName?: string;
|
||||
iconClassName?: string;
|
||||
align?: 'start' | 'center' | 'end';
|
||||
keepSidebarOpenOnSelect?: boolean;
|
||||
};
|
||||
|
||||
export function SwitcherDropdown({
|
||||
options,
|
||||
className,
|
||||
triggerClassName,
|
||||
contentClassName,
|
||||
optionClassName,
|
||||
iconClassName,
|
||||
align = 'end',
|
||||
keepSidebarOpenOnSelect = false
|
||||
}: SwitcherDropdownProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const { closeOnRedirect } = useSidebar();
|
||||
const selected = options.find((item) => item.active) ?? options[0];
|
||||
|
||||
const selectItem = (item: SwitcherDropdownOption) => {
|
||||
if (keepSidebarOpenOnSelect) {
|
||||
closeOnRedirect.current = false;
|
||||
}
|
||||
item.onSelect?.();
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const renderOption = (item: SwitcherDropdownOption) => {
|
||||
const active = item === selected;
|
||||
const content = (
|
||||
<>
|
||||
{item.icon && (
|
||||
<span className="flex size-5 shrink-0 items-center justify-center [&_svg]:size-4">
|
||||
{item.icon}
|
||||
</span>
|
||||
)}
|
||||
<span className="min-w-0 flex-1 truncate">{item.label}</span>
|
||||
<Check
|
||||
className={cn(
|
||||
'size-3.5 shrink-0 text-[#3370FF] dark:text-blue-400',
|
||||
!active && 'invisible'
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
const className = cn(
|
||||
'flex h-8 w-full cursor-pointer items-center gap-2 rounded-lg px-2.5 text-left text-fd-popover-foreground transition-colors hover:bg-fd-accent',
|
||||
active && 'font-semibold text-[#3370FF] dark:text-blue-400',
|
||||
optionClassName
|
||||
);
|
||||
|
||||
if (item.href) {
|
||||
return (
|
||||
<Link
|
||||
key={item.key}
|
||||
href={item.href}
|
||||
onClick={() => selectItem(item)}
|
||||
className={className}
|
||||
role="option"
|
||||
aria-selected={active}
|
||||
>
|
||||
{content}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
key={item.key}
|
||||
type="button"
|
||||
onClick={() => selectItem(item)}
|
||||
className={className}
|
||||
role="option"
|
||||
aria-selected={active}
|
||||
>
|
||||
{content}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<div className={cn('relative flex shrink-0', className)}>
|
||||
<PopoverTrigger
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded={open}
|
||||
data-open={open}
|
||||
className={cn(
|
||||
'inline-flex h-8 max-w-[9.5rem] cursor-pointer items-center gap-1.5 rounded-lg border border-fd-border bg-fd-background px-2.5 text-xs font-medium text-fd-foreground transition-colors hover:bg-fd-accent hover:text-fd-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fd-primary/30 data-[open=true]:bg-fd-accent data-[open=true]:text-fd-accent-foreground',
|
||||
triggerClassName
|
||||
)}
|
||||
>
|
||||
{selected?.icon && (
|
||||
<span className={cn('flex size-5 shrink-0 items-center justify-center [&_svg]:size-4', iconClassName)}>
|
||||
{selected.icon}
|
||||
</span>
|
||||
)}
|
||||
<span className="min-w-0 truncate">{selected?.label}</span>
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
'ms-auto size-3.5 shrink-0 text-fd-muted-foreground transition-transform',
|
||||
open && 'rotate-180'
|
||||
)}
|
||||
/>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
align={align}
|
||||
role="listbox"
|
||||
className={cn(
|
||||
'flex min-w-[11rem] flex-col gap-1 overflow-hidden p-1',
|
||||
contentClassName
|
||||
)}
|
||||
>
|
||||
{options.map(renderOption)}
|
||||
</PopoverContent>
|
||||
</div>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user