diff --git a/package.json b/package.json index 56384907..aa74c267 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "apache-arrow": "^17.0.0", "electron-updater": "^6.3.9", "mammoth": "^1.11.0", + "mark.js": "^8.11.1", "pdf-parse": "^2.4.5", "react-doc-viewer": "^0.1.15", "react-pdf": "^10.3.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a564bf69..a85c1a62 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -50,6 +50,9 @@ importers: mammoth: specifier: ^1.11.0 version: 1.11.0 + mark.js: + specifier: ^8.11.1 + version: 8.11.1 pdf-parse: specifier: ^2.4.5 version: 2.4.5 @@ -3633,6 +3636,9 @@ packages: engines: {node: '>=12.0.0'} hasBin: true + mark.js@8.11.1: + resolution: {integrity: sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==} + matcher@3.0.0: resolution: {integrity: sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==} engines: {node: '>=10'} @@ -8590,6 +8596,8 @@ snapshots: underscore: 1.13.7 xmlbuilder: 10.1.1 + mark.js@8.11.1: {} + matcher@3.0.0: dependencies: escape-string-regexp: 4.0.0 diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 77d00a42..83204f23 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -1,15 +1,13 @@ import React from 'react' import { HashRouter, Routes, Route, useLocation, useNavigate } from 'react-router-dom' -import { Layout, ConfigProvider, theme, Button, Segmented, Avatar } from 'antd' +import { Layout, ConfigProvider, theme, Button } from 'antd' import { ArrowLeftOutlined, - SettingOutlined, - TranslationOutlined, - SearchOutlined, - MessageOutlined + SettingOutlined } from '@ant-design/icons' import SearchPage from './pages/SearchPage' import ImportPage from './pages/ImportPage' +import brandIcon from '../../../resources/icon.png' const { Header, Content } = Layout @@ -28,11 +26,7 @@ function App(): React.JSX.Element { Button: { primaryShadow: 'none' }, - Segmented: { - itemSelectedBg: '#404040', - itemSelectedColor: '#ffffff', - trackBg: '#f5f5f5' - } + } }} > @@ -49,12 +43,12 @@ const MainLayout: React.FC = () => { const isSearchPage = location.pathname === '/' return ( - -
+ +
navigate('/')}>
- N {e.currentTarget.style.display='none'; e.currentTarget.parentElement!.innerText='N'}} /> + N
NCurator
@@ -69,38 +63,15 @@ const MainLayout: React.FC = () => { )}
- {isSearchPage && ( -
- }, - { label: '聊天', value: 'chat', icon: } - ]} - defaultValue="search" - className="bg-gray-100" - /> -
- )} - +
-
- zh-CN - -
- + } /> } /> diff --git a/src/renderer/src/assets/icon.png b/src/renderer/src/assets/icon.png new file mode 100644 index 00000000..09bda82a Binary files /dev/null and b/src/renderer/src/assets/icon.png differ diff --git a/src/renderer/src/components/TextHighlighter.tsx b/src/renderer/src/components/TextHighlighter.tsx new file mode 100644 index 00000000..39f0f264 --- /dev/null +++ b/src/renderer/src/components/TextHighlighter.tsx @@ -0,0 +1,28 @@ +import { useEffect, useRef } from 'react' +import Mark from 'mark.js' + +const TextHighlighter = ({ text, keywords, className }: { + text: string; + keywords: string[]; + className?: string; +}) => { + const contentRef = useRef(null) + + useEffect(() => { + if (!contentRef.current || keywords.length === 0) return + const mark = new Mark(contentRef.current) + mark.mark(keywords, { + separateWordSearch: false, + }) + + return () => { + mark.unmark() + } + }, [keywords, text]) + + return ( +

{text}

+ ) +} + +export default TextHighlighter diff --git a/src/renderer/src/pages/ImportPage.tsx b/src/renderer/src/pages/ImportPage.tsx index 03fc2edb..f3b5f89b 100644 --- a/src/renderer/src/pages/ImportPage.tsx +++ b/src/renderer/src/pages/ImportPage.tsx @@ -1,14 +1,12 @@ import React, { useState, useEffect } from 'react' -import { Upload, Button, message, Card, Typography, List, Collapse, Input, Table, Modal, Tag, Space } from 'antd' +import { Upload, Button, message, Typography, Collapse, Input, Table, Modal } from 'antd' import { InboxOutlined, - CheckCircleOutlined, PlusOutlined, DeleteOutlined, SettingOutlined, ReloadOutlined, SearchOutlined, - ReadOutlined, FileTextOutlined, FilePdfOutlined, BookOutlined diff --git a/src/renderer/src/pages/SearchPage.tsx b/src/renderer/src/pages/SearchPage.tsx index 5e28e54b..778095c3 100644 --- a/src/renderer/src/pages/SearchPage.tsx +++ b/src/renderer/src/pages/SearchPage.tsx @@ -1,11 +1,13 @@ import React, { useState } from 'react' -import { Input, List, Card, Modal, Empty, Typography, Tag, Button, Switch, Dropdown, Space } from 'antd' -import { FileTextOutlined, FilePdfOutlined, SendOutlined, DownOutlined } from '@ant-design/icons' +import { Input, List, Card, Modal, Empty, Typography, Tag, Button, Switch } from 'antd' +import { SendOutlined } from '@ant-design/icons' import { Document, Page, pdfjs } from 'react-pdf' import DocViewer, { DocViewerRenderers } from 'react-doc-viewer' import 'react-pdf/dist/Page/AnnotationLayer.css' import 'react-pdf/dist/Page/TextLayer.css' import type { SearchResult } from '../../../shared/types' +import TextHighlighter from '../components/TextHighlighter' +import brandIcon from '../../../../resources/icon.png' // Configure PDF worker pdfjs.GlobalWorkerOptions.workerSrc = new URL( @@ -14,7 +16,7 @@ pdfjs.GlobalWorkerOptions.workerSrc = new URL( ).toString() const { TextArea } = Input -const { Title, Paragraph } = Typography +const { Title } = Typography const SearchPage: React.FC = () => { const [loading, setLoading] = useState(false) @@ -39,31 +41,7 @@ const SearchPage: React.FC = () => { } } - const highlightText = (text: string, tokens: string[]) => { - if (!tokens.length) return text - - // Create a regex from tokens, sorting by length descending to match longest first - const sortedTokens = [...tokens].sort((a, b) => b.length - a.length) - // Escape special regex chars in tokens - const escapedTokens = sortedTokens.map(t => t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')) - const regex = new RegExp(`(${escapedTokens.join('|')})`, 'gi') - - const parts = text.split(regex) - - return ( - - {parts.map((part, i) => - regex.test(part) ? ( - - {part} - - ) : ( - part - ) - )} - - ) - } + const openPreview = (item: SearchResult) => { setCurrentDoc(item) @@ -110,15 +88,15 @@ const SearchPage: React.FC = () => { } return ( -
+
{/* Search Box */} -
+