Merge remote-tracking branch 'origin/main'

This commit is contained in:
t0ng7u
2026-05-26 01:22:56 +08:00
20 changed files with 488 additions and 146 deletions
@@ -163,7 +163,11 @@ const API_DEMOS: ApiDemoConfig[] = [
const CYCLE_INTERVAL = 4500
const TRANSITION_MS = 220
export function HeroTerminalDemo() {
interface HeroTerminalDemoProps {
className?: string
}
export function HeroTerminalDemo(props: HeroTerminalDemoProps) {
const [activeIndex, setActiveIndex] = useState(0)
const [transitioning, setTransitioning] = useState(false)
const intervalRef = useRef<ReturnType<typeof setInterval>>(undefined)
@@ -202,7 +206,7 @@ export function HeroTerminalDemo() {
const accent = ACCENT_CLASSES[demo.accent]
return (
<div className='mx-auto mt-16 w-full max-w-2xl'>
<div className={cn('mx-auto w-full max-w-2xl', props.className)}>
<div
className={cn(
'overflow-hidden rounded-2xl border backdrop-blur-sm',
+191 -58
View File
@@ -17,9 +17,11 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { Link } from '@tanstack/react-router'
import { ArrowRight } from 'lucide-react'
import { ArrowRight, BookOpen } from 'lucide-react'
import { CherryStudio } from '@lobehub/icons'
import { useTranslation } from 'react-i18next'
import { Button } from '@/components/ui/button'
import { useStatus } from '@/hooks/use-status'
import { HeroTerminalDemo } from '../hero-terminal-demo'
interface HeroProps {
@@ -27,11 +29,59 @@ interface HeroProps {
isAuthenticated?: boolean
}
// Stylized three-dots indicator representing "More"
const MoreIcon = () => (
<svg
className='size-6 shrink-0 text-muted-foreground/60 transition-colors group-hover:text-foreground'
viewBox='0 0 24 24'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<circle cx='6' cy='12' r='2' fill='currentColor' />
<circle cx='12' cy='12' r='2' fill='currentColor' />
<circle cx='18' cy='12' r='2' fill='currentColor' />
</svg>
)
export function Hero(props: HeroProps) {
const { t } = useTranslation()
const { status } = useStatus()
const docsUrl = (status?.docs_link as string | undefined) || 'https://docs.newapi.pro'
const renderDocsButton = () => {
const isExternal = docsUrl.startsWith('http')
if (isExternal) {
return (
<Button
variant='outline'
className='group border-border/50 hover:border-border hover:bg-muted/50 rounded-lg h-11 px-5 text-sm font-medium inline-flex items-center gap-1.5'
render={
<a
href={docsUrl}
target='_blank'
rel='noopener noreferrer'
/>
}
>
<BookOpen className='size-4 text-muted-foreground/80 group-hover:text-foreground transition-colors duration-200' />
<span>{t('Docs')}</span>
</Button>
)
}
return (
<Button
variant='outline'
className='group border-border/50 hover:border-border hover:bg-muted/50 rounded-lg h-11 px-5 text-sm font-medium inline-flex items-center gap-1.5'
render={<Link to={docsUrl} />}
>
<BookOpen className='size-4 text-muted-foreground/80 group-hover:text-foreground transition-colors duration-200' />
<span>{t('Docs')}</span>
</Button>
)
}
return (
<section className='relative z-10 flex flex-col items-center overflow-hidden px-6 pt-28 pb-16 md:pt-36 md:pb-24'>
<section className='relative z-10 overflow-hidden px-6 pt-24 pb-16 md:pt-32 md:pb-24 lg:pt-36 lg:pb-28'>
{/* Radial gradient background */}
<div
aria-hidden
@@ -50,63 +100,146 @@ export function Hero(props: HeroProps) {
className='absolute inset-0 -z-10 bg-[linear-gradient(to_right,var(--border)_1px,transparent_1px),linear-gradient(to_bottom,var(--border)_1px,transparent_1px)] [mask-image:radial-gradient(ellipse_60%_50%_at_50%_30%,black_20%,transparent_100%)] bg-[size:4rem_4rem] opacity-[0.08]'
/>
<div className='flex max-w-3xl flex-col items-center text-center'>
<h1
className='landing-animate-fade-up text-[clamp(2rem,5.5vw,3.5rem)] leading-[1.15] font-bold tracking-tight'
style={{ animationDelay: '0ms' }}
>
{t('Unified API Gateway for')}
<br />
<span className='bg-gradient-to-r from-blue-400 via-violet-400 to-purple-500 bg-clip-text text-transparent'>
{t('All Your AI Models')}
</span>
</h1>
<p
className='landing-animate-fade-up text-muted-foreground/80 mt-5 max-w-lg text-base leading-relaxed opacity-0 md:text-lg'
style={{ animationDelay: '80ms' }}
>
{t(
'Power AI applications, manage digital assets, connect the Future'
)}
</p>
<div
className='landing-animate-fade-up mt-8 flex items-center gap-3 opacity-0'
style={{ animationDelay: '160ms' }}
>
{props.isAuthenticated ? (
<Button
className='group rounded-lg'
render={<Link to='/dashboard' />}
>
{t('Go to Dashboard')}
<ArrowRight className='ml-1 size-3.5 transition-transform duration-200 group-hover:translate-x-0.5' />
</Button>
) : (
<>
<Button
className='group rounded-lg'
render={<Link to='/sign-up' />}
>
{t('Get Started')}
<ArrowRight className='ml-1 size-3.5 transition-transform duration-200 group-hover:translate-x-0.5' />
</Button>
<Button
variant='outline'
className='border-border/50 hover:border-border hover:bg-muted/50 rounded-lg'
render={<Link to='/pricing' />}
>
{t('View Pricing')}
</Button>
</>
)}
</div>
</div>
<div className='mx-auto grid max-w-6xl grid-cols-1 items-start gap-12 lg:grid-cols-12 lg:gap-8'>
{/* Left Column: Title, description, action buttons and application support */}
<div className='flex flex-col items-start text-left lg:col-span-6'>
{/* Top Pill Badge */}
<div
className='landing-animate-fade-up mb-5 inline-flex items-center gap-1.5 rounded-full border border-blue-500/20 bg-blue-500/5 px-3 py-1.5 text-[11px] font-medium text-blue-600 dark:border-blue-400/20 dark:bg-blue-400/5 dark:text-blue-400 opacity-0 shadow-xs'
style={{ animationDelay: '0ms' }}
>
<span className='relative flex size-1.5'>
<span className='absolute inline-flex h-full w-full animate-ping rounded-full bg-blue-400 opacity-75' />
<span className='relative inline-flex size-1.5 rounded-full bg-blue-500 dark:bg-blue-400' />
</span>
<span>{t('AI Application Infrastructure Foundation')}</span>
</div>
<div
className='landing-animate-fade-up w-full opacity-0'
style={{ animationDelay: '300ms' }}
>
<HeroTerminalDemo />
<h1
className='landing-animate-fade-up text-[clamp(2.25rem,4.5vw,3.25rem)] leading-[1.15] font-bold tracking-tight'
style={{ animationDelay: '60ms' }}
>
{t('Unified API Gateway for')}
<br />
<span className='bg-gradient-to-r from-blue-400 via-violet-400 to-purple-500 bg-clip-text text-transparent'>
{t('Vast Range of AI Models')}
</span>
</h1>
<p
className='landing-animate-fade-up text-muted-foreground/80 mt-5 max-w-xl text-base leading-relaxed opacity-0 md:text-[15px]'
style={{ animationDelay: '120ms' }}
>
{t(
'Access a vast selection of models via a standard, unified API protocol. Power AI applications, manage digital assets, and connect the Future.'
)}
</p>
<div
className='landing-animate-fade-up mt-8 flex flex-wrap items-center gap-3 opacity-0'
style={{ animationDelay: '180ms' }}
>
{props.isAuthenticated ? (
<>
<Button
className='group rounded-lg h-11 px-5 text-sm font-medium'
render={<Link to='/dashboard' />}
>
{t('Go to Dashboard')}
<ArrowRight className='ml-1.5 size-4 transition-transform duration-200 group-hover:translate-x-0.5' />
</Button>
{renderDocsButton()}
</>
) : (
<>
<Button
className='group rounded-lg h-11 px-5 text-sm font-medium'
render={<Link to='/sign-up' />}
>
{t('Get Started')}
<ArrowRight className='ml-1.5 size-4 transition-transform duration-200 group-hover:translate-x-0.5' />
</Button>
<Button
variant='outline'
className='border-border/50 hover:border-border hover:bg-muted/50 rounded-lg h-11 px-5 text-sm font-medium'
render={<Link to='/pricing' />}
>
{t('View Pricing')}
</Button>
{renderDocsButton()}
</>
)}
</div>
{/* Supported Apps (参考图二样式,进行卡片化和信息扩充设计,增加视觉高度) */}
<div
className='landing-animate-fade-up mt-10 w-full max-w-xl opacity-0'
style={{ animationDelay: '240ms' }}
>
<div className='flex flex-col gap-1 mb-4'>
<span className='text-[10px] font-bold tracking-[0.15em] text-muted-foreground/50 uppercase'>
{t('Supported Applications')}
</span>
<p className='text-xs text-muted-foreground/60 leading-relaxed'>
{t('Supports one-click configuration and perfectly adapts to NewAPI multi-protocol configuration.')}
</p>
</div>
<div className='flex flex-wrap items-center gap-3'>
{/* Cherry Studio */}
<a
href='https://cherry-ai.com'
target='_blank'
rel='noopener noreferrer'
className='group flex items-center gap-3 rounded-full border border-border/40 bg-muted/15 px-5 py-2.5 text-sm font-medium text-foreground/80 shadow-[0_1px_2.5px_rgba(0,0,0,0.01)] backdrop-blur-xs transition-all duration-300 hover:border-border hover:bg-muted/30 hover:text-foreground hover:scale-[1.02]'
>
<CherryStudio.Color size={24} className='shrink-0' />
<span>Cherry Studio</span>
</a>
{/* CC Switch */}
<a
href='https://ccswitch.io'
target='_blank'
rel='noopener noreferrer'
className='group flex items-center gap-3 rounded-full border border-border/40 bg-muted/15 px-5 py-2.5 text-sm font-medium text-foreground/80 shadow-[0_1px_2.5px_rgba(0,0,0,0.01)] backdrop-blur-xs transition-all duration-300 hover:border-border hover:bg-muted/30 hover:text-foreground hover:scale-[1.02]'
>
<img
src='https://ccswitch.io/favicon.png'
alt='CC Switch'
className='size-6 shrink-0 rounded-md object-contain'
onError={(e) => {
// Fallback to a styled text avatar if the remote favicon fails to load in sandbox or local environments
e.currentTarget.style.display = 'none'
const fallback = e.currentTarget.nextSibling as HTMLElement
if (fallback) fallback.style.display = 'flex'
}}
/>
<span
style={{ display: 'none' }}
className='size-6 shrink-0 items-center justify-center rounded-md bg-blue-500/10 text-[10px] font-bold text-blue-600 dark:bg-blue-400/10 dark:text-blue-400'
>
CC
</span>
<span>CC Switch</span>
</a>
{/* "更多" */}
<div
className='group flex items-center gap-2.5 rounded-full border border-border/40 bg-muted/15 px-5 py-2.5 text-sm font-medium text-foreground/55 shadow-[0_1px_2.5px_rgba(0,0,0,0.01)] backdrop-blur-xs transition-all duration-300 hover:border-border hover:bg-muted/30 hover:text-foreground hover:scale-[1.02] cursor-default'
>
<MoreIcon />
<span>{t('More Apps')}</span>
</div>
</div>
</div>
</div>
{/* Right Column: Hero Terminal API Demo */}
<div
className='landing-animate-fade-up flex justify-center w-full opacity-0 lg:col-span-6'
style={{ animationDelay: '320ms' }}
>
<HeroTerminalDemo className='mt-8 lg:mt-0' />
</div>
</div>
</section>
)
+1 -1
View File
@@ -42,7 +42,7 @@ export async function getApiKeys(
// Search API keys by keyword or token (with pagination)
export async function searchApiKeys(
params: SearchApiKeysParams
): Promise<{ success: boolean; message?: string; data?: ApiKey[] }> {
): Promise<GetApiKeysResponse> {
const { keyword = '', token = '', p, size } = params
const queryParams = new URLSearchParams()
if (keyword) queryParams.set('keyword', keyword)
+62 -34
View File
@@ -30,6 +30,7 @@ import {
getSortedRowModel,
useReactTable,
} from '@tanstack/react-table'
import { useDebounce } from '@/hooks'
import { Database } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
@@ -43,6 +44,7 @@ import {
EmptyMedia,
EmptyTitle,
} from '@/components/ui/empty'
import { Input } from '@/components/ui/input'
import { Skeleton } from '@/components/ui/skeleton'
import {
DISABLED_ROW_DESKTOP,
@@ -207,9 +209,35 @@ export function ApiKeysTable() {
navigate: route.useNavigate(),
pagination: { defaultPage: 1, defaultPageSize: 20 },
globalFilter: { enabled: true, key: 'filter' },
columnFilters: [{ columnId: 'status', searchKey: 'status', type: 'array' }],
columnFilters: [
{ columnId: 'status', searchKey: 'status', type: 'array' },
{ columnId: '_tokenSearch', searchKey: 'token', type: 'string' },
],
})
const tokenFilterFromUrl =
(columnFilters.find((f) => f.id === '_tokenSearch')?.value as string) || ''
const [tokenFilterInput, setTokenFilterInput] = useState(tokenFilterFromUrl)
const debouncedTokenFilter = useDebounce(tokenFilterInput, 500)
useEffect(() => {
setTokenFilterInput(tokenFilterFromUrl)
}, [tokenFilterFromUrl])
useEffect(() => {
if (debouncedTokenFilter !== tokenFilterFromUrl) {
onColumnFiltersChange((prev) => {
const filtered = prev.filter((f) => f.id !== '_tokenSearch')
return debouncedTokenFilter
? [...filtered, { id: '_tokenSearch', value: debouncedTokenFilter }]
: filtered
})
}
}, [debouncedTokenFilter, tokenFilterFromUrl, onColumnFiltersChange])
const tokenFilter = tokenFilterFromUrl
const shouldSearch = Boolean(globalFilter?.trim() || tokenFilter.trim())
// Fetch data with React Query
// eslint-disable-next-line @tanstack/query/exhaustive-deps
const { data, isLoading, isFetching } = useQuery({
@@ -218,32 +246,31 @@ export function ApiKeysTable() {
pagination.pageIndex + 1,
pagination.pageSize,
globalFilter,
tokenFilter,
refreshTrigger,
],
queryFn: async () => {
// If there's a global filter, use search
const hasFilter = globalFilter?.trim()
if (hasFilter) {
const result = await searchApiKeys({ keyword: globalFilter })
if (!result.success) {
toast.error(result.message || t(ERROR_MESSAGES.SEARCH_FAILED))
return { items: [], total: 0 }
}
return {
items: result.data || [],
total: result.data?.length || 0,
}
}
// Otherwise use pagination
const result = await getApiKeys({
p: pagination.pageIndex + 1,
size: pagination.pageSize,
})
const result = shouldSearch
? await searchApiKeys({
keyword: globalFilter,
token: tokenFilter,
p: pagination.pageIndex + 1,
size: pagination.pageSize,
})
: await getApiKeys({
p: pagination.pageIndex + 1,
size: pagination.pageSize,
})
if (!result.success) {
toast.error(result.message || t(ERROR_MESSAGES.LOAD_FAILED))
toast.error(
result.message ||
t(
shouldSearch
? ERROR_MESSAGES.SEARCH_FAILED
: ERROR_MESSAGES.LOAD_FAILED
)
)
return { items: [], total: 0 }
}
@@ -272,13 +299,7 @@ export function ApiKeysTable() {
onRowSelectionChange: setRowSelection,
onSortingChange: setSorting,
onColumnVisibilityChange: setColumnVisibility,
globalFilterFn: (row, _columnId, filterValue) => {
const name = String(row.getValue('name')).toLowerCase()
const key = String(row.original.key).toLowerCase()
const searchValue = String(filterValue).toLowerCase()
return name.includes(searchValue) || key.includes(searchValue)
},
globalFilterFn: () => true,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
@@ -288,10 +309,8 @@ export function ApiKeysTable() {
onPaginationChange,
onGlobalFilterChange,
onColumnFiltersChange,
manualPagination: !globalFilter,
pageCount: globalFilter
? Math.ceil((data?.total || 0) / pagination.pageSize)
: Math.ceil((data?.total || 0) / pagination.pageSize),
manualPagination: true,
pageCount: Math.ceil((data?.total || 0) / pagination.pageSize),
})
const pageCount = table.getPageCount()
@@ -311,7 +330,16 @@ export function ApiKeysTable() {
)}
skeletonKeyPrefix='api-keys-skeleton'
toolbarProps={{
searchPlaceholder: t('Filter by name or key...'),
searchPlaceholder: t('Filter by name...'),
additionalSearch: (
<Input
placeholder={t('Filter by API key...')}
aria-label={t('Filter by API key...')}
value={tokenFilterInput}
onChange={(e) => setTokenFilterInput(e.target.value)}
className='w-full sm:w-50 lg:w-60'
/>
),
filters: [
{
columnId: 'status',