20260526152617

This commit is contained in:
oneao committed 2026-05-26 15:26:17 +08:00
1 parent 843529e57a
commit 223435efa6
10 files changed
+1143 -1

No files matched your search

@@ -0,0 +1,61 @@
import React from 'react'
import styles from './styles.module.css'
export interface LinkCardProps {
title: string
description: string
href: string
icon?: string
}
export default function LinkCard({
title,
description,
href,
icon,
}: LinkCardProps) {
let favicon = icon
if (!favicon) {
try {
const hostname = new URL(href).hostname
favicon = `https://www.google.com/s2/favicons?domain=${hostname}&sz=128`
}
catch {
favicon = undefined
}
}
return (
<a
className={styles.card}
href={href}
target="_blank"
rel="noopener noreferrer"
>
<div className={styles.header}>
{favicon
? (
<img
src={favicon}
alt={title}
className={styles.icon}
/>
)
: (
<div className={styles.placeholder}>
{title.charAt(0)}
</div>
)}
<span className={styles.title}>
{title}
</span>
</div>
<div className={styles.description}>
{description}
</div>
</a>
)
}
@@ -0,0 +1,14 @@
import React from 'react'
import styles from './styles.module.css'
interface Props {
children: React.ReactNode
}
export default function LinkGrid({ children }: Props) {
return (
<div className={styles.grid}>
{children}
</div>
)
}
@@ -0,0 +1,98 @@
import React, { useState, useMemo } from 'react'
import LinkCard, { LinkCardProps } from './LinkCard'
import styles from './styles.module.css'
export interface GroupedLinkCardProps {
groupName: string
items: LinkCardProps[]
}
interface SearchableLinkGridProps {
groups: GroupedLinkCardProps[]
}
export default function SearchableLinkGrid({ groups }: SearchableLinkGridProps) {
const [searchTerm, setSearchTerm] = useState('')
const filteredGroups = useMemo(() => {
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),
),
})).filter(group => group.items.length > 0)
}, [searchTerm, groups])
const totalItems = groups.reduce((sum, group) => sum + group.items.length, 0)
const filteredTotalItems = filteredGroups.reduce((sum, group) => sum + group.items.length, 0)
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>
))
) : (
<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>
)
}
@@ -0,0 +1,231 @@
.grid {
display: grid;
gap: 16px;
margin-top: 16px;
/* 默认桌面4列 */
grid-template-columns: repeat(4, minmax(0, 1fr));
}
/* 平板 */
@media (max-width: 1200px) {
.grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
/* 小平板 */
@media (max-width: 996px) {
.grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
/* 手机 */
@media (max-width: 576px) {
.grid {
grid-template-columns: 1fr;
}
}
/* 卡片 */
.card {
display: flex;
flex-direction: column;
padding: 18px;
border-radius: 14px;
border: 1px solid var(--ifm-color-emphasis-200);
background: var(--ifm-card-background-color);
color: inherit;
text-decoration: none;
transition:
transform 0.2s ease,
border-color 0.2s ease,
box-shadow 0.2s ease;
}
.card:hover {
color: inherit;
text-decoration: none;
transform: translateY(-3px);
border-color: var(--ifm-color-primary);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.08);
}
/* 标题区域 */
.header {
display: flex;
align-items: center;
gap: 12px;
}
/* 图标 */
.icon {
width: 32px;
height: 32px;
object-fit: contain;
flex-shrink: 0;
}
.placeholder {
width: 40px;
height: 40px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
background: var(--ifm-color-primary);
color: white;
font-size: 18px;
font-weight: 700;
flex-shrink: 0;
}
/* 标题 */
.title {
font-size: 17px;
font-weight: 600;
line-height: 1.4;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 描述 */
.description {
margin-top: 14px;
font-size: 14px;
line-height: 1.6;
color: var(--ifm-color-emphasis-700);
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
/* 搜索容器 */
.searchContainer {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 24px;
width: 100%;
}
.searchWrapper {
flex: 1;
display: flex;
align-items: center;
background: var(--ifm-card-background-color);
border: 1px solid var(--ifm-color-emphasis-200);
border-radius: 12px;
padding: 2px 2px 2px 16px;
transition:
border-color 0.2s ease,
box-shadow 0.2s ease;
}
.searchWrapper:focus-within {
border-color: var(--ifm-color-primary);
box-shadow: 0 0 0 3px var(--ifm-color-primary-alpha-10);
}
.searchIcon {
color: var(--ifm-color-emphasis-400);
margin-right: 12px;
flex-shrink: 0;
}
.searchInput {
flex: 1;
width: 100%;
padding: 12px 16px 12px 0;
border: none;
border-radius: 0 10px 10px 0;
font-size: 15px;
background: transparent;
color: var(--ifm-font-color-base);
}
.searchInput:focus {
outline: none;
}
.searchInput::placeholder {
color: var(--ifm-color-emphasis-500);
}
.searchCount {
font-size: 13px;
color: var(--ifm-color-emphasis-500);
white-space: nowrap;
padding: 6px 12px;
background: var(--ifm-color-emphasis-100);
border-radius: 20px;
}
/* 分组样式 */
.group {
margin-bottom: 32px;
}
.group:last-child {
margin-bottom: 0;
}
.groupTitle {
font-size: 18px;
font-weight: 700;
color: var(--ifm-color-emphasis-800);
margin-bottom: 20px;
padding-left: 14px;
border-left: 4px solid var(--ifm-color-primary);
}
/* 无结果状态 */
.noResults {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
color: var(--ifm-color-emphasis-600);
}
.noResultsIcon {
color: var(--ifm-color-emphasis-400);
margin-bottom: 16px;
}
.noResults p {
margin: 0;
font-size: 15px;
}