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}
,
title: (
-
-
- 全部文件
-
+
+
+ 全部文件
+
),
children: categories.map(mapCategory),
}]
@@ -112,7 +110,6 @@ export default function FileCategoryTree({
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")