20260601173222

This commit is contained in:
oneao committed 2026-06-01 17:32:22 +08:00
1 parent 8b08d18010
commit 44dce232f4
5 files changed
+416 -149

No files matched your search

@@ -1,5 +1,6 @@
import React, { useState, useMemo } from 'react'
import LinkCard, { LinkCardProps } from './LinkCard'
import type { LinkCardProps } from './LinkCard'
import React, { useEffect, useMemo, useRef, useState } from 'react'
import LinkCard from './LinkCard'
import styles from './styles.module.css'
export interface GroupedLinkCardProps {
@@ -13,17 +14,61 @@ interface SearchableLinkGridProps {
export default function SearchableLinkGrid({ groups }: SearchableLinkGridProps) {
const [searchTerm, setSearchTerm] = useState('')
const [activeSection, setActiveSection] = useState('')
const sidebarRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const navbar = document.querySelector('.navbar')
const navbarHeight = navbar ? navbar.offsetHeight : 64
const threshold = navbarHeight + 32
if (sidebarRef.current) {
sidebarRef.current.style.top = `${threshold}px`
}
}, [])
useEffect(() => {
const navbar = document.querySelector('.navbar')
const navbarHeight = navbar ? navbar.offsetHeight : 64
const threshold = navbarHeight + 20
const observer = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
setActiveSection(entry.target.id)
}
}
},
{
rootMargin: `-${threshold}px 0px -80% 0px`,
threshold: 0.1,
},
)
groups.forEach((_, index) => {
const element = document.getElementById(`group-${index}`)
if (element) {
observer.observe(element)
}
})
return () => {
observer.disconnect()
}
}, [groups])
const filteredGroups = useMemo(() => {
if (!searchTerm.trim()) return groups
if (!searchTerm.trim())
return groups
const lowerTerm = searchTerm.toLowerCase()
return groups.map(group => ({
...group,
items: group.items.filter(
item =>
item.title.toLowerCase().includes(lowerTerm) ||
item.description.toLowerCase().includes(lowerTerm),
item.title.toLowerCase().includes(lowerTerm)
|| item.description.toLowerCase().includes(lowerTerm),
),
})).filter(group => group.items.length > 0)
}, [searchTerm, groups])
@@ -31,68 +76,99 @@ export default function SearchableLinkGrid({ groups }: SearchableLinkGridProps)
const totalItems = groups.reduce((sum, group) => sum + group.items.length, 0)
const filteredTotalItems = filteredGroups.reduce((sum, group) => sum + group.items.length, 0)
const scrollToSection = (index: number) => {
const element = document.getElementById(`group-${index}`)
if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'start' })
}
}
return (
<div>
<div className={styles.searchContainer}>
<div className={styles.searchWrapper}>
<svg
className={styles.searchIcon}
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="11" cy="11" r="8" />
<path d="m21 21-4.35-4.35" />
</svg>
<input
type="text"
placeholder="搜索资源..."
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
className={styles.searchInput}
/>
</div>
<span className={styles.searchCount}>
{filteredTotalItems} / {totalItems} 项
</span>
</div>
{filteredGroups.length > 0 ? (
filteredGroups.map((group, groupIndex) => (
<div key={groupIndex} className={styles.group}>
<h3 className={styles.groupTitle}>{group.groupName}</h3>
<div className={styles.grid}>
{group.items.map((item, itemIndex) => (
<LinkCard key={itemIndex} {...item} />
))}
</div>
<div className={styles.container}>
<div className={styles.mainContent}>
<div className={styles.searchContainer}>
<div className={styles.searchWrapper}>
<svg
className={styles.searchIcon}
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="11" cy="11" r="8" />
<path d="m21 21-4.35-4.35" />
</svg>
<input
type="text"
placeholder="搜索资源..."
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
className={styles.searchInput}
/>
</div>
))
) : (
<div className={styles.noResults}>
<svg
className={styles.noResultsIcon}
width="48"
height="48"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="11" cy="11" r="8" />
<path d="m21 21-4.35-4.35" />
<path d="M8 11s1.5-2 4-2 4 2 4 2" />
</svg>
<p>未找到匹配的资源</p>
<span className={styles.searchCount}>
{filteredTotalItems}
{' '}
/
{totalItems}
{' '}
项
</span>
</div>
)}
{filteredGroups.length > 0
? (
filteredGroups.map((group, groupIndex) => (
<div key={groupIndex} id={`group-${groupIndex}`} className={styles.group}>
<h3 className={styles.groupTitle}>{group.groupName}</h3>
<div className={styles.grid}>
{group.items.map((item, itemIndex) => (
<LinkCard key={itemIndex} {...item} />
))}
</div>
</div>
))
)
: (
<div className={styles.noResults}>
<svg
className={styles.noResultsIcon}
width="48"
height="48"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="11" cy="11" r="8" />
<path d="m21 21-4.35-4.35" />
<path d="M8 11s1.5-2 4-2 4 2 4 2" />
</svg>
<p>未找到匹配的资源</p>
</div>
)}
</div>
<nav className={styles.sidebar} ref={sidebarRef}>
<div className={styles.sidebarHeader}>快速导航</div>
<div className={styles.sidebarLinks}>
{groups.map((group, index) => (
<button
key={index}
onClick={() => scrollToSection(index)}
className={`${styles.sidebarLink} ${activeSection === `group-${index}` ? styles.active : ''}`}
>
{group.groupName}
</button>
))}
</div>
</nav>
</div>
)
}
}
@@ -1,3 +1,65 @@
/* 容器布局 */
.container {
display: flex;
gap: 24px;
}
.mainContent {
flex: 1;
}
/* 右侧导航 */
.sidebar {
width: 180px;
flex-shrink: 0;
position: sticky;
top: 2rem;
height: fit-content;
}
.sidebarHeader {
font-size: 14px;
font-weight: 600;
color: var(--ifm-color-emphasis-600);
margin-bottom: 12px;
padding-left: 8px;
}
.sidebarLinks {
display: flex;
flex-direction: column;
gap: 4px;
}
.sidebarLink {
display: block;
padding: 8px 12px;
font-size: 14px;
text-align: left;
border: none;
background: transparent;
color: var(--ifm-color-emphasis-700);
border-radius: 8px;
cursor: pointer;
transition:
background-color 0.2s ease,
color 0.2s ease;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.sidebarLink:hover {
background-color: var(--ifm-color-emphasis-100);
color: var(--ifm-color-emphasis-900);
}
.sidebarLink.active {
background-color: var(--ifm-color-primary);
color: white;
}
/* 网格布局 */
.grid {
display: grid;
gap: 16px;
@@ -28,6 +90,13 @@
}
}
/* 响应式:隐藏右侧导航 */
@media (max-width: 1200px) {
.sidebar {
display: none;
}
}
/* 卡片 */
.card {
@@ -194,6 +263,7 @@
.group {
margin-bottom: 32px;
scroll-margin-top: calc(var(--ifm-navbar-height, 64px) + 1rem);
}
.group:last-child {