feat: connect search and project settings
This commit is contained in:
@@ -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 }))} />
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user