Skip to content

侧边栏筛选 ​

filters 让你在浏览页的侧边栏里加自己的筛选条件——比如站点的分类、元素、状态。

ts
async filters(contentType: ContentType): Promise<SourceFilterGroup[]>

这个方法不需要在 capabilities 里声明,实现了就会用。

返回什么 ​

ts
[
	{
		id: 'category',
		header: 'categories',
		options: [
			{ id: '4780', label: '数据包' },
			{ id: '12', label: '科技' },
		],
	},
]
字段说明
id组的 id,唯一。回传时作为 request.filters 的键
header组的标题。命中内置名字时会合并(见下)
options[].id选项 id,必须稳定
options[].label显示名,可以翻译

与内置分组合并 ​

启动器侧边栏有几个内置分组:

  • categories(分类)
  • features(特性)
  • performance impact(性能影响)

你的 header 命中其中一个时,你的选项会并进那一个分组,而不是多出一个同名分组:

ts
// header 是 'categories' → 和 Modrinth 的分类合并成一个「分类」栏,选项去重
{ id: 'category', header: 'categories', options: [...] }

// header 是别的 → 单独成一组
{ id: 'elements', header: '元素', options: [...] }

这正是要的效果

「分类」在侧边栏只出现一次,里面同时有 Modrinth 的分类和你的分类。如果每个源都出一个「分类」栏,用户会看到好几个一模一样的标题。

官方的 CurseForge 源就把它的分类用 header: 'categories' 声明,于是和 Modrinth 的合并在了一起。

回传给 search / browse ​

用户勾选的结果通过 request.filters 回来:

ts
// 用户在 category 组里勾了 4780 和 12
request.filters // { category: ['4780', '12'] }

键是你在 filters() 里给的组 id,值是勾选的选项 id 数组。

ts
async search(request: ContentQuery): Promise<ContentPage> {
	const params = new URLSearchParams({ q: request.query ?? '' })

	const categoryIds = request.filters?.category
	if (Array.isArray(categoryIds) && categoryIds.length > 0) {
		params.set('categories', categoryIds.join(','))
	}

	// ...
}

两条铁律 ​

一、选项 id 必须稳定

options[].id 是回传给你做筛选的。它必须是一个不变的值——用站点自己的数字 id 最省事。

ts
// ✅ 用站点的 id
{ id: '4780', label: '数据包' }

// ❌ 用显示名/id 混用,翻译一改筛选就失灵
{ id: '数据包', label: '数据包' }

二、只有 label 该翻译

label 是给人看的,可以随 api.i18n.locale 翻译。id 绝对不能翻——翻了之后用户勾选回传的 id 和你期望的对不上,筛选直接失效。

ts
// ✅ 翻译 label,保留 id
const zh = api.i18n.locale.toLowerCase().startsWith('zh')
options.map((option) => ({
	id: option.id,
	label: zh ? CATEGORY_ZH[option.id] ?? option.label : option.label,
}))

详见多语言。

分组按内容类型给 ​

filters 会收到 contentType——因为不同内容类型的筛选条件不同:

ts
async filters(contentType: ContentType): Promise<SourceFilterGroup[]> {
	if (contentType === 'mod') {
		return [
			{ id: 'category', header: 'categories', options: modCategories },
			{ id: 'loader', header: 'loaders', options: loaders },
		]
	}
	if (contentType === 'modpack') {
		return [{ id: 'category', header: 'categories', options: packCategories }]
	}
	return []
}

返回空数组表示「这个类型没有额外筛选」。

完整例子 ​

ts
api.register({
	async filters(contentType: ContentType): Promise<SourceFilterGroup[]> {
		if (contentType !== 'mod') return []

		const categories = await get<any>('/categories')

		return [
			{
				id: 'category',
				header: 'categories',
				options: categories.map((category: any) => ({
					id: String(category.id), // 稳定
					label: category.name, // 可翻译
				})),
			},
		]
	},

	async search(request: ContentQuery): Promise<ContentPage> {
		const params = new URLSearchParams({
			q: request.query ?? '',
			page: String(request.page),
			limit: String(request.limit),
		})

		const selected = request.filters?.category
		if (Array.isArray(selected) && selected.length > 0) {
			params.set('category', selected.join(','))
		}

		const page = await get<any>(`/search?${params}`)
		return { items: page.hits.map(toCard), total: page.total }
	},
})

筛选是「或」还是「与」 ​

启动器不做判断——它只把勾选的 id 数组原样给你。同一个组内多个选项之间是什么关系(或 / 与),由你的 API 决定,你只要把 id 传给它就行。

常见错误 ​

现象原因
侧边栏出现两个「分类」header 没写 'categories'(大小写也要一致)
勾了没反应没读 request.filters,或者键名和组 id 对不上
换语言后筛选失灵把 id 也翻译了
选项重复出现和内置分组同名但没合并——检查 header 拼写

接下来:整合包。

Celestial Launcher 基于 Modrinth App 构建,遵循其开源许可。