diff --git a/backend/app/search_client.py b/backend/app/search_client.py index f9f0bd1..0a2aa7d 100644 --- a/backend/app/search_client.py +++ b/backend/app/search_client.py @@ -36,14 +36,21 @@ class SearchClient: "highlightPostTag": "", "attributesToCrop": ["content"], "cropLength": 30, + "showMatchesPosition": True, }) results = [] for hit in response.get("hits", []): formatted = hit.get("_formatted", {}) + line_number = None + matches = hit.get("_matchesPosition", {}).get("content", []) + if matches and hit.get("content"): + start = matches[0]["start"] + line_number = hit["content"][:start].count("\n") + 1 results.append({ "key": hit["key"], "title": hit["title"], "path": hit["path"], "snippet": formatted.get("content", hit.get("content", "")), + "lineNumber": line_number, }) return results diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index db08fe7..bdf19d8 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -104,6 +104,7 @@ export interface SearchResult { title: string; path: string; snippet: string; + lineNumber: number | null; } export interface SearchResponse { diff --git a/frontend/src/components/SearchResults.tsx b/frontend/src/components/SearchResults.tsx index ec58ef3..c87a653 100644 --- a/frontend/src/components/SearchResults.tsx +++ b/frontend/src/components/SearchResults.tsx @@ -32,6 +32,9 @@ export default function SearchResults({ results, query, onJumpToResult }: Search
{result.title} + {result.lineNumber != null && ( + 第 {result.lineNumber} 行 + )}
diff --git a/frontend/src/styles.css b/frontend/src/styles.css index c7a8abc..8ba3ad8 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -664,6 +664,17 @@ body { color: var(--ink); } +.result-line { + font-family: 'IBM Plex Mono', monospace; + font-size: 11px; + color: var(--ash); + background: var(--paper-alt); + border: 1px solid var(--rule); + border-radius: 4px; + padding: 1px 6px; + white-space: nowrap; +} + .result-snippet { font-size: 12.5px; color: var(--ash); diff --git a/frontend/tests/App.test.tsx b/frontend/tests/App.test.tsx index fe8ec53..b9cf783 100644 --- a/frontend/tests/App.test.tsx +++ b/frontend/tests/App.test.tsx @@ -85,7 +85,7 @@ test('selecting a file from the tree loads it into the editor', async () => { test('searching shows the results overlay, and clicking a result opens the document', async () => { vi.mocked(listDocuments).mockResolvedValue({ folders: [], files: [] }); vi.mocked(search).mockResolvedValue({ - results: [{ key: '产品文档/架构设计.md', title: '架构设计.md', path: '产品文档', snippet: '...检索...' }], + results: [{ key: '产品文档/架构设计.md', title: '架构设计.md', path: '产品文档', snippet: '...检索...', lineNumber: 5 }], }); vi.mocked(getDocument).mockResolvedValue({ key: '产品文档/架构设计.md', @@ -121,7 +121,7 @@ test('searching shows the results overlay, and clicking a result opens the docum test('jumping to a search result clears the search input so it does not show stale text', async () => { vi.mocked(listDocuments).mockResolvedValue({ folders: [], files: [] }); vi.mocked(search).mockResolvedValue({ - results: [{ key: '产品文档/架构设计.md', title: '架构设计.md', path: '产品文档', snippet: '...检索...' }], + results: [{ key: '产品文档/架构设计.md', title: '架构设计.md', path: '产品文档', snippet: '...检索...', lineNumber: 5 }], }); vi.mocked(getDocument).mockResolvedValue({ key: '产品文档/架构设计.md', diff --git a/frontend/tests/components/SearchResults.test.tsx b/frontend/tests/components/SearchResults.test.tsx index b440e56..0662853 100644 --- a/frontend/tests/components/SearchResults.test.tsx +++ b/frontend/tests/components/SearchResults.test.tsx @@ -12,6 +12,7 @@ test('renders result count, titles, and highlighted snippets', () => { title: '架构设计.md', path: '产品文档', snippet: '检索:Meilisearch,支持 中文全文检索。', + lineNumber: null, }, ]} onJumpToResult={vi.fn()} @@ -28,7 +29,7 @@ test('clicking a result calls onJumpToResult with its key', () => { render( );