feat(search): include user profiles in global search results

Add users group to global search API and frontend types
Query users by display name and link to their public profiles
Update system wordings for the new search group
This commit is contained in:
2026-05-04 16:04:58 +08:00
parent 8cb8190554
commit 504849c14a
5 changed files with 32 additions and 9 deletions

View File

@@ -9,7 +9,7 @@
- Home 首页路径为 `/`,用于聚合公开 Wiki 入口Logo 导航回到 Home用户可从 Home 进入核心资料、每日 CheckList、Life 和正在准备中的分区。 - Home 首页路径为 `/`,用于聚合公开 Wiki 入口Logo 导航回到 Home用户可从 Home 进入核心资料、每日 CheckList、Life 和正在准备中的分区。
- 桌面端使用侧边栏导航,侧边栏可折叠为图标栏;移动端继续使用抽屉式侧边栏。 - 桌面端使用侧边栏导航,侧边栏可折叠为图标栏;移动端继续使用抽屉式侧边栏。
- 全局顶部导航栏承载语言切换、通知、User Profile 和登录 / 退出等账号操作;除 User Profile 可展示用户名外,顶部操作以图标按钮呈现。 - 全局顶部导航栏承载语言切换、通知、User Profile 和登录 / 退出等账号操作;除 User Profile 可展示用户名外,顶部操作以图标按钮呈现。
- 全局顶部导航栏提供全站搜索。搜索结果按内容类型分组展示,覆盖 Pokemon、Habitats、Items、Ancient Artifacts、Recipes、Daily CheckList公开可见的 Life Post结果跳转到对应公开详情页页面锚点。 - 全局顶部导航栏提供全站搜索。搜索结果按内容类型分组展示,覆盖 Pokemon、Habitats、Items、Ancient Artifacts、Recipes、Daily CheckList公开可见的 Life Post 和公开用户 Profile;结果跳转到对应公开详情页页面锚点`/profile/:id`
- 管理入口用于维护全局配置、语言、系统文案、列表排序和每日 CheckList。 - 管理入口用于维护全局配置、语言、系统文案、列表排序和每日 CheckList。
## 技术栈 ## 技术栈
@@ -24,7 +24,7 @@
- `DESIGN.md` 是产品行为、数据结构和 API 暴露边界的单一事实来源。 - `DESIGN.md` 是产品行为、数据结构和 API 暴露边界的单一事实来源。
- API 只返回业务需要的字段不返回密码、token hash、验证 token、内部调试字段或不必要的元数据。 - API 只返回业务需要的字段不返回密码、token hash、验证 token、内部调试字段或不必要的元数据。
- 全局搜索 API 只返回公开浏览所需的最小结果字段结果类型、ID、展示标题、目标 URL、可选摘要和可选图片不返回编辑审计、权限、审核原因、内部字段或调试信息。 - 全局搜索 API 只返回公开浏览所需的最小结果字段结果类型、ID、展示标题、目标 URL、可选摘要和可选图片用户搜索结果只使用公开 Profile 所需的 `id``displayName` 和目标 URL不返回邮箱、角色、权限、Referral、编辑审计、审核原因、token/hash、内部字段或调试信息。
- 用户界面只展示业务数据和设计内的文案,不展示提示词、计划、调试信息、字段内部名或修改说明。 - 用户界面只展示业务数据和设计内的文案,不展示提示词、计划、调试信息、字段内部名或修改说明。
- 可编辑 Wiki 内容必须记录创建者、最后编辑者、创建时间、最后编辑时间和编辑历史。 - 可编辑 Wiki 内容必须记录创建者、最后编辑者、创建时间、最后编辑时间和编辑历史。
- 列表顺序由 `sort_order` 控制,默认按创建时间旧到新初始化,排序值按 10 递增以便后续插入和拖拽排序。 - 列表顺序由 `sort_order` 控制,默认按创建时间旧到新初始化,排序值按 10 递增以便后续插入和拖拽排序。

View File

