From edeb9f84f917d4613b714184c102123451142ce1 Mon Sep 17 00:00:00 2001 From: suyao Date: Fri, 13 Jun 2025 19:11:10 +0800 Subject: [PATCH] refactor(FileItem, FileList, FilesPage, i18n): enhance file management UI and localization - Updated FileItem component to display file count and additional metadata. - Refactored FileList to support checkbox selection and improved layout. - Enhanced FilesPage with batch delete functionality and improved file selection handling. - Updated localization strings across multiple languages for consistency in delete confirmation messages. --- src/renderer/src/i18n/locales/en-us.json | 2 +- src/renderer/src/i18n/locales/ja-jp.json | 2 +- src/renderer/src/i18n/locales/ru-ru.json | 2 +- src/renderer/src/i18n/locales/zh-cn.json | 2 +- src/renderer/src/i18n/locales/zh-tw.json | 2 +- src/renderer/src/pages/files/FileItem.tsx | 70 ++++-- src/renderer/src/pages/files/FileList.tsx | 200 +++++++++-------- src/renderer/src/pages/files/FilesPage.tsx | 209 +++++++++++++++--- .../src/pages/knowledge/KnowledgeContent.tsx | 14 +- .../components/KnowledgeFileItem.tsx | 124 +++++++++++ 10 files changed, 458 insertions(+), 169 deletions(-) create mode 100644 src/renderer/src/pages/knowledge/components/KnowledgeFileItem.tsx diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 16b4ec5694..3bb2a78449 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -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", diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index b3e61ad0c2..542fa9bdf6 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -470,7 +470,7 @@ "count": "ファイル", "created_at": "作成日", "delete": "削除", - "delete.content": "ファイルを削除すると、ファイルがすべてのメッセージで参照されることを削除します。このファイルを削除してもよろしいですか?", + "delete.content": "ファイルを削除すると、ファイルがすべてのメッセージで参照されることを削除します。この{{count}}ファイルを削除してもよろしいですか?", "delete.paintings.warning": "画像に含まれているため、削除できません", "delete.title": "ファイルを削除", "document": "ドキュメント", diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index 615df64789..6b0fab3cdb 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -470,7 +470,7 @@ "count": "файлов", "created_at": "Дата создания", "delete": "Удалить", - "delete.content": "Удаление файла удалит его из всех сообщений, вы уверены, что хотите удалить этот файл?", + "delete.content": "Удаление файла удалит его из всех сообщений, вы уверены, что хотите удалить этот {{count}} файл", "delete.paintings.warning": "В изображениях содержится этот файл, удаление невозможно", "delete.title": "Удалить файл", "document": "Документ", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index f7e0248dd4..0494e16097 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -470,7 +470,7 @@ "count": "个文件", "created_at": "创建时间", "delete": "删除", - "delete.content": "删除文件会删除文件在所有消息中的引用,确定要删除此文件吗?", + "delete.content": "删除文件会删除文件在所有消息中的引用,确定要删除这{{count}}个文件吗?", "delete.paintings.warning": "绘图中包含该图片,暂时无法删除", "delete.title": "删除文件", "document": "文档", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 4b8b027677..e7d943eae3 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -470,7 +470,7 @@ "count": "個檔案", "created_at": "建立時間", "delete": "刪除", - "delete.content": "刪除檔案會刪除檔案在所有訊息中的引用,確定要刪除此檔案嗎?", + "delete.content": "刪除檔案會刪除檔案在所有訊息中的引用,確定要刪除這{{count}}個檔案嗎?", "delete.paintings.warning": "繪圖中包含該圖片,暫時無法刪除", "delete.title": "刪除檔案", "document": "文件", diff --git a/src/renderer/src/pages/files/FileItem.tsx b/src/renderer/src/pages/files/FileItem.tsx index ce611ce983..08724033e8 100644 --- a/src/renderer/src/pages/files/FileItem.tsx +++ b/src/renderer/src/pages/files/FileItem.tsx @@ -12,7 +12,7 @@ import { GlobalOutlined, LinkOutlined } from '@ant-design/icons' -import { Flex } from 'antd' +import { t } from 'i18next' import React, { memo } from 'react' import styled from 'styled-components' @@ -21,10 +21,14 @@ interface FileItemProps { icon?: React.ReactNode name: React.ReactNode | string ext: string - extra?: React.ReactNode | string + size: string + created_at: string + count?: number + checkbox?: React.ReactNode actions: React.ReactNode } style?: React.CSSProperties + gridTemplate?: string } const getFileIcon = (type?: string) => { @@ -75,19 +79,30 @@ const getFileIcon = (type?: string) => { return } -const FileItem: React.FC = ({ fileInfo, style }) => { - const { name, ext, extra, actions, icon } = fileInfo +const FileItem: React.FC = ({ fileInfo, style, gridTemplate = '' }) => { + const { name, ext, size, created_at, count, actions, icon, checkbox } = fileInfo return ( - - {icon || getFileIcon(ext)} - - {name} - {extra && {extra}} - - {actions} - + + {checkbox && {checkbox}} + + {icon || getFileIcon(ext)} + + + + {name} + {count && ( + + {count} {t('files.count')} + + )} + + + {size} + {created_at} + {actions} + ) } @@ -111,20 +126,36 @@ const FileItemCard = styled.div` } ` -const CardContent = styled.div` +const FileGrid = styled.div` + display: grid; + gap: 8px; padding: 8px 8px 8px 16px; + align-items: center; +` + +const FileCell = styled.div` display: flex; - align-items: stretch; - gap: 16px; + align-items: center; + min-width: 0; ` const FileIcon = styled.div` max-height: 44px; + width: 100%; color: var(--color-text-3); font-size: 32px; display: flex; align-items: center; justify-content: center; + flex-shrink: 0; +` + +const FileNameColumn = styled.div` + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; + width: 100%; ` const FileName = styled.div` @@ -142,16 +173,9 @@ const FileName = styled.div` } ` -const FileInfo = styled.div` +const FileCount = styled.div` font-size: 13px; color: var(--color-text-2); ` -const FileActions = styled.div` - max-height: 44px; - display: flex; - align-items: center; - justify-content: center; -` - export default memo(FileItem) diff --git a/src/renderer/src/pages/files/FileList.tsx b/src/renderer/src/pages/files/FileList.tsx index cdb0421439..39fb61dfa9 100644 --- a/src/renderer/src/pages/files/FileList.tsx +++ b/src/renderer/src/pages/files/FileList.tsx @@ -1,11 +1,6 @@ -import FileManager from '@renderer/services/FileManager' import { FileType, FileTypes } from '@renderer/types' -import { formatFileSize } from '@renderer/utils' -import { Col, Image, Row, Spin } from 'antd' -import { t } from 'i18next' import VirtualList from 'rc-virtual-list' import React, { memo } from 'react' -import styled from 'styled-components' import FileItem from './FileItem' @@ -19,43 +14,47 @@ interface FileItemProps { size: string ext: string created_at: string + checkbox?: React.ReactNode actions: React.ReactNode }[] files?: FileType[] + selectedFileIds?: string[] + onFileSelect?: (fileId: string, checked: boolean) => void + columnWidths?: string } -const FileList: React.FC = ({ id, list, files }) => { - if (id === FileTypes.IMAGE && files?.length && files?.length > 0) { - return ( -
- - - {files?.map((file) => ( - - - - - - { - const img = e.target as HTMLImageElement - img.parentElement?.classList.add('loaded') - }} - /> - -
{formatFileSize(file.size)}
-
-
- - ))} -
-
-
- ) - } +const FileList: React.FC = ({ list, columnWidths }) => { + // if (id === FileTypes.IMAGE && files?.length && files?.length > 0) { + // return ( + //
+ // + // + // {files?.map((file) => ( + // + // + // + // + // + // { + // const img = e.target as HTMLImageElement + // img.parentElement?.classList.add('loaded') + // }} + // /> + // + //
{formatFileSize(file.size)}
+ //
+ //
+ // + // ))} + //
+ //
+ //
+ // ) + // } return ( = ({ id, list, files }) => { height={window.innerHeight - 100} itemHeight={75} itemKey="key" - style={{ padding: '0 16px 16px 16px' }} + style={{ padding: '0 0 16px 0' }} styles={{ verticalScrollBar: { width: 6 @@ -76,16 +75,21 @@ const FileList: React.FC = ({ id, list, files }) => {
)} @@ -93,70 +97,70 @@ const FileList: React.FC = ({ id, list, files }) => { ) } -const ImageWrapper = styled.div` - position: relative; - aspect-ratio: 1; - overflow: hidden; - border-radius: 8px; - background-color: var(--color-background-soft); - display: flex; - align-items: center; - justify-content: center; - border: 0.5px solid var(--color-border); +// const ImageWrapper = styled.div` +// position: relative; +// aspect-ratio: 1; +// overflow: hidden; +// border-radius: 8px; +// background-color: var(--color-background-soft); +// display: flex; +// align-items: center; +// justify-content: center; +// border: 0.5px solid var(--color-border); - .ant-image { - height: 100%; - width: 100%; - opacity: 0; - transition: - opacity 0.3s ease, - transform 0.3s ease; +// .ant-image { +// height: 100%; +// width: 100%; +// opacity: 0; +// transition: +// opacity 0.3s ease, +// transform 0.3s ease; - &.loaded { - opacity: 1; - } - } +// &.loaded { +// opacity: 1; +// } +// } - &:hover { - .ant-image.loaded { - transform: scale(1.05); - } +// &:hover { +// .ant-image.loaded { +// transform: scale(1.05); +// } - div:last-child { - opacity: 1; - } - } -` +// div:last-child { +// opacity: 1; +// } +// } +// ` -const LoadingWrapper = styled.div` - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - display: flex; - align-items: center; - justify-content: center; - background-color: var(--color-background-soft); -` +// const LoadingWrapper = styled.div` +// position: absolute; +// top: 0; +// left: 0; +// right: 0; +// bottom: 0; +// display: flex; +// align-items: center; +// justify-content: center; +// background-color: var(--color-background-soft); +// ` -const ImageInfo = styled.div` - position: absolute; - bottom: 0; - left: 0; - right: 0; - background: rgba(0, 0, 0, 0.6); - color: white; - padding: 5px 8px; - opacity: 0; - transition: opacity 0.3s ease; - font-size: 12px; +// const ImageInfo = styled.div` +// position: absolute; +// bottom: 0; +// left: 0; +// right: 0; +// background: rgba(0, 0, 0, 0.6); +// color: white; +// padding: 5px 8px; +// opacity: 0; +// transition: opacity 0.3s ease; +// font-size: 12px; - > div:first-child { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } -` +// > div:first-child { +// white-space: nowrap; +// overflow: hidden; +// text-overflow: ellipsis; +// } +// ` export default memo(FileList) diff --git a/src/renderer/src/pages/files/FilesPage.tsx b/src/renderer/src/pages/files/FilesPage.tsx index fa4b220e42..64b21839cf 100644 --- a/src/renderer/src/pages/files/FilesPage.tsx +++ b/src/renderer/src/pages/files/FilesPage.tsx @@ -15,7 +15,7 @@ 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, 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' @@ -25,6 +25,8 @@ import styled from 'styled-components' import FileList from './FileList' +const GRID_TEMPLATE = 'auto 60px 1fr 120px 140px 100px' + type SortField = 'created_at' | 'size' | 'name' type SortOrder = 'asc' | 'desc' @@ -33,6 +35,7 @@ const FilesPage: FC = () => { const [fileType, setFileType] = useState('document') const [sortField, setSortField] = useState('created_at') const [sortOrder, setSortOrder] = useState('desc') + const [selectedFileIds, setSelectedFileIds] = useState([]) const tempFilesSort = (files: FileType[]) => { return files.sort((a, b) => { @@ -170,6 +173,38 @@ const FilesPage: FC = () => { } } + // 全选/取消全选 + const handleSelectAll = (checked: boolean) => { + if (checked) { + setSelectedFileIds(sortedFiles.map((file) => file.id)) + } else { + setSelectedFileIds([]) + } + } + + // 单个文件选择 + const handleFileSelect = (fileId: string, checked: boolean) => { + if (checked) { + setSelectedFileIds((prev) => [...prev, fileId]) + } else { + setSelectedFileIds((prev) => prev.filter((id) => id !== fileId)) + } + } + + // 批量删除 + const handleBatchDelete = async () => { + if (selectedFileIds.length === 0) return + + try { + for (const fileId of selectedFileIds) { + await handleDelete(fileId) + } + setSelectedFileIds([]) + } catch (error) { + Logger.error('Batch delete error:', error) + } + } + const dataSource = sortedFiles?.map((file) => { return { key: file.id, @@ -181,12 +216,18 @@ const FilesPage: FC = () => { ext: file.ext, created_at: dayjs(file.created_at).format('MM-DD HH:mm'), created_at_unix: dayjs(file.created_at).unix(), + checkbox: ( + handleFileSelect(file.id, e.target.checked)} + /> + ), actions: (