feat: connect search and project settings

This commit is contained in:
2026-07-21 17:16:45 +08:00
parent 2275d388a7
commit 000de4bcdb
14 changed files with 698 additions and 141 deletions

View File

@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react'
import { Badge, Button, Divider, Input, Layout, Modal, Space, Typography } from '@arco-design/web-react'
import { Alert, Badge, Button, Divider, Input, Layout, Modal, Space, Typography } from '@arco-design/web-react'
import { IconApps, IconDashboard, IconMore, IconPlus, IconSettings } from '@arco-design/web-react/icon'
import type { ChannelKey, ProjectChannel, ProjectWorkspace, WorkbenchView } from './project-types'
@@ -29,11 +29,13 @@ export function ProjectSidebar({
activeChannel: ChannelKey
onSelectChannel: (key: ChannelKey) => void
onSelectItem: (title: string) => void
onUpdateProject: (update: ProjectSettingsUpdate) => void
onUpdateProject: (update: ProjectSettingsUpdate) => Promise<void>
}) {
const workspaceMode = activeView === 'workspace'
const channels = activeWorkspace.channels
const [settingsOpen, setSettingsOpen] = useState(false)
const [settingsSaving, setSettingsSaving] = useState(false)
const [settingsError, setSettingsError] = useState('')
const [draft, setDraft] = useState({
name: activeWorkspace.project.name,
identifier: activeWorkspace.project.identifier,
@@ -44,6 +46,7 @@ export function ProjectSidebar({
useEffect(() => {
if (!settingsOpen) return
setSettingsError('')
setDraft({
name: activeWorkspace.project.name,
identifier: activeWorkspace.project.identifier,
@@ -61,7 +64,7 @@ export function ProjectSidebar({
{workspaceMode ? <IconApps /> : <span className="project-title-icon">{projectIcon}</span>}
<Title heading={5}>{workspaceMode ? '工作台' : activeWorkspace.project.name}</Title>
</Space>
<Button icon={<IconSettings />} size="mini" onClick={() => setSettingsOpen(true)} />
<Button aria-label="编辑项目设置" icon={<IconSettings />} size="mini" onClick={() => setSettingsOpen(true)} />
</div>
<div className="channel-list">
@@ -113,20 +116,34 @@ export function ProjectSidebar({
className="action-modal"
title="编辑项目"
visible={settingsOpen}
onCancel={() => setSettingsOpen(false)}
onOk={() => {
onUpdateProject({
projectId: activeWorkspace.project.id,
name: draft.name.trim() || activeWorkspace.project.name,
identifier: draft.identifier.trim(),
icon: draft.icon.trim(),
background: draft.background.trim(),
description: draft.description.trim(),
})
setSettingsOpen(false)
confirmLoading={settingsSaving}
maskClosable={!settingsSaving}
cancelButtonProps={{ disabled: settingsSaving }}
onCancel={() => {
if (!settingsSaving) setSettingsOpen(false)
}}
onOk={async () => {
setSettingsSaving(true)
setSettingsError('')
try {
await onUpdateProject({
projectId: activeWorkspace.project.id,
name: draft.name.trim() || activeWorkspace.project.name,
identifier: draft.identifier.trim(),
icon: draft.icon.trim(),
background: draft.background.trim(),
description: draft.description.trim(),
})
setSettingsOpen(false)
} catch (error) {
setSettingsError(error instanceof Error ? error.message : '项目设置保存失败,请稍后重试')
} finally {
setSettingsSaving(false)
}
}}
>
<Space direction="vertical" size={12} className="action-form">
{settingsError && <Alert className="project-settings-error" type="error" content={settingsError} />}
<label>
<Text></Text>
<Input value={draft.name} onChange={(name) => setDraft((value) => ({ ...value, name }))} />

View File

@@ -1,23 +1,102 @@
import { Button, Input, Layout, Space, Typography } from '@arco-design/web-react'
import { useEffect, useState } from 'react'
import { Button, Dropdown, Input, Layout, List, Space, Typography } from '@arco-design/web-react'
import { IconApps, IconMoon, IconSearch, IconSun } from '@arco-design/web-react/icon'
import type { SearchResultDTO } from '../../api/search'
import type { Theme } from './project-types'
const { Header } = Layout
const { Text } = Typography
export function ProjectTopbar({ theme, onToggleTheme }: { theme: Theme; onToggleTheme: () => void }) {
export function ProjectTopbar({
theme,
onToggleTheme,
query,
loading,
searched,
results,
onQueryChange,
onSearch,
onSelectSearchResult,
}: {
theme: Theme
onToggleTheme: () => void
query: string
loading: boolean
searched: boolean
results: SearchResultDTO[]
onQueryChange: (value: string) => void
onSearch: () => void
onSelectSearchResult: (result: SearchResultDTO) => void
}) {
const desktop = isDesktopRuntime()
const [popupVisible, setPopupVisible] = useState(false)
useEffect(() => {
if (!loading && searched) setPopupVisible(true)
}, [loading, results, searched])
const submitSearch = () => {
setPopupVisible(false)
onSearch()
}
const searchResults = (
<div className="search-results-dropdown" aria-label="搜索结果">
<List<SearchResultDTO>
size="small"
bordered={false}
dataSource={results}
noDataElement="未找到匹配结果"
render={(result) => (
<List.Item key={`${result.type}:${result.id}`}>
<button
type="button"
className="search-result-item"
onClick={() => {
setPopupVisible(false)
onSelectSearchResult(result)
}}
>
<span className="search-result-heading">
<Text>{result.title}</Text>
<Text className="search-result-type" type="secondary">{searchTypeLabel(result.type)}</Text>
</span>
{result.snippet && <Text className="search-result-snippet" type="secondary">{result.snippet}</Text>}
</button>
</List.Item>
)}
/>
</div>
)
return (
<Header className="topbar">
<div className="brand-lockup">
<img className="brand-icon" src="/senlinai-icon.svg" alt="" aria-hidden="true" />
<img className="brand-icon" src="/senlinai-icon.svg" alt="" />
<Text className="brand-name">AI</Text>
</div>
<div className="global-search" role="search">
<Input size="large" prefix={<IconSearch />} placeholder="搜索项目、频道、文档、任务、联系人..." />
<Button size="large" type="primary"></Button>
</div>
<Dropdown
droplist={searchResults}
popupVisible={popupVisible && searched}
trigger="click"
position="bl"
onVisibleChange={(visible) => setPopupVisible(visible && searched)}
>
<div className="global-search" role="search">
<Input
size="large"
prefix={<IconSearch />}
placeholder="搜索项目、任务和笔记"
value={query}
onChange={(value) => {
setPopupVisible(false)
onQueryChange(value)
}}
onPressEnter={submitSearch}
/>
<Button size="large" type="primary" loading={loading} onClick={submitSearch}></Button>
</div>
</Dropdown>
<Space className="topbar-actions">
<Button aria-label={theme === 'dark' ? '切换浅色模式' : '切换深色模式'} icon={theme === 'dark' ? <IconSun /> : <IconMoon />} onClick={onToggleTheme} />
{desktop && (
@@ -32,6 +111,12 @@ export function ProjectTopbar({ theme, onToggleTheme }: { theme: Theme; onToggle
)
}
function searchTypeLabel(type: SearchResultDTO['type']) {
if (type === 'project') return '项目'
if (type === 'task') return '任务'
return '笔记'
}
function isDesktopRuntime() {
return typeof window !== 'undefined' && '__TAURI__' in window
}

View File

@@ -7,6 +7,7 @@ import { ProjectRail } from './projects/project-rail'
import { ProjectSidebar, type ProjectSettingsUpdate } from './projects/project-sidebar'
import { ProjectStatusbar } from './projects/project-statusbar'
import { ProjectTopbar } from './projects/project-topbar'
import type { SearchResultDTO } from '../api/search'
import type { ChannelKey, Project, ProjectWorkspace, Theme, WorkbenchView } from './projects/project-types'
const { Content } = Layout
@@ -33,6 +34,13 @@ export function ProjectPage({
onUpdateWorkspaceTask,
onUpdateProject,
onCreateProjectTag,
searchQuery,
searchLoading,
searchSearched,
searchResults,
onSearchQueryChange,
onSearch,
onSelectSearchResult,
}: {
activeView: WorkbenchView
activeWorkspace: ProjectWorkspace
@@ -53,8 +61,15 @@ export function ProjectPage({
onUploadSource: () => void
onCreateCronPlan: () => void
onUpdateWorkspaceTask: (update: WorkspaceTaskUpdate) => void
onUpdateProject: (update: ProjectSettingsUpdate) => void
onUpdateProject: (update: ProjectSettingsUpdate) => Promise<void>
onCreateProjectTag: (name: string) => void
searchQuery: string
searchLoading: boolean
searchSearched: boolean
searchResults: SearchResultDTO[]
onSearchQueryChange: (value: string) => void
onSearch: () => void
onSelectSearchResult: (result: SearchResultDTO) => void
}) {
const isProject = activeView === 'project'
const projects = workspaces.map((workspace) => workspace.project)
@@ -72,7 +87,17 @@ export function ProjectPage({
return (
<Layout className="workbench-shell">
<ProjectTopbar theme={theme} onToggleTheme={onToggleTheme} />
<ProjectTopbar
theme={theme}
onToggleTheme={onToggleTheme}
query={searchQuery}
loading={searchLoading}
searched={searchSearched}
results={searchResults}
onQueryChange={onSearchQueryChange}
onSearch={onSearch}
onSelectSearchResult={onSelectSearchResult}
/>
<Layout className="workbench-main" hasSider>
<ProjectRail