Skip to content

Commit d21ca60

Browse files
committed
feat: support multi-select mode
1 parent b9356cf commit d21ca60

5 files changed

Lines changed: 148 additions & 10 deletions

File tree

src/renderer/src/components/layout/sidebar/Sidebar.tsx

Lines changed: 79 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,16 @@ export default function Sidebar({
3838
deleteMultiplePages
3939
} = usePagesStore()
4040
const { openTab } = useTabsStore()
41-
const { selectedNodeType, selectedNodeId, checkedNodeIds, setSelectedNode, clearCheckedNodes } =
42-
useUIStore()
41+
const {
42+
selectedNodeType,
43+
selectedNodeId,
44+
checkedNodeIds,
45+
isMultiSelectMode,
46+
setSelectedNode,
47+
clearCheckedNodes,
48+
enterMultiSelectMode,
49+
exitMultiSelectMode
50+
} = useUIStore()
4351
const { filterFolderId } = useSearchStore()
4452
const { modal } = App.useApp()
4553

@@ -106,8 +114,68 @@ export default function Sidebar({
106114

107115
// 批量删除选中的聊天
108116
const handleBatchDelete = useCallback(() => {
109-
// TODO
110-
}, [checkedNodeIds, clearCheckedNodes, deleteMultiplePages])
117+
if (checkedNodeIds.length === 0) return
118+
119+
// 解析选中的节点,分离聊天和文件夹
120+
const chatIds: string[] = []
121+
const folderIds: string[] = []
122+
123+
checkedNodeIds.forEach((nodeKey) => {
124+
if (nodeKey.startsWith('chat-')) {
125+
chatIds.push(nodeKey.replace('chat-', ''))
126+
} else if (nodeKey.startsWith('folder-')) {
127+
folderIds.push(nodeKey.replace('folder-', ''))
128+
}
129+
})
130+
131+
const totalCount = chatIds.length + folderIds.length
132+
const description =
133+
folderIds.length > 0
134+
? `确定要删除选中的 ${totalCount} 项吗?包含 ${chatIds.length} 个聊天和 ${folderIds.length} 个文件夹。文件夹中的所有内容也将被删除。此操作无法撤销。`
135+
: `确定要删除选中的 ${chatIds.length} 个聊天吗?此操作无法撤销。`
136+
137+
modal.confirm({
138+
title: '批量删除',
139+
content: description,
140+
okText: '确定删除',
141+
cancelText: '取消',
142+
okType: 'danger',
143+
onOk() {
144+
const { deleteFolder, deletePage } = usePagesStore.getState()
145+
146+
// 递归删除文件夹及其内容
147+
const deleteRecursive = (folderId: string) => {
148+
const currentState = usePagesStore.getState()
149+
// 删除该文件夹下的所有聊天
150+
const chatsToDelete = currentState.pages.filter(
151+
(p) => p.folderId === folderId && p.type !== 'settings'
152+
)
153+
chatsToDelete.forEach((chat) => deletePage(chat.id))
154+
155+
// 递归删除子文件夹
156+
const subFolders = currentState.folders.filter((f) => f.parentId === folderId)
157+
subFolders.forEach((subFolder) => {
158+
deleteRecursive(subFolder.id)
159+
deleteFolder(subFolder.id)
160+
})
161+
}
162+
163+
// 先删除文件夹(包含递归删除内容)
164+
folderIds.forEach((folderId) => {
165+
deleteRecursive(folderId)
166+
deleteFolder(folderId)
167+
})
168+
169+
// 再删除独立的聊天
170+
if (chatIds.length > 0) {
171+
deleteMultiplePages(chatIds)
172+
}
173+
174+
// 清空选中状态并退出多选模式
175+
exitMultiSelectMode()
176+
}
177+
})
178+
}, [checkedNodeIds, deleteMultiplePages, exitMultiSelectMode, modal])
111179

112180
// 检查是否有选中的项目
113181
const hasCheckedItems = checkedNodeIds.length > 0
@@ -127,16 +195,21 @@ export default function Sidebar({
127195
<SidebarActions
128196
collapsed={false}
129197
hasCheckedItems={hasCheckedItems}
198+
isMultiSelectMode={isMultiSelectMode}
130199
onCreateChat={handleCreateChat}
131200
onCreateCrosstabChat={handleCreateCrosstabChat}
132201
onCreateObjectChat={handleCreateObjectChat}
133202
onCreateFolder={handleCreateFolder}
134203
onBatchDelete={handleBatchDelete}
204+
onEnterMultiSelectMode={enterMultiSelectMode}
205+
onExitMultiSelectMode={exitMultiSelectMode}
135206
/>
136207
</div>
137208
<div className="sidebar-content">
138-
{hasCheckedItems && (
139-
<div className="multi-select-indicator">已选中 {checkedNodeIds.length}</div>
209+
{isMultiSelectMode && (
210+
<div className="multi-select-indicator">
211+
{hasCheckedItems ? `已选中 ${checkedNodeIds.length} 项` : '请勾选要操作的项目'}
212+
</div>
140213
)}
141214
<ChatHistoryTree onChatClick={handleChatClick} onFindInFolder={onFindInFolder} />
142215
</div>

src/renderer/src/components/layout/sidebar/sidebar.css

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,16 @@
6262
flex-direction: column;
6363
height: 100%;
6464
}
65+
66+
.multi-select-indicator {
67+
display: flex;
68+
align-items: center;
69+
justify-content: center;
70+
padding: 6px 12px;
71+
margin-bottom: 8px;
72+
background: #e6f4ff;
73+
border: 1px solid #91caff;
74+
border-radius: 4px;
75+
color: #1677ff;
76+
font-size: 12px;
77+
}

src/renderer/src/components/layout/sidebar_items/SidebarActions.tsx

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,27 +7,35 @@ import {
77
DeleteOutlined,
88
TableOutlined,
99
BlockOutlined,
10-
DownOutlined
10+
DownOutlined,
11+
CheckSquareOutlined,
12+
CloseOutlined
1113
} from '@ant-design/icons'
1214

1315
interface SidebarActionsProps {
1416
collapsed?: boolean
1517
hasCheckedItems?: boolean
18+
isMultiSelectMode?: boolean
1619
onCreateChat: () => void
1720
onCreateCrosstabChat: () => void
1821
onCreateObjectChat: () => void
1922
onCreateFolder: () => void
2023
onBatchDelete?: () => void
24+
onEnterMultiSelectMode?: () => void
25+
onExitMultiSelectMode?: () => void
2126
}
2227

2328
export default function SidebarActions({
2429
collapsed = false,
2530
hasCheckedItems = false,
31+
isMultiSelectMode = false,
2632
onCreateChat,
2733
onCreateCrosstabChat,
2834
onCreateObjectChat,
2935
onCreateFolder,
30-
onBatchDelete
36+
onBatchDelete,
37+
onEnterMultiSelectMode,
38+
onExitMultiSelectMode
3139
}: SidebarActionsProps) {
3240
const createOptions: MenuProps['items'] = [
3341
{
@@ -47,6 +55,15 @@ export default function SidebarActions({
4755
label: '新建文件夹',
4856
icon: <FolderAddOutlined />,
4957
onClick: onCreateFolder
58+
},
59+
{
60+
type: 'divider'
61+
},
62+
{
63+
key: 'multiselect',
64+
label: isMultiSelectMode ? '退出多选模式' : '进入多选模式',
65+
icon: isMultiSelectMode ? <CloseOutlined /> : <CheckSquareOutlined />,
66+
onClick: isMultiSelectMode ? onExitMultiSelectMode : onEnterMultiSelectMode
5067
}
5168
]
5269
if (collapsed) {

src/renderer/src/components/layout/sidebar_items/chat/ChatHistoryTree.tsx

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export default function ChatHistoryTree({ onChatClick, onFindInFolder }: ChatHis
4040
selectedNodeId,
4141
selectedNodeType,
4242
checkedNodeIds,
43+
isMultiSelectMode,
4344
setSelectedNode,
4445
setCheckedNodes,
4546
clearCheckedNodes
@@ -736,6 +737,19 @@ export default function ChatHistoryTree({ onChatClick, onFindInFolder }: ChatHis
736737
]
737738
)
738739

740+
// 处理多选模式下的勾选事件
741+
const handleCheck = useCallback(
742+
(
743+
checked: React.Key[] | { checked: React.Key[]; halfChecked: React.Key[] },
744+
info: { checked: boolean; checkedNodes: any[]; node: any; event: 'check'; halfCheckedKeys?: React.Key[] }
745+
) => {
746+
// 处理 checked 可能是数组或对象的情况
747+
const checkedKeys = Array.isArray(checked) ? checked : checked.checked
748+
setCheckedNodes(checkedKeys.map(String))
749+
},
750+
[setCheckedNodes]
751+
)
752+
739753
const expandedKeys = useMemo(
740754
() => folders.filter((f) => f.expanded).map((f) => `folder-${f.id}`),
741755
[folders]
@@ -748,15 +762,17 @@ export default function ChatHistoryTree({ onChatClick, onFindInFolder }: ChatHis
748762
className="chat-history-tree"
749763
showIcon
750764
blockNode
751-
draggable={!editingNodeKey}
752-
checkable={false}
765+
draggable={!editingNodeKey && !isMultiSelectMode}
766+
checkable={isMultiSelectMode}
753767
multiple
754768
virtual
755769
height={virtualHeight}
756770
expandedKeys={expandedKeys}
757771
selectedKeys={checkedNodeIds.length > 0 ? checkedKeys : selectedKeys}
772+
checkedKeys={isMultiSelectMode ? checkedNodeIds : undefined}
758773
treeData={treeData}
759774
onSelect={handleTreeSelect}
775+
onCheck={handleCheck}
760776
onDrop={handleDrop}
761777
allowDrop={({ dropNode, dragNode, dropPosition }) => {
762778
const dragNodeInfo = parseNodeKey(dragNode.key)

src/renderer/src/stores/uiStore.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export interface UIState {
1010
selectedNodeId: string | null
1111
selectedNodeType: 'folder' | 'chat' | null
1212
checkedNodeIds: string[]
13+
isMultiSelectMode: boolean
1314

1415
// 收藏面板选中状态
1516
selectedFavoriteId: string | null
@@ -34,6 +35,8 @@ export interface UIActions {
3435
setCheckedNodes: (nodeIds: string[]) => void
3536
clearCheckedNodes: () => void
3637
toggleNodeCheck: (nodeId: string) => void
38+
enterMultiSelectMode: () => void
39+
exitMultiSelectMode: () => void
3740

3841
// 收藏面板选择
3942
setSelectedFavorite: (favoriteId: string | null, favoriteType: 'folder' | 'item' | null) => void
@@ -65,6 +68,7 @@ const initialState: UIState = {
6568
selectedNodeId: null,
6669
selectedNodeType: null,
6770
checkedNodeIds: [],
71+
isMultiSelectMode: false,
6872
selectedFavoriteId: null,
6973
selectedFavoriteType: null,
7074
sidebarCollapsed: false,
@@ -116,6 +120,20 @@ export const useUIStore = create<UIState & UIActions>()(
116120
})
117121
},
118122

123+
enterMultiSelectMode: () => {
124+
set((state) => {
125+
state.isMultiSelectMode = true
126+
state.checkedNodeIds = []
127+
})
128+
},
129+
130+
exitMultiSelectMode: () => {
131+
set((state) => {
132+
state.isMultiSelectMode = false
133+
state.checkedNodeIds = []
134+
})
135+
},
136+
119137
// 收藏面板选择
120138
setSelectedFavorite: (favoriteId, favoriteType) => {
121139
set((state) => {
@@ -239,6 +257,7 @@ export const useUIStore = create<UIState & UIActions>()(
239257
state.selectedNodeId = null
240258
state.selectedNodeType = null
241259
state.checkedNodeIds = []
260+
state.isMultiSelectMode = false
242261
state.selectedFavoriteId = null
243262
state.selectedFavoriteType = null
244263
state.collapsedMessages = {}

0 commit comments

Comments
 (0)