@infinilabs/search-results
v0.0.16
Published
搜索结果列表/图片组/视频组展示组件。
Readme
@infinilabs/search-results
搜索结果列表/图片组/视频组展示组件。
- 支持列表结果(标题、摘要、面包屑、作者/时间、缩略图、文件类型徽标等)
- 支持媒体宫格(图片/视频,支持列数、底部动作)
- 对外提供 3 个组件:默认组件 + 图片组组件 + 视频组组件,业务侧自行选择使用哪个
- 单个组件只接收一个
section入参;多段数据由业务侧自行map渲染多个组件
安装
pnpm add @infinilabs/search-results对等依赖(peerDependencies)需要由业务侧提供:
reactreact-dom
使用方式
1) 业务侧选择组件(推荐)
默认组件 SearchResults 的 section 类型最宽;当你明确知道当前数据是“图片组/视频组”时,推荐使用 SearchResultsImageGroup / SearchResultsVideoGroup,获得更好的类型约束。
import SearchResults, {
SearchResultsImageGroup,
SearchResultsVideoGroup,
type SearchResultImageGroupItem,
type SearchResultVideoGroupItem,
type SearchResultsProps,
type SearchResultsRecord
} from "@infinilabs/search-results";
export function SectionRenderer({
section
}: {
section: SearchResultsProps["section"];
}) {
if (Array.isArray(section)) {
const first = section[0] as SearchResultsRecord | undefined;
const recordType =
typeof first?.metadata?.content_type === "string"
? first.metadata.content_type.toLowerCase()
: undefined;
if (recordType === "image") return <SearchResultsImageGroup section={section as SearchResultsRecord[]} />;
if (recordType === "video") return <SearchResultsVideoGroup section={section as SearchResultsRecord[]} />;
}
if (typeof section === "object" && section && "type" in section) {
const t = (section as { type?: unknown }).type;
if (t === "imageGroup") return <SearchResultsImageGroup section={section as SearchResultImageGroupItem} />;
if (t === "videoGroup") return <SearchResultsVideoGroup section={section as SearchResultVideoGroupItem} />;
}
return <SearchResults section={section} />;
}2) 传 record / record[](最接近真实数据)
import SearchResults, {
SearchResultsImageGroup,
SearchResultsVideoGroup,
type SearchResultsRecord
} from "@infinilabs/search-results";
const doc: SearchResultsRecord = {
title: "Q3 Business Report",
type: "PDF",
category: "report",
url: "https://drive.google.com/file/d/abc123/view",
thumbnail: "https://example.com/images/report_thumbnail.jpg",
source: { name: "My Hugo Site", type: "connector", id: "e806831dacc3" }
};
const images: SearchResultsRecord[] = [
{
id: "img-0",
title: "黑色壁纸全屏",
metadata: { content_type: "image" },
thumbnail: "https://example.com/image_thumb_0.png",
url: "https://example.com/image_0.png",
source: { name: "壁纸", type: "connector", id: "wallpaper" }
}
];
const videos: SearchResultsRecord[] = [
{
id: "video-0",
title: "城市夜景延时摄影",
metadata: { content_type: "video" },
thumbnail: "https://picsum.photos/seed/video-thumb-0/640/360",
url: "https://example.com/video/0",
source: { name: "视频库", type: "connector", id: "connector-video" }
}
];
export default function Demo() {
return (
<div className="space-y-10">
<SearchResults
section={doc}
onRecordClick={(record) => {
if (record.url) window.open(record.url);
}}
/>
<SearchResultsImageGroup
section={images}
footerAction={{ label: "所有图片 >", href: "/search?type=image" }}
onRecordClick={(record) => {
if (record.url) window.open(record.url);
}}
/>
<SearchResultsVideoGroup
section={videos}
footerAction={{ label: "所有视频 >", href: "/search?type=video" }}
onRecordClick={(record) => {
if (record.url) window.open(record.url);
}}
/>
</div>
);
}说明:
onRecordClick用于业务侧接管点击(埋点/跳转/弹窗等),组件内部会让整条结果区域可点击- 当
onRecordClick存在时,组件会优先走回调,不再走默认href跳转
3) 传 imageGroup / videoGroup(业务侧自己组装 items)
import {
SearchResultsImageGroup,
SearchResultsVideoGroup,
type SearchResultImageGroupItem,
type SearchResultVideoGroupItem
} from "@infinilabs/search-results";
const imageGroup: SearchResultImageGroupItem = {
type: "imageGroup",
id: "image-group-0",
title: "图片组",
columns: 3,
footerAction: { label: "所有图片 >", href: "/search?type=image" },
items: [
{
type: "image",
id: "img-group-0",
title: "黑色壁纸全屏",
imageUrl: "https://example.com/image_0.png",
subtitle: "壁纸",
onClick: () => console.log("clicked image item")
}
]
};
const videoGroup: SearchResultVideoGroupItem = {
type: "videoGroup",
id: "video-group-0",
title: "视频组",
columns: 3,
footerAction: { label: "所有视频 >", href: "/search?type=video" },
items: [
{
type: "media",
id: "video-group-item-0",
mediaType: "video",
title: "城市夜景延时摄影",
href: "https://example.com/video/0",
cover: "https://picsum.photos/seed/video-thumb-0/640/360",
breadcrumbs: ["视频库", "视频"],
onClick: () => console.log("clicked video item")
}
]
};
export default function Demo() {
return (
<div className="space-y-10">
<SearchResultsImageGroup section={imageGroup} />
<SearchResultsVideoGroup section={videoGroup} />
</div>
);
}4) 多段数据(业务侧 map 渲染多个组件)
import SearchResults, {
SearchResultsImageGroup,
SearchResultsVideoGroup,
type SearchResultsProps
} from "@infinilabs/search-results";
export default function Demo({ data }: { data: Array<SearchResultsProps["section"]> }) {
return (
<div className="space-y-10">
{data.map((section, index) => {
if (Array.isArray(section)) {
const first = section[0] as SearchResultsRecord | undefined;
const recordType =
typeof first?.metadata?.content_type === "string"
? first.metadata.content_type.toLowerCase()
: undefined;
if (recordType === "image") return <SearchResultsImageGroup key={index} section={section} />;
if (recordType === "video") return <SearchResultsVideoGroup key={index} section={section} />;
}
return <SearchResults key={index} section={section} />;
})}
</div>
);
}5) 点击事件(埋点/统一处理)
组件内部触发点击时,会先调用 item 自身的 onClick(如果有),再调用 onItemClick。
import SearchResults, {
type SearchResultListItem,
type SearchResultsItem
} from "@infinilabs/search-results";
export default function Demo({ items }: { items: SearchResultsItem[] }) {
const listItems = items.filter((i): i is SearchResultListItem => i.type === "result");
return (
<SearchResults
section={{ type: "section", layout: "list", items: listItems }}
onItemClick={(item) => {
console.log("clicked", item);
}}
/>
);
}6) 底部动作(“查看全部”)
footerAction 可以写在:
SearchResults/SearchResultsImageGroup/SearchResultsVideoGroup的footerActionprop 上(如果 section 本身没提供,会自动补上)SearchResultsSection.footerAction/imageGroup.footerAction/videoGroup.footerAction上(优先级更高)
参数说明(Props)
import type { SearchResultsProps } from "@infinilabs/search-results";| 参数 | 类型 | 说明 |
|---|---|---|
| section | SearchResultsProps["section"] | 输入数据(section / item / record / 数组) |
| className | string | 根容器 className |
| theme | "light" \| "dark" \| "auto" | 组件主题(可选) |
| footerAction | SearchResultsAction | 底部“查看全部”动作(可选) |
| onRecordClick | (record: SearchResultsRecord, index: number) => void | record 输入时的点击回调 |
| onItemClick | (item: SearchResultsItem) => void | 统一点击回调 |
主题适配(light / dark)
组件随包产出样式(业务侧无需安装 Tailwind)。暗色样式基于 .dark class 驱动(语义等同于 Tailwind dark: 变体)。
你有两种接入方式:
方式 1:跟随业务侧主题(推荐)
业务侧在外层容器挂载/移除 dark class(例如放在页面根节点或局部容器),组件会自动跟随:
<div className={isDark ? "dark" : ""}>
<SearchResults section={data} />
</div>方式 2:组件级主题开关
通过 theme 让组件在自身根节点挂载 dark class(不依赖外层):
<SearchResults section={data} theme="light" />
<SearchResults section={data} theme="dark" />
<SearchResults section={data} theme="auto" />说明:
theme="auto"会根据prefers-color-scheme自动选择 light/dark- 如果业务侧把
dark挂在html上,组件无法在局部“强制 light”;想局部控制请把dark放到局部容器
工具函数
import { itemsToSections, recordsToItems } from "@infinilabs/search-results";类型导出
import type {
SearchResultsAction,
SearchResultsTheme,
SearchResultsItem,
SearchResultsSection,
SearchResultsRecord,
SearchResultListItem,
SearchResultImageItem,
SearchResultMediaItem,
SearchResultImageGroupItem,
SearchResultVideoGroupItem,
SearchResultFileType
} from "@infinilabs/search-results";SearchResultFileType 支持:
pdfdocpptxlslinkwordtextunknown
