20260601173222
This commit is contained in:
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 {
|
||||
|
||||
Reference in new issue
Block a user