Skip to content

Commit 93860b5

Browse files
committed
refactor(web): code review DRY/YAGNI 收敛
- 抽 components/ui/ActivateCover,InspectorPane 与 ChatComposer 共用 会话未激活覆盖层(替代 InspectorPane.ResumeCover 与 ChatComposer 内联块, 保留 sender-overlay 磨砂遮罩) - 抽 components/session/inspectorActions 导出 INSPECTOR_ACTIONS, 空态卡片与「+」下拉菜单共用,消除能力漂移风险 - 删除 InspectorTabKind 死类型与 InspectorTabEntry.kind 单值字段(零消费方)
1 parent 237a04b commit 93860b5

6 files changed

Lines changed: 146 additions & 84 deletions

File tree

packages/web/src/components/composer/ChatComposer.tsx

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
import { useState, useCallback, useMemo, useRef, useEffect } from 'react'
1818
import { Button, Tooltip, Select, theme, Typography, Popover, message } from 'antd'
19-
import { PlusOutlined, PlayCircleOutlined, SwapOutlined, SafetyOutlined, RightOutlined, InboxOutlined } from '@ant-design/icons'
19+
import { PlusOutlined, SwapOutlined, SafetyOutlined, RightOutlined, InboxOutlined } from '@ant-design/icons'
2020
import { Sender } from '@ant-design/x'
2121
import { useTranslation } from 'react-i18next'
2222
import styled from '@emotion/styled'
@@ -41,6 +41,7 @@ import { MentionDropdown } from './MentionDropdown'
4141
import { SlashCommandDropdown } from './SlashCommandDropdown'
4242
import { CommandHintBar } from './CommandHintBar'
4343
import { ResponsiveActionBar, type ActionItem } from './ResponsiveActionBar'
44+
import { ActivateCover } from '@/components/ui/ActivateCover'
4445
import { getPermissionModeColor } from './permissionModeColors'
4546
import { useHasFinePointer } from '@/core/data/hooks/useMediaQuery'
4647

