mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-06 13:19:33 +08:00
Revert "refactor(FileItem, FileList, FilesPage, i18n): enhance file management UI and localization"
This reverts commit edeb9f84f9.
This commit is contained in:
parent
f1804bc3a0
commit
20b3db0c01
@ -470,7 +470,7 @@
|
|||||||
"count": "files",
|
"count": "files",
|
||||||
"created_at": "Created At",
|
"created_at": "Created At",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"delete.content": "Deleting a file will delete its reference from all messages. Are you sure you want to delete {{count}} files?",
|
"delete.content": "Deleting a file will delete its reference from all messages. Are you sure you want to delete this file?",
|
||||||
"delete.paintings.warning": "Image contains this file, deletion is not possible",
|
"delete.paintings.warning": "Image contains this file, deletion is not possible",
|
||||||
"delete.title": "Delete File",
|
"delete.title": "Delete File",
|
||||||
"document": "Document",
|
"document": "Document",
|
||||||
|
|||||||
@ -470,7 +470,7 @@
|
|||||||
"count": "ファイル",
|
"count": "ファイル",
|
||||||
"created_at": "作成日",
|
"created_at": "作成日",
|
||||||
"delete": "削除",
|
"delete": "削除",
|
||||||
"delete.content": "ファイルを削除すると、ファイルがすべてのメッセージで参照されることを削除します。この{{count}}ファイルを削除してもよろしいですか?",
|
"delete.content": "ファイルを削除すると、ファイルがすべてのメッセージで参照されることを削除します。このファイルを削除してもよろしいですか?",
|
||||||
"delete.paintings.warning": "画像に含まれているため、削除できません",
|
"delete.paintings.warning": "画像に含まれているため、削除できません",
|
||||||
"delete.title": "ファイルを削除",
|
"delete.title": "ファイルを削除",
|
||||||
"document": "ドキュメント",
|
"document": "ドキュメント",
|
||||||
|
|||||||
@ -470,7 +470,7 @@
|
|||||||
"count": "файлов",
|
"count": "файлов",
|
||||||
"created_at": "Дата создания",
|
"created_at": "Дата создания",
|
||||||
"delete": "Удалить",
|
"delete": "Удалить",
|
||||||
"delete.content": "Удаление файла удалит его из всех сообщений, вы уверены, что хотите удалить этот {{count}} файл",
|
"delete.content": "Удаление файла удалит его из всех сообщений, вы уверены, что хотите удалить этот файл?",
|
||||||
"delete.paintings.warning": "В изображениях содержится этот файл, удаление невозможно",
|
"delete.paintings.warning": "В изображениях содержится этот файл, удаление невозможно",
|
||||||
"delete.title": "Удалить файл",
|
"delete.title": "Удалить файл",
|
||||||
"document": "Документ",
|
"document": "Документ",
|
||||||
|
|||||||
@ -470,7 +470,7 @@
|
|||||||
"count": "个文件",
|
"count": "个文件",
|
||||||
"created_at": "创建时间",
|
"created_at": "创建时间",
|
||||||
"delete": "删除",
|
"delete": "删除",
|
||||||
"delete.content": "删除文件会删除文件在所有消息中的引用,确定要删除这{{count}}个文件吗?",
|
"delete.content": "删除文件会删除文件在所有消息中的引用,确定要删除此文件吗?",
|
||||||
"delete.paintings.warning": "绘图中包含该图片,暂时无法删除",
|
"delete.paintings.warning": "绘图中包含该图片,暂时无法删除",
|
||||||
"delete.title": "删除文件",
|
"delete.title": "删除文件",
|
||||||
"document": "文档",
|
"document": "文档",
|
||||||
|
|||||||
@ -470,7 +470,7 @@
|
|||||||
"count": "個檔案",
|
"count": "個檔案",
|
||||||
"created_at": "建立時間",
|
"created_at": "建立時間",
|
||||||
"delete": "刪除",
|
"delete": "刪除",
|
||||||
"delete.content": "刪除檔案會刪除檔案在所有訊息中的引用,確定要刪除這{{count}}個檔案嗎?",
|
"delete.content": "刪除檔案會刪除檔案在所有訊息中的引用,確定要刪除此檔案嗎?",
|
||||||
"delete.paintings.warning": "繪圖中包含該圖片,暫時無法刪除",
|
"delete.paintings.warning": "繪圖中包含該圖片,暫時無法刪除",
|
||||||
"delete.title": "刪除檔案",
|
"delete.title": "刪除檔案",
|
||||||
"document": "文件",
|
"document": "文件",
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import {
|
|||||||
GlobalOutlined,
|
GlobalOutlined,
|
||||||
LinkOutlined
|
LinkOutlined
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import { t } from 'i18next'
|
import { Flex } from 'antd'
|
||||||
import React, { memo } from 'react'
|
import React, { memo } from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
@ -21,14 +21,10 @@ interface FileItemProps {
|
|||||||
icon?: React.ReactNode
|
icon?: React.ReactNode
|
||||||
name: React.ReactNode | string
|
name: React.ReactNode | string
|
||||||
ext: string
|
ext: string
|
||||||
size: string
|
extra?: React.ReactNode | string
|
||||||
created_at: string
|
|
||||||
count?: number
|
|
||||||
checkbox?: React.ReactNode
|
|
||||||
actions: React.ReactNode
|
actions: React.ReactNode
|
||||||
}
|
}
|
||||||
style?: React.CSSProperties
|
style?: React.CSSProperties
|
||||||
gridTemplate?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getFileIcon = (type?: string) => {
|
const getFileIcon = (type?: string) => {
|
||||||
@ -79,30 +75,19 @@ const getFileIcon = (type?: string) => {
|
|||||||
return <FileUnknownFilled />
|
return <FileUnknownFilled />
|
||||||
}
|
}
|
||||||
|
|
||||||
const FileItem: React.FC<FileItemProps> = ({ fileInfo, style, gridTemplate = '' }) => {
|
const FileItem: React.FC<FileItemProps> = ({ fileInfo, style }) => {
|
||||||
const { name, ext, size, created_at, count, actions, icon, checkbox } = fileInfo
|
const { name, ext, extra, actions, icon } = fileInfo
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FileItemCard style={style}>
|
<FileItemCard style={style}>
|
||||||
<FileGrid style={{ gridTemplateColumns: gridTemplate }}>
|
<CardContent>
|
||||||
{checkbox && <FileCell>{checkbox}</FileCell>}
|
<FileIcon>{icon || getFileIcon(ext)}</FileIcon>
|
||||||
<FileCell>
|
<Flex vertical justify="center" gap={0} flex={1} style={{ width: '0px' }}>
|
||||||
<FileIcon>{icon || getFileIcon(ext)}</FileIcon>
|
<FileName>{name}</FileName>
|
||||||
</FileCell>
|
{extra && <FileInfo>{extra}</FileInfo>}
|
||||||
<FileCell>
|
</Flex>
|
||||||
<FileNameColumn>
|
<FileActions>{actions}</FileActions>
|
||||||
<FileName>{name}</FileName>
|
</CardContent>
|
||||||
{count && (
|
|
||||||
<FileCount>
|
|
||||||
{count} {t('files.count')}
|
|
||||||
</FileCount>
|
|
||||||
)}
|
|
||||||
</FileNameColumn>
|
|
||||||
</FileCell>
|
|
||||||
<FileCell style={{ textAlign: 'right' }}>{size}</FileCell>
|
|
||||||
<FileCell style={{ textAlign: 'right' }}>{created_at}</FileCell>
|
|
||||||
<FileCell style={{ justifyContent: 'center' }}>{actions}</FileCell>
|
|
||||||
</FileGrid>
|
|
||||||
</FileItemCard>
|
</FileItemCard>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -126,36 +111,20 @@ const FileItemCard = styled.div`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const FileGrid = styled.div`
|
const CardContent = styled.div`
|
||||||
display: grid;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 8px 8px 8px 16px;
|
padding: 8px 8px 8px 16px;
|
||||||
align-items: center;
|
|
||||||
`
|
|
||||||
|
|
||||||
const FileCell = styled.div`
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: stretch;
|
||||||
min-width: 0;
|
gap: 16px;
|
||||||
`
|
`
|
||||||
|
|
||||||
const FileIcon = styled.div`
|
const FileIcon = styled.div`
|
||||||
max-height: 44px;
|
max-height: 44px;
|
||||||
width: 100%;
|
|
||||||
color: var(--color-text-3);
|
color: var(--color-text-3);
|
||||||
font-size: 32px;
|
font-size: 32px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: 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`
|
const FileName = styled.div`
|
||||||
@ -173,9 +142,16 @@ const FileName = styled.div`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const FileCount = styled.div`
|
const FileInfo = styled.div`
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: var(--color-text-2);
|
color: var(--color-text-2);
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const FileActions = styled.div`
|
||||||
|
max-height: 44px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
`
|
||||||
|
|
||||||
export default memo(FileItem)
|
export default memo(FileItem)
|
||||||
|
|||||||
@ -1,6 +1,11 @@
|
|||||||
|
import FileManager from '@renderer/services/FileManager'
|
||||||
import { FileType, FileTypes } from '@renderer/types'
|
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 VirtualList from 'rc-virtual-list'
|
||||||
import React, { memo } from 'react'
|
import React, { memo } from 'react'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
import FileItem from './FileItem'
|
import FileItem from './FileItem'
|
||||||
|
|
||||||
@ -14,47 +19,43 @@ interface FileItemProps {
|
|||||||
size: string
|
size: string
|
||||||
ext: string
|
ext: string
|
||||||
created_at: string
|
created_at: string
|
||||||
checkbox?: React.ReactNode
|
|
||||||
actions: React.ReactNode
|
actions: React.ReactNode
|
||||||
}[]
|
}[]
|
||||||
files?: FileType[]
|
files?: FileType[]
|
||||||
selectedFileIds?: string[]
|
|
||||||
onFileSelect?: (fileId: string, checked: boolean) => void
|
|
||||||
columnWidths?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const FileList: React.FC<FileItemProps> = ({ list, columnWidths }) => {
|
const FileList: React.FC<FileItemProps> = ({ id, list, files }) => {
|
||||||
// if (id === FileTypes.IMAGE && files?.length && files?.length > 0) {
|
if (id === FileTypes.IMAGE && files?.length && files?.length > 0) {
|
||||||
// return (
|
return (
|
||||||
// <div style={{ padding: 16, overflowY: 'auto' }}>
|
<div style={{ padding: 16, overflowY: 'auto' }}>
|
||||||
// <Image.PreviewGroup>
|
<Image.PreviewGroup>
|
||||||
// <Row gutter={[16, 16]}>
|
<Row gutter={[16, 16]}>
|
||||||
// {files?.map((file) => (
|
{files?.map((file) => (
|
||||||
// <Col key={file.id} xs={24} sm={12} md={8} lg={4} xl={3}>
|
<Col key={file.id} xs={24} sm={12} md={8} lg={4} xl={3}>
|
||||||
// <ImageWrapper>
|
<ImageWrapper>
|
||||||
// <LoadingWrapper>
|
<LoadingWrapper>
|
||||||
// <Spin />
|
<Spin />
|
||||||
// </LoadingWrapper>
|
</LoadingWrapper>
|
||||||
// <Image
|
<Image
|
||||||
// src={FileManager.getFileUrl(file)}
|
src={FileManager.getFileUrl(file)}
|
||||||
// style={{ height: '100%', objectFit: 'cover', cursor: 'pointer' }}
|
style={{ height: '100%', objectFit: 'cover', cursor: 'pointer' }}
|
||||||
// preview={{ mask: false }}
|
preview={{ mask: false }}
|
||||||
// onLoad={(e) => {
|
onLoad={(e) => {
|
||||||
// const img = e.target as HTMLImageElement
|
const img = e.target as HTMLImageElement
|
||||||
// img.parentElement?.classList.add('loaded')
|
img.parentElement?.classList.add('loaded')
|
||||||
// }}
|
}}
|
||||||
// />
|
/>
|
||||||
// <ImageInfo>
|
<ImageInfo>
|
||||||
// <div>{formatFileSize(file.size)}</div>
|
<div>{formatFileSize(file.size)}</div>
|
||||||
// </ImageInfo>
|
</ImageInfo>
|
||||||
// </ImageWrapper>
|
</ImageWrapper>
|
||||||
// </Col>
|
</Col>
|
||||||
// ))}
|
))}
|
||||||
// </Row>
|
</Row>
|
||||||
// </Image.PreviewGroup>
|
</Image.PreviewGroup>
|
||||||
// </div>
|
</div>
|
||||||
// )
|
)
|
||||||
// }
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VirtualList
|
<VirtualList
|
||||||
@ -62,7 +63,7 @@ const FileList: React.FC<FileItemProps> = ({ list, columnWidths }) => {
|
|||||||
height={window.innerHeight - 100}
|
height={window.innerHeight - 100}
|
||||||
itemHeight={75}
|
itemHeight={75}
|
||||||
itemKey="key"
|
itemKey="key"
|
||||||
style={{ padding: '0 0 16px 0' }}
|
style={{ padding: '0 16px 16px 16px' }}
|
||||||
styles={{
|
styles={{
|
||||||
verticalScrollBar: {
|
verticalScrollBar: {
|
||||||
width: 6
|
width: 6
|
||||||
@ -75,21 +76,16 @@ const FileList: React.FC<FileItemProps> = ({ list, columnWidths }) => {
|
|||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
height: '75px',
|
height: '75px',
|
||||||
paddingTop: '8px',
|
paddingTop: '12px'
|
||||||
margin: '0 16px'
|
|
||||||
}}>
|
}}>
|
||||||
<FileItem
|
<FileItem
|
||||||
key={item.key}
|
key={item.key}
|
||||||
fileInfo={{
|
fileInfo={{
|
||||||
name: item.file,
|
name: item.file,
|
||||||
ext: item.ext,
|
ext: item.ext,
|
||||||
size: item.size,
|
extra: `${item.created_at} · ${item.count}${t('files.count')} · ${item.size}`,
|
||||||
created_at: item.created_at,
|
|
||||||
count: item.count,
|
|
||||||
checkbox: item.checkbox,
|
|
||||||
actions: item.actions
|
actions: item.actions
|
||||||
}}
|
}}
|
||||||
gridTemplate={columnWidths}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -97,70 +93,70 @@ const FileList: React.FC<FileItemProps> = ({ list, columnWidths }) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// const ImageWrapper = styled.div`
|
const ImageWrapper = styled.div`
|
||||||
// position: relative;
|
position: relative;
|
||||||
// aspect-ratio: 1;
|
aspect-ratio: 1;
|
||||||
// overflow: hidden;
|
overflow: hidden;
|
||||||
// border-radius: 8px;
|
border-radius: 8px;
|
||||||
// background-color: var(--color-background-soft);
|
background-color: var(--color-background-soft);
|
||||||
// display: flex;
|
display: flex;
|
||||||
// align-items: center;
|
align-items: center;
|
||||||
// justify-content: center;
|
justify-content: center;
|
||||||
// border: 0.5px solid var(--color-border);
|
border: 0.5px solid var(--color-border);
|
||||||
|
|
||||||
// .ant-image {
|
.ant-image {
|
||||||
// height: 100%;
|
height: 100%;
|
||||||
// width: 100%;
|
width: 100%;
|
||||||
// opacity: 0;
|
opacity: 0;
|
||||||
// transition:
|
transition:
|
||||||
// opacity 0.3s ease,
|
opacity 0.3s ease,
|
||||||
// transform 0.3s ease;
|
transform 0.3s ease;
|
||||||
|
|
||||||
// &.loaded {
|
&.loaded {
|
||||||
// opacity: 1;
|
opacity: 1;
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
|
|
||||||
// &:hover {
|
&:hover {
|
||||||
// .ant-image.loaded {
|
.ant-image.loaded {
|
||||||
// transform: scale(1.05);
|
transform: scale(1.05);
|
||||||
// }
|
}
|
||||||
|
|
||||||
// div:last-child {
|
div:last-child {
|
||||||
// opacity: 1;
|
opacity: 1;
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
// `
|
`
|
||||||
|
|
||||||
// const LoadingWrapper = styled.div`
|
const LoadingWrapper = styled.div`
|
||||||
// position: absolute;
|
position: absolute;
|
||||||
// top: 0;
|
top: 0;
|
||||||
// left: 0;
|
left: 0;
|
||||||
// right: 0;
|
right: 0;
|
||||||
// bottom: 0;
|
bottom: 0;
|
||||||
// display: flex;
|
display: flex;
|
||||||
// align-items: center;
|
align-items: center;
|
||||||
// justify-content: center;
|
justify-content: center;
|
||||||
// background-color: var(--color-background-soft);
|
background-color: var(--color-background-soft);
|
||||||
// `
|
`
|
||||||
|
|
||||||
// const ImageInfo = styled.div`
|
const ImageInfo = styled.div`
|
||||||
// position: absolute;
|
position: absolute;
|
||||||
// bottom: 0;
|
bottom: 0;
|
||||||
// left: 0;
|
left: 0;
|
||||||
// right: 0;
|
right: 0;
|
||||||
// background: rgba(0, 0, 0, 0.6);
|
background: rgba(0, 0, 0, 0.6);
|
||||||
// color: white;
|
color: white;
|
||||||
// padding: 5px 8px;
|
padding: 5px 8px;
|
||||||
// opacity: 0;
|
opacity: 0;
|
||||||
// transition: opacity 0.3s ease;
|
transition: opacity 0.3s ease;
|
||||||
// font-size: 12px;
|
font-size: 12px;
|
||||||
|
|
||||||
// > div:first-child {
|
> div:first-child {
|
||||||
// white-space: nowrap;
|
white-space: nowrap;
|
||||||
// overflow: hidden;
|
overflow: hidden;
|
||||||
// text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
// }
|
}
|
||||||
// `
|
`
|
||||||
|
|
||||||
export default memo(FileList)
|
export default memo(FileList)
|
||||||
|
|||||||
@ -15,7 +15,7 @@ import store from '@renderer/store'
|
|||||||
import { FileType, FileTypes } from '@renderer/types'
|
import { FileType, FileTypes } from '@renderer/types'
|
||||||
import { Message } from '@renderer/types/newMessage'
|
import { Message } from '@renderer/types/newMessage'
|
||||||
import { formatFileSize } from '@renderer/utils'
|
import { formatFileSize } from '@renderer/utils'
|
||||||
import { Button, Checkbox, Empty, Flex, Popconfirm } from 'antd'
|
import { Button, Empty, Flex, Popconfirm } from 'antd'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { useLiveQuery } from 'dexie-react-hooks'
|
import { useLiveQuery } from 'dexie-react-hooks'
|
||||||
import { File as FileIcon, FileImage, FileText, FileType as FileTypeIcon } from 'lucide-react'
|
import { File as FileIcon, FileImage, FileText, FileType as FileTypeIcon } from 'lucide-react'
|
||||||
@ -25,8 +25,6 @@ import styled from 'styled-components'
|
|||||||
|
|
||||||
import FileList from './FileList'
|
import FileList from './FileList'
|
||||||
|
|
||||||
const GRID_TEMPLATE = 'auto 60px 1fr 120px 140px 100px'
|
|
||||||
|
|
||||||
type SortField = 'created_at' | 'size' | 'name'
|
type SortField = 'created_at' | 'size' | 'name'
|
||||||
type SortOrder = 'asc' | 'desc'
|
type SortOrder = 'asc' | 'desc'
|
||||||
|
|
||||||
@ -35,7 +33,6 @@ const FilesPage: FC = () => {
|
|||||||
const [fileType, setFileType] = useState<string>('document')
|
const [fileType, setFileType] = useState<string>('document')
|
||||||
const [sortField, setSortField] = useState<SortField>('created_at')
|
const [sortField, setSortField] = useState<SortField>('created_at')
|
||||||
const [sortOrder, setSortOrder] = useState<SortOrder>('desc')
|
const [sortOrder, setSortOrder] = useState<SortOrder>('desc')
|
||||||
const [selectedFileIds, setSelectedFileIds] = useState<string[]>([])
|
|
||||||
|
|
||||||
const tempFilesSort = (files: FileType[]) => {
|
const tempFilesSort = (files: FileType[]) => {
|
||||||
return files.sort((a, b) => {
|
return files.sort((a, b) => {
|
||||||
@ -173,38 +170,6 @@ 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) => {
|
const dataSource = sortedFiles?.map((file) => {
|
||||||
return {
|
return {
|
||||||
key: file.id,
|
key: file.id,
|
||||||
@ -216,18 +181,12 @@ const FilesPage: FC = () => {
|
|||||||
ext: file.ext,
|
ext: file.ext,
|
||||||
created_at: dayjs(file.created_at).format('MM-DD HH:mm'),
|
created_at: dayjs(file.created_at).format('MM-DD HH:mm'),
|
||||||
created_at_unix: dayjs(file.created_at).unix(),
|
created_at_unix: dayjs(file.created_at).unix(),
|
||||||
checkbox: (
|
|
||||||
<Checkbox
|
|
||||||
checked={selectedFileIds.includes(file.id)}
|
|
||||||
onChange={(e) => handleFileSelect(file.id, e.target.checked)}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
actions: (
|
actions: (
|
||||||
<Flex align="center" gap={0} style={{ opacity: 0.7 }}>
|
<Flex align="center" gap={0} style={{ opacity: 0.7 }}>
|
||||||
<Button type="text" icon={<EditOutlined />} onClick={() => handleRename(file.id)} />
|
<Button type="text" icon={<EditOutlined />} onClick={() => handleRename(file.id)} />
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
title={t('files.delete.title')}
|
title={t('files.delete.title')}
|
||||||
description={t('files.delete.content', { count: 1 })}
|
description={t('files.delete.content')}
|
||||||
okText={t('common.confirm')}
|
okText={t('common.confirm')}
|
||||||
cancelText={t('common.cancel')}
|
cancelText={t('common.cancel')}
|
||||||
onConfirm={() => handleDelete(file.id)}
|
onConfirm={() => handleDelete(file.id)}
|
||||||
@ -264,90 +223,26 @@ const FilesPage: FC = () => {
|
|||||||
))}
|
))}
|
||||||
</SideNav>
|
</SideNav>
|
||||||
<MainContent>
|
<MainContent>
|
||||||
<TableHeader>
|
<SortContainer>
|
||||||
<TableGrid style={{ gridTemplateColumns: GRID_TEMPLATE }}>
|
{['created_at', 'size', 'name'].map((field) => (
|
||||||
<TableCell>
|
<SortButton
|
||||||
<Checkbox
|
key={field}
|
||||||
indeterminate={selectedFileIds.length > 0 && selectedFileIds.length < sortedFiles.length}
|
active={sortField === field}
|
||||||
checked={selectedFileIds.length === sortedFiles.length && sortedFiles.length > 0}
|
onClick={() => {
|
||||||
onChange={(e) => handleSelectAll(e.target.checked)}
|
if (sortField === field) {
|
||||||
disabled={sortedFiles.length === 0}
|
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')
|
||||||
/>
|
} else {
|
||||||
</TableCell>
|
setSortField(field as 'created_at' | 'size' | 'name')
|
||||||
<TableCell>{/* 图标列 */}</TableCell>
|
setSortOrder('desc')
|
||||||
<TableCell>
|
}
|
||||||
<SortButton
|
}}>
|
||||||
active={sortField === 'name'}
|
{t(`files.${field}`)}
|
||||||
onClick={() => {
|
{sortField === field && (sortOrder === 'desc' ? <SortDescendingOutlined /> : <SortAscendingOutlined />)}
|
||||||
if (sortField === 'name') {
|
</SortButton>
|
||||||
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')
|
))}
|
||||||
} else {
|
</SortContainer>
|
||||||
setSortField('name')
|
|
||||||
setSortOrder('desc')
|
|
||||||
}
|
|
||||||
}}>
|
|
||||||
{t('files.name')}
|
|
||||||
{sortField === 'name' &&
|
|
||||||
(sortOrder === 'desc' ? <SortDescendingOutlined /> : <SortAscendingOutlined />)}
|
|
||||||
</SortButton>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<SortButton
|
|
||||||
active={sortField === 'size'}
|
|
||||||
onClick={() => {
|
|
||||||
if (sortField === 'size') {
|
|
||||||
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')
|
|
||||||
} else {
|
|
||||||
setSortField('size')
|
|
||||||
setSortOrder('desc')
|
|
||||||
}
|
|
||||||
}}>
|
|
||||||
{t('files.size')}
|
|
||||||
{sortField === 'size' &&
|
|
||||||
(sortOrder === 'desc' ? <SortDescendingOutlined /> : <SortAscendingOutlined />)}
|
|
||||||
</SortButton>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<SortButton
|
|
||||||
active={sortField === 'created_at'}
|
|
||||||
onClick={() => {
|
|
||||||
if (sortField === 'created_at') {
|
|
||||||
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')
|
|
||||||
} else {
|
|
||||||
setSortField('created_at')
|
|
||||||
setSortOrder('desc')
|
|
||||||
}
|
|
||||||
}}>
|
|
||||||
{t('files.created_at')}
|
|
||||||
{sortField === 'created_at' &&
|
|
||||||
(sortOrder === 'desc' ? <SortDescendingOutlined /> : <SortAscendingOutlined />)}
|
|
||||||
</SortButton>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell style={{ justifyContent: 'center' }}>
|
|
||||||
{selectedFileIds.length > 0 && (
|
|
||||||
<Popconfirm
|
|
||||||
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 style={{ color: 'red' }} />}>
|
|
||||||
<Button type="text" danger icon={<DeleteOutlined />} />
|
|
||||||
</Popconfirm>
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
</TableGrid>
|
|
||||||
</TableHeader>
|
|
||||||
|
|
||||||
{dataSource && dataSource?.length > 0 ? (
|
{dataSource && dataSource?.length > 0 ? (
|
||||||
<FileList
|
<FileList id={fileType} list={dataSource} files={sortedFiles} />
|
||||||
id={fileType}
|
|
||||||
list={dataSource}
|
|
||||||
files={sortedFiles}
|
|
||||||
selectedFileIds={selectedFileIds}
|
|
||||||
onFileSelect={handleFileSelect}
|
|
||||||
columnWidths={GRID_TEMPLATE}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||||
)}
|
)}
|
||||||
@ -370,6 +265,14 @@ const MainContent = styled.div`
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const SortContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-bottom: 0.5px solid var(--color-border);
|
||||||
|
`
|
||||||
|
|
||||||
const ContentContainer = styled.div`
|
const ContentContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@ -412,39 +315,20 @@ const SideNav = styled.div`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const SortButton = styled.div<{ active?: boolean }>`
|
const SortButton = styled(Button)<{ active?: boolean }>`
|
||||||
position: relative;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
|
padding: 4px 12px;
|
||||||
height: 30px;
|
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)')};
|
color: ${(props) => (props.active ? 'var(--color-text)' : 'var(--color-text-secondary)')};
|
||||||
font-weight: ${(props) => (props.active ? '500' : '400')};
|
|
||||||
cursor: pointer;
|
|
||||||
z-index: 1;
|
|
||||||
|
|
||||||
&::before {
|
&:hover {
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: -4px;
|
|
||||||
left: -8px;
|
|
||||||
right: -8px;
|
|
||||||
bottom: -4px;
|
|
||||||
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')};
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover::before {
|
|
||||||
background-color: var(--color-background-soft);
|
background-color: var(--color-background-soft);
|
||||||
border-color: var(--color-border);
|
color: var(--color-text);
|
||||||
}
|
|
||||||
|
|
||||||
> * {
|
|
||||||
position: relative;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.anticon {
|
.anticon {
|
||||||
@ -452,25 +336,4 @@ const SortButton = styled.div<{ active?: boolean }>`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const TableGrid = styled.div`
|
|
||||||
display: grid;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 8px 8px 0px 16px;
|
|
||||||
align-items: center;
|
|
||||||
`
|
|
||||||
|
|
||||||
const TableCell = styled.div`
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-start;
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const TableHeader = styled.div`
|
|
||||||
margin: 0 16px;
|
|
||||||
`
|
|
||||||
|
|
||||||
export default FilesPage
|
export default FilesPage
|
||||||
|
|||||||
@ -21,8 +21,8 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
import CustomCollapse from '../../components/CustomCollapse'
|
import CustomCollapse from '../../components/CustomCollapse'
|
||||||
|
import FileItem from '../files/FileItem'
|
||||||
import { NavbarIcon } from '../home/ChatNavbar'
|
import { NavbarIcon } from '../home/ChatNavbar'
|
||||||
import KnowledgeFileItem from './components/KnowledgeFileItem'
|
|
||||||
import KnowledgeSearchPopup from './components/KnowledgeSearchPopup'
|
import KnowledgeSearchPopup from './components/KnowledgeSearchPopup'
|
||||||
import KnowledgeSettingsPopup from './components/KnowledgeSettingsPopup'
|
import KnowledgeSettingsPopup from './components/KnowledgeSettingsPopup'
|
||||||
import StatusIcon from './components/StatusIcon'
|
import StatusIcon from './components/StatusIcon'
|
||||||
@ -323,8 +323,8 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
|||||||
{(item) => {
|
{(item) => {
|
||||||
const file = item.content as FileType
|
const file = item.content as FileType
|
||||||
return (
|
return (
|
||||||
<div style={{ height: '75px', marginTop: '12px' }}>
|
<div style={{ height: '75px', paddingTop: '12px' }}>
|
||||||
<KnowledgeFileItem
|
<FileItem
|
||||||
key={item.id}
|
key={item.id}
|
||||||
fileInfo={{
|
fileInfo={{
|
||||||
name: (
|
name: (
|
||||||
@ -381,7 +381,7 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
|||||||
<FlexColumn>
|
<FlexColumn>
|
||||||
{directoryItems.length === 0 && <EmptyView />}
|
{directoryItems.length === 0 && <EmptyView />}
|
||||||
{directoryItems.reverse().map((item) => (
|
{directoryItems.reverse().map((item) => (
|
||||||
<KnowledgeFileItem
|
<FileItem
|
||||||
key={item.id}
|
key={item.id}
|
||||||
fileInfo={{
|
fileInfo={{
|
||||||
name: (
|
name: (
|
||||||
@ -433,7 +433,7 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
|||||||
<FlexColumn>
|
<FlexColumn>
|
||||||
{urlItems.length === 0 && <EmptyView />}
|
{urlItems.length === 0 && <EmptyView />}
|
||||||
{urlItems.reverse().map((item) => (
|
{urlItems.reverse().map((item) => (
|
||||||
<KnowledgeFileItem
|
<FileItem
|
||||||
key={item.id}
|
key={item.id}
|
||||||
fileInfo={{
|
fileInfo={{
|
||||||
name: (
|
name: (
|
||||||
@ -510,7 +510,7 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
|||||||
<FlexColumn>
|
<FlexColumn>
|
||||||
{sitemapItems.length === 0 && <EmptyView />}
|
{sitemapItems.length === 0 && <EmptyView />}
|
||||||
{sitemapItems.reverse().map((item) => (
|
{sitemapItems.reverse().map((item) => (
|
||||||
<KnowledgeFileItem
|
<FileItem
|
||||||
key={item.id}
|
key={item.id}
|
||||||
fileInfo={{
|
fileInfo={{
|
||||||
name: (
|
name: (
|
||||||
@ -565,7 +565,7 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
|||||||
<FlexColumn>
|
<FlexColumn>
|
||||||
{noteItems.length === 0 && <EmptyView />}
|
{noteItems.length === 0 && <EmptyView />}
|
||||||
{noteItems.reverse().map((note) => (
|
{noteItems.reverse().map((note) => (
|
||||||
<KnowledgeFileItem
|
<FileItem
|
||||||
key={note.id}
|
key={note.id}
|
||||||
fileInfo={{
|
fileInfo={{
|
||||||
name: <span onClick={() => handleEditNote(note)}>{(note.content as string).slice(0, 50)}...</span>,
|
name: <span onClick={() => handleEditNote(note)}>{(note.content as string).slice(0, 50)}...</span>,
|
||||||
|
|||||||
@ -1,124 +0,0 @@
|
|||||||
import { getFileIcon } from '@renderer/pages/home/Inputbar/AttachmentPreview'
|
|
||||||
import React, { memo } from 'react'
|
|
||||||
import styled from 'styled-components'
|
|
||||||
|
|
||||||
interface KnowledgeFileItemProps {
|
|
||||||
fileInfo: {
|
|
||||||
icon?: React.ReactNode
|
|
||||||
name: React.ReactNode | string
|
|
||||||
ext: string
|
|
||||||
extra?: string
|
|
||||||
actions: React.ReactNode
|
|
||||||
}
|
|
||||||
style?: React.CSSProperties
|
|
||||||
}
|
|
||||||
|
|
||||||
const KnowledgeFileItem: React.FC<KnowledgeFileItemProps> = ({ fileInfo, style }) => {
|
|
||||||
const { name, ext, extra, actions, icon } = fileInfo
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FileItemCard style={style}>
|
|
||||||
<FileContainer>
|
|
||||||
<FileIconContainer>
|
|
||||||
<FileIcon>{icon || getFileIcon(ext)}</FileIcon>
|
|
||||||
</FileIconContainer>
|
|
||||||
<FileContent>
|
|
||||||
<FileNameContainer>
|
|
||||||
<FileName>{name}</FileName>
|
|
||||||
{extra && <FileExtra>{extra}</FileExtra>}
|
|
||||||
</FileNameContainer>
|
|
||||||
</FileContent>
|
|
||||||
<FileActions>{actions}</FileActions>
|
|
||||||
</FileContainer>
|
|
||||||
</FileItemCard>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const FileItemCard = styled.div`
|
|
||||||
border-radius: 8px;
|
|
||||||
overflow: hidden;
|
|
||||||
border: 0.5px solid var(--color-border);
|
|
||||||
flex-shrink: 0;
|
|
||||||
transition:
|
|
||||||
box-shadow 0.2s ease,
|
|
||||||
background-color 0.2s ease;
|
|
||||||
--shadow-color: rgba(0, 0, 0, 0.05);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
box-shadow:
|
|
||||||
0 10px 15px -3px var(--shadow-color),
|
|
||||||
0 4px 6px -4px var(--shadow-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
body[theme-mode='dark'] & {
|
|
||||||
--shadow-color: rgba(255, 255, 255, 0.02);
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const FileContainer = styled.div`
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
padding: 12px 16px;
|
|
||||||
min-height: 63px;
|
|
||||||
`
|
|
||||||
|
|
||||||
const FileIconContainer = styled.div`
|
|
||||||
flex-shrink: 0;
|
|
||||||
`
|
|
||||||
|
|
||||||
const FileIcon = styled.div`
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
color: var(--color-text-3);
|
|
||||||
font-size: 32px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
`
|
|
||||||
|
|
||||||
const FileContent = styled.div`
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
`
|
|
||||||
|
|
||||||
const FileNameContainer = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
min-width: 0;
|
|
||||||
`
|
|
||||||
|
|
||||||
const FileName = styled.div`
|
|
||||||
font-size: 15px;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: color 0.2s ease;
|
|
||||||
|
|
||||||
span {
|
|
||||||
font-size: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: var(--color-primary);
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const FileExtra = styled.div`
|
|
||||||
font-size: 13px;
|
|
||||||
color: var(--color-text-2);
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
`
|
|
||||||
|
|
||||||
const FileActions = styled.div`
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
`
|
|
||||||
|
|
||||||
export default memo(KnowledgeFileItem)
|
|
||||||
Loading…
Reference in New Issue
Block a user