mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-31 16:49:07 +08:00
feat(FilesPage): 添加批量删除功能
This commit is contained in:
parent
7d70425c75
commit
91892ea619
@ -470,7 +470,7 @@
|
||||
"count": "files",
|
||||
"created_at": "Created At",
|
||||
"delete": "Delete",
|
||||
"delete.content": "Deleting a file will delete its reference from all messages. Are you sure you want to delete this file?",
|
||||
"delete.content": "Deleting a file will delete its reference from all messages. Are you sure you want to delete {{count}} files?",
|
||||
"delete.paintings.warning": "Image contains this file, deletion is not possible",
|
||||
"delete.title": "Delete File",
|
||||
"document": "Document",
|
||||
@ -482,7 +482,9 @@
|
||||
"size": "Size",
|
||||
"text": "Text",
|
||||
"title": "Files",
|
||||
"type": "Type"
|
||||
"type": "Type",
|
||||
"batch_operation": "Batch Operation",
|
||||
"batch_delete": "Batch Delete"
|
||||
},
|
||||
"gpustack": {
|
||||
"keep_alive_time.description": "The time in minutes to keep the connection alive, default is 5 minutes.",
|
||||
@ -1730,6 +1732,7 @@
|
||||
"theme.light": "Light",
|
||||
"theme.title": "Theme",
|
||||
"theme.color_primary": "Primary Color",
|
||||
"theme.window.style.opaque": "Opaque Window",
|
||||
"theme.window.style.title": "Window Style",
|
||||
"theme.window.style.transparent": "Transparent Window",
|
||||
"title": "Settings",
|
||||
|
||||
@ -470,7 +470,7 @@
|
||||
"count": "ファイル",
|
||||
"created_at": "作成日",
|
||||
"delete": "削除",
|
||||
"delete.content": "ファイルを削除すると、ファイルがすべてのメッセージで参照されることを削除します。このファイルを削除してもよろしいですか?",
|
||||
"delete.content": "ファイルを削除すると、ファイルがすべてのメッセージで参照されることを削除します。この{{count}}ファイルを削除してもよろしいですか?",
|
||||
"delete.paintings.warning": "画像に含まれているため、削除できません",
|
||||
"delete.title": "ファイルを削除",
|
||||
"document": "ドキュメント",
|
||||
@ -482,7 +482,9 @@
|
||||
"size": "サイズ",
|
||||
"text": "テキスト",
|
||||
"title": "ファイル",
|
||||
"type": "タイプ"
|
||||
"type": "タイプ",
|
||||
"batch_operation": "一括操作",
|
||||
"batch_delete": "一括削除"
|
||||
},
|
||||
"gpustack": {
|
||||
"keep_alive_time.description": "モデルがメモリに保持される時間(デフォルト:5分)",
|
||||
@ -1718,6 +1720,7 @@
|
||||
"theme.light": "ライト",
|
||||
"theme.title": "テーマ",
|
||||
"theme.color_primary": "テーマ色",
|
||||
"theme.window.style.opaque": "不透明ウィンドウ",
|
||||
"theme.window.style.title": "ウィンドウスタイル",
|
||||
"theme.window.style.transparent": "透明ウィンドウ",
|
||||
"title": "設定",
|
||||
|
||||
@ -470,7 +470,7 @@
|
||||
"count": "файлов",
|
||||
"created_at": "Дата создания",
|
||||
"delete": "Удалить",
|
||||
"delete.content": "Удаление файла удалит его из всех сообщений, вы уверены, что хотите удалить этот файл?",
|
||||
"delete.content": "Удаление файла удалит его из всех сообщений, вы уверены, что хотите удалить этот {{count}} файл",
|
||||
"delete.paintings.warning": "В изображениях содержится этот файл, удаление невозможно",
|
||||
"delete.title": "Удалить файл",
|
||||
"document": "Документ",
|
||||
@ -482,7 +482,9 @@
|
||||
"size": "Размер",
|
||||
"text": "Текст",
|
||||
"title": "Файлы",
|
||||
"type": "Тип"
|
||||
"type": "Тип",
|
||||
"batch_operation": "Пакетная операция",
|
||||
"batch_delete": "Пакетное удаление"
|
||||
},
|
||||
"gpustack": {
|
||||
"keep_alive_time.description": "Время в минутах, в течение которого модель остается активной, по умолчанию 5 минут.",
|
||||
@ -1718,6 +1720,7 @@
|
||||
"theme.light": "Светлая",
|
||||
"theme.title": "Тема",
|
||||
"theme.color_primary": "Цвет темы",
|
||||
"theme.window.style.opaque": "Непрозрачное окно",
|
||||
"theme.window.style.title": "Стиль окна",
|
||||
"theme.window.style.transparent": "Прозрачное окно",
|
||||
"title": "Настройки",
|
||||
|
||||
@ -470,7 +470,7 @@
|
||||
"count": "个文件",
|
||||
"created_at": "创建时间",
|
||||
"delete": "删除",
|
||||
"delete.content": "删除文件会删除文件在所有消息中的引用,确定要删除此文件吗?",
|
||||
"delete.content": "删除文件会删除文件在所有消息中的引用,确定要删除这{{count}}个文件吗?",
|
||||
"delete.paintings.warning": "绘图中包含该图片,暂时无法删除",
|
||||
"delete.title": "删除文件",
|
||||
"document": "文档",
|
||||
@ -482,7 +482,9 @@
|
||||
"size": "大小",
|
||||
"text": "文本",
|
||||
"title": "文件",
|
||||
"type": "类型"
|
||||
"type": "类型",
|
||||
"batch_operation": "批量操作",
|
||||
"batch_delete": "批量删除"
|
||||
},
|
||||
"gpustack": {
|
||||
"keep_alive_time.description": "模型在内存中保持的时间(默认:5分钟)",
|
||||
@ -1730,6 +1732,7 @@
|
||||
"theme.light": "浅色",
|
||||
"theme.title": "主题",
|
||||
"theme.color_primary": "主题颜色",
|
||||
"theme.window.style.opaque": "不透明窗口",
|
||||
"theme.window.style.title": "窗口样式",
|
||||
"theme.window.style.transparent": "透明窗口",
|
||||
"title": "设置",
|
||||
|
||||
@ -470,7 +470,7 @@
|
||||
"count": "個檔案",
|
||||
"created_at": "建立時間",
|
||||
"delete": "刪除",
|
||||
"delete.content": "刪除檔案會刪除檔案在所有訊息中的引用,確定要刪除此檔案嗎?",
|
||||
"delete.content": "刪除檔案會刪除檔案在所有訊息中的引用,確定要刪除這{{count}}個檔案嗎?",
|
||||
"delete.paintings.warning": "繪圖中包含該圖片,暫時無法刪除",
|
||||
"delete.title": "刪除檔案",
|
||||
"document": "文件",
|
||||
@ -482,7 +482,9 @@
|
||||
"size": "大小",
|
||||
"text": "文字",
|
||||
"title": "檔案",
|
||||
"type": "類型"
|
||||
"type": "類型",
|
||||
"batch_operation": "批量操作",
|
||||
"batch_delete": "批量刪除"
|
||||
},
|
||||
"gpustack": {
|
||||
"keep_alive_time.description": "模型在記憶體中保持的時間(預設為 5 分鐘)",
|
||||
|
||||
@ -15,11 +15,11 @@ import store from '@renderer/store'
|
||||
import { FileType, FileTypes } from '@renderer/types'
|
||||
import { Message } from '@renderer/types/newMessage'
|
||||
import { formatFileSize } from '@renderer/utils'
|
||||
import { Button, Empty, Flex, Popconfirm } from 'antd'
|
||||
import { Button, Checkbox, Dropdown, Empty, Flex, Popconfirm } from 'antd'
|
||||
import dayjs from 'dayjs'
|
||||
import { useLiveQuery } from 'dexie-react-hooks'
|
||||
import { File as FileIcon, FileImage, FileText, FileType as FileTypeIcon } from 'lucide-react'
|
||||
import { FC, useState } from 'react'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
@ -33,6 +33,11 @@ const FilesPage: FC = () => {
|
||||
const [fileType, setFileType] = useState<string>('document')
|
||||
const [sortField, setSortField] = useState<SortField>('created_at')
|
||||
const [sortOrder, setSortOrder] = useState<SortOrder>('desc')
|
||||
const [selectedFileIds, setSelectedFileIds] = useState<string[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedFileIds([])
|
||||
}, [fileType])
|
||||
|
||||
const tempFilesSort = (files: FileType[]) => {
|
||||
return files.sort((a, b) => {
|
||||
@ -153,6 +158,8 @@ const FilesPage: FC = () => {
|
||||
})
|
||||
|
||||
Logger.log(`Deleted ${blockIdsToDelete.length} blocks and updated relevant topic messages for file ${fileId}.`)
|
||||
|
||||
setSelectedFileIds((prev) => prev.filter((id) => id !== fileId))
|
||||
} catch (error) {
|
||||
Logger.error(`Error updating topics or deleting blocks for file ${fileId}:`, error)
|
||||
window.modal.error({ content: t('files.delete.db_error'), centered: true }) // 提示数据库操作失败
|
||||
@ -170,6 +177,46 @@ const FilesPage: FC = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleBatchDelete = async () => {
|
||||
const selectedFiles = await Promise.all(selectedFileIds.map((id) => FileManager.getFile(id)))
|
||||
const validFiles = selectedFiles.filter((file): file is FileType => file !== null && file !== undefined)
|
||||
|
||||
const paintings = await store.getState().paintings.paintings
|
||||
const paintingsFiles = paintings.flatMap((p) => p.files)
|
||||
|
||||
const filesInPaintings = validFiles.filter((file) => paintingsFiles.some((p) => p.id === file.id))
|
||||
|
||||
if (filesInPaintings.length > 0) {
|
||||
window.modal.warning({
|
||||
content: t('files.delete.paintings.warning', { count: filesInPaintings.length }),
|
||||
centered: true
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
for (const fileId of selectedFileIds) {
|
||||
await handleDelete(fileId)
|
||||
}
|
||||
|
||||
setSelectedFileIds([])
|
||||
}
|
||||
|
||||
const handleSelectFile = (fileId: string, checked: boolean) => {
|
||||
if (checked) {
|
||||
setSelectedFileIds((prev) => [...prev, fileId])
|
||||
} else {
|
||||
setSelectedFileIds((prev) => prev.filter((id) => id !== fileId))
|
||||
}
|
||||
}
|
||||
|
||||
const handleSelectAll = (checked: boolean) => {
|
||||
if (checked) {
|
||||
setSelectedFileIds(sortedFiles.map((file) => file.id))
|
||||
} else {
|
||||
setSelectedFileIds([])
|
||||
}
|
||||
}
|
||||
|
||||
const dataSource = sortedFiles?.map((file) => {
|
||||
return {
|
||||
key: file.id,
|
||||
@ -186,13 +233,20 @@ const FilesPage: FC = () => {
|
||||
<Button type="text" icon={<EditOutlined />} onClick={() => handleRename(file.id)} />
|
||||
<Popconfirm
|
||||
title={t('files.delete.title')}
|
||||
description={t('files.delete.content')}
|
||||
description={t('files.delete.content', { count: 1 })}
|
||||
okText={t('common.confirm')}
|
||||
cancelText={t('common.cancel')}
|
||||
onConfirm={() => handleDelete(file.id)}
|
||||
icon={<ExclamationCircleOutlined style={{ color: 'red' }} />}>
|
||||
<Button type="text" danger icon={<DeleteOutlined />} />
|
||||
</Popconfirm>
|
||||
{fileType !== 'image' && (
|
||||
<Checkbox
|
||||
checked={selectedFileIds.includes(file.id)}
|
||||
onChange={(e) => handleSelectFile(file.id, e.target.checked)}
|
||||
style={{ margin: '0 8px' }}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
@ -224,22 +278,59 @@ const FilesPage: FC = () => {
|
||||
</SideNav>
|
||||
<MainContent>
|
||||
<SortContainer>
|
||||
{['created_at', 'size', 'name'].map((field) => (
|
||||
<SortButton
|
||||
key={field}
|
||||
active={sortField === field}
|
||||
onClick={() => {
|
||||
if (sortField === field) {
|
||||
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')
|
||||
} else {
|
||||
setSortField(field as 'created_at' | 'size' | 'name')
|
||||
setSortOrder('desc')
|
||||
}
|
||||
}}>
|
||||
{t(`files.${field}`)}
|
||||
{sortField === field && (sortOrder === 'desc' ? <SortDescendingOutlined /> : <SortAscendingOutlined />)}
|
||||
</SortButton>
|
||||
))}
|
||||
<Flex gap={8} align="center">
|
||||
{['created_at', 'size', 'name'].map((field) => (
|
||||
<Button
|
||||
color="default"
|
||||
key={field}
|
||||
variant={sortField === field ? 'filled' : 'text'}
|
||||
onClick={() => {
|
||||
if (sortField === field) {
|
||||
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')
|
||||
} else {
|
||||
setSortField(field as 'created_at' | 'size' | 'name')
|
||||
setSortOrder('desc')
|
||||
}
|
||||
}}>
|
||||
{t(`files.${field}`)}
|
||||
{sortField === field &&
|
||||
(sortOrder === 'desc' ? <SortDescendingOutlined /> : <SortAscendingOutlined />)}
|
||||
</Button>
|
||||
))}
|
||||
</Flex>
|
||||
{fileType !== 'image' && (
|
||||
<Dropdown.Button
|
||||
style={{ width: 'auto' }}
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
key: 'delete',
|
||||
disabled: selectedFileIds.length === 0,
|
||||
danger: true,
|
||||
label: (
|
||||
<Popconfirm
|
||||
disabled={selectedFileIds.length === 0}
|
||||
title={t('files.delete.title')}
|
||||
description={t('files.delete.content', { count: selectedFileIds.length })}
|
||||
okText={t('common.confirm')}
|
||||
cancelText={t('common.cancel')}
|
||||
onConfirm={handleBatchDelete}
|
||||
icon={<ExclamationCircleOutlined />}>
|
||||
{t('files.batch_delete')} ({selectedFileIds.length})
|
||||
</Popconfirm>
|
||||
)
|
||||
}
|
||||
]
|
||||
}}
|
||||
trigger={['click']}>
|
||||
<Checkbox
|
||||
indeterminate={selectedFileIds.length > 0 && selectedFileIds.length < sortedFiles.length}
|
||||
checked={selectedFileIds.length === sortedFiles.length}
|
||||
onChange={(e) => handleSelectAll(e.target.checked)}>
|
||||
{t('files.batch_operation')}
|
||||
</Checkbox>
|
||||
</Dropdown.Button>
|
||||
)}
|
||||
</SortContainer>
|
||||
{dataSource && dataSource?.length > 0 ? (
|
||||
<FileList id={fileType} list={dataSource} files={sortedFiles} />
|
||||
@ -268,6 +359,7 @@ const MainContent = styled.div`
|
||||
const SortContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
border-bottom: 0.5px solid var(--color-border);
|
||||
@ -315,25 +407,4 @@ const SideNav = styled.div`
|
||||
}
|
||||
`
|
||||
|
||||
const SortButton = styled(Button)<{ active?: boolean }>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px 12px;
|
||||
height: 30px;
|
||||
border-radius: var(--list-item-border-radius);
|
||||
border: 0.5px solid ${(props) => (props.active ? 'var(--color-border)' : 'transparent')};
|
||||
background-color: ${(props) => (props.active ? 'var(--color-background-soft)' : 'transparent')};
|
||||
color: ${(props) => (props.active ? 'var(--color-text)' : 'var(--color-text-secondary)')};
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-background-soft);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.anticon {
|
||||
font-size: 12px;
|
||||
}
|
||||
`
|
||||
|
||||
export default FilesPage
|
||||
|
||||
Loading…
Reference in New Issue
Block a user