diff --git a/backend/agenteval/web/file_schemas.py b/backend/agenteval/web/file_schemas.py index 028ca9b..1f63b51 100644 --- a/backend/agenteval/web/file_schemas.py +++ b/backend/agenteval/web/file_schemas.py @@ -41,6 +41,7 @@ class FileRecordResponse(BaseModel): mime_type: str file_ext: str category_id: str | None + storage_directory: str created_at: datetime | None @field_serializer("created_at") @@ -56,6 +57,7 @@ class FileRecordResponse(BaseModel): mime_type=record.mime_type, file_ext=record.file_ext, category_id=record.category_id, + storage_directory="data/files", created_at=record.created_at, ) diff --git a/frontend/web/src/api.ts b/frontend/web/src/api.ts index c943940..d406788 100644 --- a/frontend/web/src/api.ts +++ b/frontend/web/src/api.ts @@ -166,6 +166,7 @@ export interface FileRecord { mime_type: string file_ext: string category_id: string | null + storage_directory: string created_at: string | null } diff --git a/frontend/web/src/components/files/FileCategoryTree.tsx b/frontend/web/src/components/files/FileCategoryTree.tsx index 2f2812b..21f17b5 100644 --- a/frontend/web/src/components/files/FileCategoryTree.tsx +++ b/frontend/web/src/components/files/FileCategoryTree.tsx @@ -1,7 +1,6 @@ import { DeleteOutlined, EditOutlined, - FileOutlined, FolderAddOutlined, FolderOutlined, PlusOutlined, @@ -32,10 +31,10 @@ export default function FileCategoryTree({ }: FileCategoryTreeProps) { const mapCategory = (category: FileCategory): DataNode => ({ key: category.id, - icon: , title: (
- {category.name} + + {category.name}
void @@ -27,6 +29,7 @@ interface FileTableProps { export default function FileTable({ files, + categories, loading, categoryName, onRefresh, @@ -34,12 +37,30 @@ export default function FileTable({ onDownload, onDelete, }: FileTableProps) { + const screens = Grid.useBreakpoint() const totalSize = useMemo(() => files.reduce((sum, file) => sum + file.file_size, 0), [files]) const extensionCounts = useMemo(() => files.reduce>((counts, file) => { counts[file.file_ext] = (counts[file.file_ext] || 0) + 1 return counts }, {}), [files]) + const renderLocation = (record: FileRecord, mobile = false) => { + const categoryPath = record.category_id + ? getCategoryPath(categories, record.category_id) + : [] + const categoryLabel = categoryPath.length > 0 ? categoryPath.join(' / ') : '未分类' + + return ( +
+
+ + {categoryLabel} +
+ {record.storage_directory} +
+ ) + } + const columns: ColumnsType = [ { title: '文件名', @@ -47,12 +68,22 @@ export default function FileTable({ key: 'name', ellipsis: true, render: (name: string, record) => ( - - - {name} - +
+
+ + {name} +
+ {renderLocation(record, true)} +
), }, + { + title: '文件位置', + key: 'location', + width: 260, + responsive: ['md'], + render: (_, record) => renderLocation(record), + }, { title: '大小', dataIndex: 'file_size', @@ -160,7 +191,7 @@ export default function FileTable({ columns={columns} rowKey="id" loading={loading} - scroll={{ x: 620 }} + scroll={screens.md ? { x: 900 } : undefined} pagination={files.length > 10 ? { pageSize: 15, showSizeChanger: true, diff --git a/frontend/web/src/index.css b/frontend/web/src/index.css index 4ff6378..5acfb5c 100644 --- a/frontend/web/src/index.css +++ b/frontend/web/src/index.css @@ -164,15 +164,32 @@ body { flex: 1; } -.cat-tree-node { - display: flex; - align-items: center; - justify-content: space-between; - gap: 4px; +.files-category-scroll .ant-tree-title { + display: block; width: 100%; min-width: 0; } +.cat-tree-node { + display: grid; + grid-template-columns: 16px minmax(0, 1fr) auto; + align-items: center; + gap: 6px; + width: 100%; + min-width: 0; +} + +.cat-tree-node-all { + grid-template-columns: 16px minmax(0, 1fr); + font-size: 14px; + font-weight: 600; +} + +.cat-tree-icon { + color: #d48806; + font-size: 15px; +} + .cat-tree-label { min-width: 0; overflow: hidden; @@ -238,15 +255,67 @@ body { margin-left: auto; } -.files-name-cell { - display: block; +.files-name-stack { + display: grid; + gap: 5px; min-width: 0; +} + +.files-name-row { + display: grid; + grid-template-columns: 16px minmax(0, 1fr); + align-items: center; + gap: 8px; + min-width: 0; +} + +.files-name-cell { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; } +.files-location-cell { + display: grid; + gap: 3px; + min-width: 0; +} + +.files-category-path { + display: grid; + grid-template-columns: 14px minmax(0, 1fr); + align-items: center; + gap: 5px; + min-width: 0; + color: #595959; + font-size: 13px; +} + +.files-category-path .anticon { + color: #d48806; +} + +.files-category-path span, +.files-location-cell code { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.files-location-cell code { + display: block; + padding-left: 19px; + color: #8c8c8c; + font-size: 12px; + background: transparent; +} + +.files-location-mobile { + display: none; +} + .files-empty { display: flex; flex-direction: column; @@ -293,3 +362,14 @@ body { margin-left: auto; } } + +@media (max-width: 767px) { + .files-location-mobile { + display: grid; + padding-left: 24px; + } + + .files-location-mobile code { + padding-left: 19px; + } +} diff --git a/frontend/web/src/pages/Files.tsx b/frontend/web/src/pages/Files.tsx index eb6c07e..3f37c6f 100644 --- a/frontend/web/src/pages/Files.tsx +++ b/frontend/web/src/pages/Files.tsx @@ -114,6 +114,7 @@ export default function FilesPage() { /> void refresh().catch(() => undefined)} diff --git a/frontend/web/src/utils/fileTree.ts b/frontend/web/src/utils/fileTree.ts index c62e512..cb253b9 100644 --- a/frontend/web/src/utils/fileTree.ts +++ b/frontend/web/src/utils/fileTree.ts @@ -18,6 +18,15 @@ export function categoryContains(category: FileCategory, id: string): boolean { return category.id === id || category.children.some((child) => categoryContains(child, id)) } +export function getCategoryPath(categories: FileCategory[], id: string): string[] { + for (const category of categories) { + if (category.id === id) return [category.name] + const nestedPath = getCategoryPath(category.children, id) + if (nestedPath.length > 0) return [category.name, ...nestedPath] + } + return [] +} + export function flattenCategoryOptions( categories: FileCategory[], depth = 0, diff --git a/tests/integration/test_files_api.py b/tests/integration/test_files_api.py index 9e6ea4b..744b978 100644 --- a/tests/integration/test_files_api.py +++ b/tests/integration/test_files_api.py @@ -134,6 +134,8 @@ def test_upload_download_and_delete_file(files_client): record = uploaded.json() assert record["original_name"] == "notes.txt" assert record["file_size"] == 5 + assert record["storage_directory"] == "data/files" + assert client.get("/api/files").json()[0]["storage_directory"] == "data/files" assert len(list(files_dir.iterdir())) == 1 downloaded = client.get(f"/api/files/{record['id']}/download")