@@ -43,7 +43,8 @@ type GlobalSearchGroupType =
| 'ancient-artifacts' | 'ancient-artifacts'
| 'recipes' | 'recipes'
| 'daily-checklist' | 'daily-checklist'
| 'life'; | 'life'
| 'users';
type GlobalSearchItem = { type GlobalSearchItem = {
id: number; id: number;
type: GlobalSearchGroupType; type: GlobalSearchGroupType;
@@ -2466,7 +2467,7 @@ export async function globalSearch(paramsQuery: QueryParams = {}, locale = defau
const checklistTitle = localizedField('daily-checklist-items', 'c.id', 'c.title', 'title', locale); const checklistTitle = localizedField('daily-checklist-items', 'c.id', 'c.title', 'title', locale);
const lifeCategoryName = localizedName('life-tags', 'lc', locale); const lifeCategoryName = localizedName('life-tags', 'lc', locale);
const [pokemon, habitats, items, artifacts, recipes, checklist, life] = await Promise.all([ const [pokemon, habitats, items, artifacts, recipes, checklist, life, users] = await Promise.all([
query<GlobalSearchItem>( query<GlobalSearchItem>(
` `
SELECT SELECT
@@ -2604,6 +2605,23 @@ export async function globalSearch(paramsQuery: QueryParams = {}, locale = defau
LIMIT $2 LIMIT $2
`, `,
[pattern, limit] [pattern, limit]
),
query<GlobalSearchItem>(
`
SELECT
u.id,
'users' AS type,
u.display_name AS title,
'/profile/' || u.id AS url,
NULL AS summary,
NULL AS meta,
NULL AS image
FROM users u
WHERE u.display_name ILIKE $1
ORDER BY lower(u.display_name), u.id
LIMIT $2
`,
[pattern, limit]
) )
]); ]);
@@ -2614,7 +2632,8 @@ export async function globalSearch(paramsQuery: QueryParams = {}, locale = defau
{ type: 'ancient-artifacts', items: artifacts }, { type: 'ancient-artifacts', items: artifacts },
{ type: 'recipes', items: recipes }, { type: 'recipes', items: recipes },
{ type: 'daily-checklist', items: checklist }, { type: 'daily-checklist', items: checklist },
{ type: 'life', items: life } { type: 'life', items: life },
{ type: 'users', items: users }
]; ];
return { query: search, groups: groups.filter((group) => group.items.length > 0) }; return { query: search, groups: groups.filter((group) => group.items.length > 0) };

View File

@@ -36,7 +36,8 @@ const groupLabels: Record<GlobalSearchGroupType, string> = {
'ancient-artifacts': 'search.groups.ancientArtifacts', 'ancient-artifacts': 'search.groups.ancientArtifacts',
recipes: 'search.groups.recipes', recipes: 'search.groups.recipes',
'daily-checklist': 'search.groups.dailyChecklist', 'daily-checklist': 'search.groups.dailyChecklist',
life: 'search.groups.life' life: 'search.groups.life',
users: 'search.groups.users'
}; };
function clearSearchTimeout() { function clearSearchTimeout() {

View File

@@ -325,7 +325,8 @@ export type GlobalSearchGroupType =
| 'ancient-artifacts' | 'ancient-artifacts'
| 'recipes' | 'recipes'
| 'daily-checklist' | 'daily-checklist'
| 'life'; | 'life'
| 'users';
export interface GlobalSearchItem { export interface GlobalSearchItem {
id: number; id: number;

View File

@@ -90,7 +90,8 @@ export const systemWordingMessages = {
ancientArtifacts: 'Ancient Artifacts', ancientArtifacts: 'Ancient Artifacts',
recipes: 'Recipes', recipes: 'Recipes',
dailyChecklist: 'Daily CheckList', dailyChecklist: 'Daily CheckList',
life: 'Life' life: 'Life',
users: 'Users'
} }
}, },
notifications: { notifications: {
@@ -1409,7 +1410,8 @@ export const systemWordingMessages = {
ancientArtifacts: 'Ancient Artifacts', ancientArtifacts: 'Ancient Artifacts',
recipes: '材料单', recipes: '材料单',
dailyChecklist: '每日 CheckList', dailyChecklist: '每日 CheckList',
life: 'Life' life: 'Life',
users: '用户'
} }
}, },
notifications: { notifications: {