@@ -799,27 +800,12 @@ export function ChatComposer(props: ChatComposerProps) {
799800

800801
{/* 未激活覆盖层 */}
801802
{showInactiveCover && (
802-
<div
803+
<ActivateCover
803804
className="sender-overlay"
804-
style={{
805-
position: 'absolute',
806-
inset: 0,
807-
display: 'flex',
808-
alignItems: 'center',
809-
justifyContent: 'center',
810-
borderRadius: 'var(--ant-border-radius)',
811-
zIndex: 10,
812-
}}
813-
>
814-
<Button
815-
type="primary"
816-
icon={<PlayCircleOutlined />}
817-
loading={activatePending}
818-
onClick={onActivate}
819-
>
820-
{t('composer.activate')}
821-
</Button>
822-
</div>
805+
loading={activatePending}
806+
onActivate={onActivate!}
807+
borderRadius="var(--ant-border-radius)"
808+
/>
823809
)}
824810

825811
{/* 本地模式覆盖层 */}

packages/web/src/components/session/InspectorEmptyState.tsx

Lines changed: 17 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -15,52 +15,41 @@
1515
*/
1616

1717
import { useTranslation } from 'react-i18next'
18-
import { Folder, Terminal, FileSearch } from 'lucide-react'
1918
import styled from '@emotion/styled'
2019
import { theme as antTheme } from 'antd'
20+
import { INSPECTOR_ACTIONS } from './inspectorActions'
2121

2222
interface InspectorEmptyStateProps {
2323
/** 点「文件」 */
2424
onOpenFile: () => void
2525
}
2626

27-
interface ActionItem {
28-
key: string
29-
icon: React.ReactNode
30-
labelKey: string
31-
disabled?: boolean
32-
onClick?: () => void
33-
}
34-
3527
/**
3628
* 空态:居中的卡片行列表(参考 macOS 菜单风格——图标 + 标签,浅灰圆角卡)。
37-
* 终端/审查 disabled(未支持)。仅「文件」可点
29+
* 动作清单与「+」下拉菜单共用 INSPECTOR_ACTIONS,避免两处能力漂移
3830
*/
3931
export function InspectorEmptyState({ onOpenFile }: InspectorEmptyStateProps) {
4032
const { t } = useTranslation()
4133
const { token } = antTheme.useToken()
4234

43-
const items: ActionItem[] = [
44-
{ key: 'file', icon: <Folder size={18} />, labelKey: 'session.inspector.openFile', onClick: onOpenFile },
45-
{ key: 'terminal', icon: <Terminal size={18} />, labelKey: 'session.inspector.terminal', disabled: true },
46-
{ key: 'review', icon: <FileSearch size={18} />, labelKey: 'session.inspector.review', disabled: true },
47-
]
48-
4935
return (
5036
<Wrap>
5137
<List role="list">
52-
{items.map((item) => (
53-
<Row
54-
key={item.key}
55-
type="button"
56-
disabled={item.disabled}
57-
onClick={item.disabled ? undefined : item.onClick}
58-
$token={token}
59-
>
60-
<span className="icon">{item.icon}</span>
61-
<span className="label">{t(item.labelKey)}</span>
62-
</Row>
63-
))}
38+
{INSPECTOR_ACTIONS.map((item) => {
39+
const { Icon } = item
40+
return (
41+
<Row
42+
key={item.key}
43+
type="button"
44+
disabled={item.disabled}
45+
onClick={item.disabled ? undefined : onOpenFile}
46+
$token={token}
47+
>
48+
<span className="icon"><Icon size={18} /></span>
49+
<span className="label">{t(item.labelKey)}</span>
50+
</Row>
51+
)
52+
})}
6453
</List>
6554
</Wrap>
6655
)

packages/web/src/components/session/InspectorPane.tsx

Lines changed: 15 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@ import { Layout, Tabs, Button, Tooltip, Dropdown } from 'antd'
1919
import type { MenuProps } from 'antd'
2020
import { useTranslation } from 'react-i18next'
2121
import styled from '@emotion/styled'
22-
import { PanelRightClose, Folder, Terminal, FileSearch, Maximize, Minimize, Plus, PlayCircle } from 'lucide-react'
22+
import { PanelRightClose, Folder, FileSearch, Maximize, Minimize, Plus } from 'lucide-react'
2323
import FileTreeView from '@/components/files/FileTreeView'
2424
import FileContentView from '@/components/files/FileContentView'
25+
import { ActivateCover } from '@/components/ui/ActivateCover'
2526
import { InspectorEmptyState } from './InspectorEmptyState'
27+
import { INSPECTOR_ACTIONS } from './inspectorActions'
2628
import { useIsMobile } from '@/core/data/hooks/useMediaQuery'
2729
import { useSessionActions } from '@/core/data/hooks/mutations/useSessionActions'
2830
import { useWorkspaceStore, type InspectorTabEntry } from '@/core/data/stores/workspaceStore'
@@ -131,7 +133,7 @@ export function InspectorPane({ sessionId, active = true }: InspectorPaneProps)
131133
if (!active) {
132134
return (
133135
<Layout style={{ height: '100%', position: 'relative' }}>
134-
<ResumeCover loading={isResumePending} onResume={() => resumeSession()} label={t('composer.activate')} />
136+
<ActivateCover loading={isResumePending} onActivate={() => resumeSession()} />
135137
<div style={{ position: 'absolute', top: 4, right: 8, zIndex: 2 }}>{rightChrome}</div>
136138
</Layout>
137139
)
@@ -149,16 +151,17 @@ export function InspectorPane({ sessionId, active = true }: InspectorPaneProps)
149151

150152
if (!everExpanded) return <Layout style={{ height: '100%' }} />
151153

152-
const addMenuItems: MenuProps['items'] = [
153-
{
154-
key: 'file',
155-
icon: <Folder size={14} />,
156-
label: t('session.inspector.openFile'),
157-
onClick: () => openFileTreeTab(sessionId),
158-
},
159-
{ key: 'terminal', icon: <Terminal size={14} />, label: t('session.inspector.terminal'), disabled: true },
160-
{ key: 'review', icon: <FileSearch size={14} />, label: t('session.inspector.review'), disabled: true },
161-
]
154+
// 「+」下拉菜单:与空态卡片共用 INSPECTOR_ACTIONS,仅「文件」可点
155+
const addMenuItems: MenuProps['items'] = INSPECTOR_ACTIONS.map((action) => {
156+
const { Icon } = action
157+
return {
158+
key: action.key,
159+
icon: <Icon size={14} />,
160+
label: t(action.labelKey),
161+
disabled: action.disabled,
162+
onClick: action.disabled ? undefined : () => openFileTreeTab(sessionId),
163+
}
164+
})
162165

163166
const renderTabContent = (tab: InspectorTabEntry): ReactNode => {
164167
if (tab.mode === 'file' && tab.filePath) {
@@ -232,24 +235,6 @@ export function InspectorPane({ sessionId, active = true }: InspectorPaneProps)
232235
)
233236
}
234237

235-
/** session 离线覆盖层:居中「恢复会话」按钮,覆盖整个检视面板内容区。 */
236-
function ResumeCover({ loading, onResume, label }: {
237-
loading: boolean
238-
onResume: () => void
239-
label: string
240-
}) {
241-
return (
242-
<div style={{
243-
position: 'absolute', inset: 0, zIndex: 1,
244-
display: 'flex', alignItems: 'center', justifyContent: 'center',
245-
}}>
246-
<Button type="primary" icon={<PlayCircle size={18} />} loading={loading} onClick={onResume}>
247-
{label}
248-
</Button>
249-
</div>
250-
)
251-
}
252-
253238
/** 右上角 chrome:最大化/恢复 + 收起。空态与 Tab 态共用。 */
254239
function RightChrome({
255240
isMobile,
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright Maner·Fan
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { Folder, Terminal, FileSearch, type LucideIcon } from 'lucide-react'
18+
19+
/** 检视面板可用动作的唯一真相源:空态卡片列表与「+」下拉菜单共同消费。 */
20+
export interface InspectorActionDescriptor {
21+
key: 'file' | 'terminal' | 'review'
22+
/** lucide 图标组件(调用方按需传 size) */
23+
Icon: LucideIcon
24+
/** 文案 i18n key */
25+
labelKey: string
26+
/** 暂未支持的动作置灰(终端/审查) */
27+
disabled: boolean
28+
}
29+
30+
/**
31+
* 检视面板动作清单。新增/启用某项能力时只改这里,
32+
* 空态卡片与「+」下拉菜单自动一致,避免两处能力漂移。
33+
*/
34+
export const INSPECTOR_ACTIONS: readonly InspectorActionDescriptor[] = [
35+
{ key: 'file', Icon: Folder, labelKey: 'session.inspector.openFile', disabled: false },
36+
{ key: 'terminal', Icon: Terminal, labelKey: 'session.inspector.terminal', disabled: true },
37+
{ key: 'review', Icon: FileSearch, labelKey: 'session.inspector.review', disabled: true },
38+
]
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Copyright Maner·Fan
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { Button } from 'antd'
18+
import { PlayCircle } from 'lucide-react'
19+
import { useTranslation } from 'react-i18next'
20+
21+
interface ActivateCoverProps {
22+
/** 点击「恢复/激活」回调 */
23+
onActivate: () => void
24+
/** 恢复请求进行中 */
25+
loading?: boolean
26+
/** 按钮文案的 i18n key,默认 composer.activate */
27+
labelKey?: string
28+
/** 覆盖层 zIndex,默认 10 */
29+
zIndex?: number
30+
/** 覆盖层圆角,默认 0 */
31+
borderRadius?: string | number
32+
/** 透传 className(如磨砂遮罩 sender-overlay) */
33+
className?: string
34+
}
35+
36+
/**
37+
* 会话未激活(CLI runner 未连接)时的覆盖层:铺满父容器、居中「恢复会话」按钮。
38+
* 聊天输入框与检视面板共用,避免两处各自维护样式/loading/文案。
39+
* 父容器需 position: relative。
40+
*/
41+
export function ActivateCover({
42+
onActivate,
43+
loading = false,
44+
labelKey = 'composer.activate',
45+
zIndex = 10,
46+
borderRadius = 0,
47+
className,
48+
}: ActivateCoverProps) {
49+
const { t } = useTranslation()
50+
return (
51+
<div
52+
className={className}
53+
style={{
54+
position: 'absolute',
55+
inset: 0,
56+
display: 'flex',
57+
alignItems: 'center',
58+
justifyContent: 'center',
59+
borderRadius,
60+
zIndex,
61+
}}
62+
>
63+
<Button type="primary" icon={<PlayCircle size={18} />} loading={loading} onClick={onActivate}>
64+
{t(labelKey)}
65+
</Button>
66+
</div>
67+
)
68+
}

packages/web/src/core/data/stores/workspaceStore.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,9 @@
1717
import { create } from 'zustand'
1818
import { uuid } from '@/core/lib/uuid'
1919

20-
/** tab 类型;'terminal' | 'review' 暂不接入(占位) */
21-
export type InspectorTabKind = 'files'
22-
2320
/** 单个 tab:文件树视图或已打开的文件 */
2421
export interface InspectorTabEntry {
2522
id: string
26-
kind: 'files'
2723
mode: 'tree' | 'file'
2824
/** mode='file':相对路径(去重 key + tooltip) */
2925
filePath?: string
@@ -123,7 +119,7 @@ export const useWorkspaceStore = create<WorkspaceState>((set, get) => ({
123119
next.set(sessionId, { ...cur, activeTabId: existedTree.id })
124120
return { sessions: next }
125121
}
126-
const entry: InspectorTabEntry = { id: uuid(), kind: 'files', mode: 'tree' }
122+
const entry: InspectorTabEntry = { id: uuid(), mode: 'tree' }
127123
const tabs = [...cur.tabs, entry]
128124
const next = new Map(state.sessions)
129125
next.set(sessionId, { ...cur, tabs, activeTabId: entry.id })

0 commit comments

Comments
 (0)