修改前端图标以及修改oss存储桶位置
This commit is contained in:
@@ -4,8 +4,6 @@ import {
|
||||
CheckCircle2,
|
||||
ChevronDown,
|
||||
Folder,
|
||||
FileText,
|
||||
Image as ImageIcon,
|
||||
Download,
|
||||
ChevronRight,
|
||||
ChevronUp,
|
||||
@@ -28,10 +26,12 @@ import {
|
||||
import { NetdiskPathPickerModal } from '@/src/components/ui/NetdiskPathPickerModal';
|
||||
import { Button } from '@/src/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/src/components/ui/card';
|
||||
import { FileTypeIcon, getFileTypeTheme } from '@/src/components/ui/FileTypeIcon';
|
||||
import { Input } from '@/src/components/ui/input';
|
||||
import { ApiError, apiBinaryUploadRequest, apiDownload, apiRequest, apiUploadRequest } from '@/src/lib/api';
|
||||
import { copyFileToNetdiskPath } from '@/src/lib/file-copy';
|
||||
import { moveFileToNetdiskPath } from '@/src/lib/file-move';
|
||||
import { resolveStoredFileType, type FileTypeKind } from '@/src/lib/file-type';
|
||||
import { readCachedValue, writeCachedValue } from '@/src/lib/cache';
|
||||
import { createFileShareLink, getCurrentFileShareUrl } from '@/src/lib/file-share';
|
||||
import { getFilesLastPathCacheKey, getFilesListCacheKey } from '@/src/lib/page-cache';
|
||||
@@ -147,27 +147,31 @@ function formatDateTime(value: string) {
|
||||
}
|
||||
|
||||
function toUiFile(file: FileMetadata) {
|
||||
const extension = file.filename.includes('.') ? file.filename.split('.').pop()?.toLowerCase() : '';
|
||||
let type = extension || 'document';
|
||||
|
||||
if (file.directory) {
|
||||
type = 'folder';
|
||||
} else if (file.contentType?.startsWith('image/')) {
|
||||
type = 'image';
|
||||
} else if (file.contentType?.includes('pdf')) {
|
||||
type = 'pdf';
|
||||
}
|
||||
const resolvedType = resolveStoredFileType({
|
||||
filename: file.filename,
|
||||
contentType: file.contentType,
|
||||
directory: file.directory,
|
||||
});
|
||||
|
||||
return {
|
||||
id: file.id,
|
||||
name: file.filename,
|
||||
type,
|
||||
type: resolvedType.kind,
|
||||
typeLabel: resolvedType.label,
|
||||
size: file.directory ? '—' : formatFileSize(file.size),
|
||||
modified: formatDateTime(file.createdAt),
|
||||
};
|
||||
}
|
||||
|
||||
type UiFile = ReturnType<typeof toUiFile>;
|
||||
interface UiFile {
|
||||
id: FileMetadata['id'];
|
||||
modified: string;
|
||||
name: string;
|
||||
size: string;
|
||||
type: FileTypeKind;
|
||||
typeLabel: string;
|
||||
}
|
||||
|
||||
type NetdiskTargetAction = 'move' | 'copy';
|
||||
|
||||
export default function Files() {
|
||||
@@ -854,20 +858,23 @@ export default function Files() {
|
||||
>
|
||||
<td className="py-3 pl-4">
|
||||
<div className="flex items-center gap-3">
|
||||
{file.type === 'folder' ? (
|
||||
<Folder className="w-5 h-5 text-[#336EFF]" />
|
||||
) : file.type === 'image' ? (
|
||||
<ImageIcon className="w-5 h-5 text-purple-400" />
|
||||
) : (
|
||||
<FileText className="w-5 h-5 text-blue-400" />
|
||||
)}
|
||||
<FileTypeIcon type={file.type} size="sm" />
|
||||
<span className={cn('text-sm font-medium', selectedFile?.id === file.id ? 'text-[#336EFF]' : 'text-slate-200')}>
|
||||
{file.name}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-3 text-sm text-slate-400 hidden md:table-cell">{file.modified}</td>
|
||||
<td className="py-3 text-sm text-slate-400 hidden lg:table-cell uppercase">{file.type}</td>
|
||||
<td className="py-3 text-sm text-slate-400 hidden lg:table-cell">
|
||||
<span
|
||||
className={cn(
|
||||
'inline-flex items-center rounded-full px-2.5 py-1 text-[11px] font-medium tracking-wide',
|
||||
getFileTypeTheme(file.type).badgeClassName,
|
||||
)}
|
||||
>
|
||||
{file.typeLabel}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-3 text-sm text-slate-400 font-mono">{file.size}</td>
|
||||
<td className="py-3 pr-4 text-right">
|
||||
<FileActionMenu
|
||||
@@ -916,20 +923,15 @@ export default function Files() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-3 flex h-16 w-16 items-center justify-center rounded-2xl bg-white/5 transition-colors group-hover:bg-white/10">
|
||||
{file.type === 'folder' ? (
|
||||
<Folder className="w-8 h-8 text-[#336EFF]" />
|
||||
) : file.type === 'image' ? (
|
||||
<ImageIcon className="w-8 h-8 text-purple-400" />
|
||||
) : (
|
||||
<FileText className="w-8 h-8 text-blue-400" />
|
||||
)}
|
||||
</div>
|
||||
<FileTypeIcon type={file.type} size="lg" className="mb-3 transition-transform duration-200 group-hover:scale-[1.03]" />
|
||||
|
||||
<span className={cn('w-full truncate px-2 text-center text-sm font-medium', selectedFile?.id === file.id ? 'text-[#336EFF]' : 'text-slate-200')}>
|
||||
{file.name}
|
||||
</span>
|
||||
<span className="mt-1 text-xs text-slate-500">
|
||||
<span className={cn('mt-1 inline-flex rounded-full px-2 py-1 text-[11px] font-medium', getFileTypeTheme(file.type).badgeClassName)}>
|
||||
{file.typeLabel}
|
||||
</span>
|
||||
<span className="mt-2 text-xs text-slate-500">
|
||||
{file.type === 'folder' ? file.modified : file.size}
|
||||
</span>
|
||||
</div>
|
||||
@@ -967,15 +969,7 @@ export default function Files() {
|
||||
</CardHeader>
|
||||
<CardContent className="p-6 space-y-6">
|
||||
<div className="flex flex-col items-center text-center space-y-3">
|
||||
<div className="w-16 h-16 rounded-2xl bg-[#336EFF]/10 flex items-center justify-center">
|
||||
{selectedFile.type === 'folder' ? (
|
||||
<Folder className="w-8 h-8 text-[#336EFF]" />
|
||||
) : selectedFile.type === 'image' ? (
|
||||
<ImageIcon className="w-8 h-8 text-purple-400" />
|
||||
) : (
|
||||
<FileText className="w-8 h-8 text-blue-400" />
|
||||
)}
|
||||
</div>
|
||||
<FileTypeIcon type={selectedFile.type} size="lg" />
|
||||
<h3 className="text-sm font-medium text-white break-all">{selectedFile.name}</h3>
|
||||
</div>
|
||||
|
||||
@@ -983,7 +977,7 @@ export default function Files() {
|
||||
<DetailItem label="位置" value={`网盘 > ${currentPath.length === 0 ? '根目录' : currentPath.join(' > ')}`} />
|
||||
<DetailItem label="大小" value={selectedFile.size} />
|
||||
<DetailItem label="修改时间" value={selectedFile.modified} />
|
||||
<DetailItem label="类型" value={selectedFile.type.toUpperCase()} />
|
||||
<DetailItem label="类型" value={selectedFile.typeLabel} />
|
||||
</div>
|
||||
|
||||
<div className="pt-4 space-y-3 border-t border-white/10">
|
||||
@@ -1090,18 +1084,26 @@ export default function Files() {
|
||||
)}
|
||||
|
||||
<div className="relative z-10 flex items-start gap-3">
|
||||
<div className="mt-0.5">
|
||||
{task.status === 'completed' ? (
|
||||
<CheckCircle2 className="h-5 w-5 text-emerald-500" />
|
||||
) : task.status === 'error' ? (
|
||||
<TriangleAlert className="h-5 w-5 text-rose-400" />
|
||||
) : (
|
||||
<FileUp className="h-5 w-5 animate-pulse text-[#336EFF]" />
|
||||
)}
|
||||
</div>
|
||||
<FileTypeIcon type={task.type} size="sm" className="mt-0.5" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate text-sm font-medium text-slate-200">{task.fileName}</p>
|
||||
<p className="mt-0.5 truncate text-xs text-slate-500">上传至: {task.destination}</p>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<p className="truncate text-sm font-medium text-slate-200">{task.fileName}</p>
|
||||
<div className="shrink-0">
|
||||
{task.status === 'completed' ? (
|
||||
<CheckCircle2 className="h-[18px] w-[18px] text-emerald-400" />
|
||||
) : task.status === 'error' ? (
|
||||
<TriangleAlert className="h-[18px] w-[18px] text-rose-400" />
|
||||
) : (
|
||||
<FileUp className="h-[18px] w-[18px] animate-pulse text-[#78A1FF]" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-1 flex flex-wrap items-center gap-2 text-xs">
|
||||
<span className={cn('rounded-full px-2 py-1 font-medium', getFileTypeTheme(task.type).badgeClassName)}>
|
||||
{task.typeLabel}
|
||||
</span>
|
||||
<span className="truncate text-slate-500">上传至: {task.destination}</span>
|
||||
</div>
|
||||
{task.noticeMessage && (
|
||||
<p className="mt-2 truncate text-xs text-amber-300">{task.noticeMessage}</p>
|
||||
)}
|
||||
|
||||
@@ -17,8 +17,10 @@ import {
|
||||
import { shouldLoadAvatarWithAuth } from '@/src/components/layout/account-utils';
|
||||
import { Button } from '@/src/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/src/components/ui/card';
|
||||
import { FileTypeIcon, getFileTypeTheme } from '@/src/components/ui/FileTypeIcon';
|
||||
import { apiDownload, apiRequest } from '@/src/lib/api';
|
||||
import { readCachedValue, writeCachedValue } from '@/src/lib/cache';
|
||||
import { resolveStoredFileType } from '@/src/lib/file-type';
|
||||
import { getOverviewCacheKey } from '@/src/lib/page-cache';
|
||||
import { clearPostLoginPending, hasPostLoginPending, readStoredSession } from '@/src/lib/session';
|
||||
import type { FileMetadata, PageResponse, UserProfile } from '@/src/lib/types';
|
||||
@@ -249,22 +251,39 @@ export default function Overview() {
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
{recentFiles.slice(0, 3).map((file, index) => (
|
||||
<div
|
||||
key={`${file.id}-${index}`}
|
||||
className="flex items-center justify-between p-3 rounded-xl hover:bg-white/5 transition-colors cursor-pointer group"
|
||||
onClick={() => navigate('/files')}
|
||||
>
|
||||
<div className="flex items-center gap-4 overflow-hidden">
|
||||
<div className="w-10 h-10 rounded-xl bg-[#336EFF]/10 flex items-center justify-center shrink-0 group-hover:bg-[#336EFF]/20 transition-colors">
|
||||
<FileText className="w-5 h-5 text-[#336EFF]" />
|
||||
(() => {
|
||||
const fileType = resolveStoredFileType({
|
||||
filename: file.filename,
|
||||
contentType: file.contentType,
|
||||
directory: file.directory,
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`${file.id}-${index}`}
|
||||
className="flex items-center justify-between rounded-xl p-3 transition-colors cursor-pointer group hover:bg-white/5"
|
||||
onClick={() => navigate('/files')}
|
||||
>
|
||||
<div className="flex items-center gap-4 overflow-hidden">
|
||||
<FileTypeIcon type={fileType.kind} size="sm" className="group-hover:scale-[1.03] transition-transform duration-200" />
|
||||
<div className="min-w-0 truncate">
|
||||
<p className="truncate text-sm font-medium text-white">{file.filename}</p>
|
||||
<div className="mt-1 flex flex-wrap items-center gap-2">
|
||||
<span
|
||||
className={`inline-flex rounded-full px-2 py-1 text-[11px] font-medium ${getFileTypeTheme(fileType.kind).badgeClassName}`}
|
||||
>
|
||||
{fileType.label}
|
||||
</span>
|
||||
<p className="text-xs text-slate-400">{formatRecentTime(file.createdAt)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span className="ml-4 shrink-0 text-xs font-mono text-slate-500">
|
||||
{file.directory ? '文件夹' : formatFileSize(file.size)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="truncate">
|
||||
<p className="text-sm font-medium text-white truncate">{file.filename}</p>
|
||||
<p className="text-xs text-slate-400 mt-0.5">{formatRecentTime(file.createdAt)}</p>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-xs text-slate-500 font-mono shrink-0 ml-4">{formatFileSize(file.size)}</span>
|
||||
</div>
|
||||
);
|
||||
})()
|
||||
))}
|
||||
{recentFiles.length === 0 ? (
|
||||
<div className="p-3 rounded-xl border border-dashed border-white/10 text-sm text-slate-500">
|
||||
|
||||
@@ -24,6 +24,70 @@ test('createUploadTask uses current path as upload destination', () => {
|
||||
assert.equal(task.speed, '等待上传...');
|
||||
});
|
||||
|
||||
test('createUploadTask classifies common file families beyond images and office basics', () => {
|
||||
const spreadsheetTask = createUploadTask(
|
||||
new File(['sheet'], '预算.xlsx', {type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'}),
|
||||
[],
|
||||
'task-sheet',
|
||||
);
|
||||
const presentationTask = createUploadTask(
|
||||
new File(['slides'], '发布会.pptx', {type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation'}),
|
||||
[],
|
||||
'task-slides',
|
||||
);
|
||||
const archiveTask = createUploadTask(
|
||||
new File(['archive'], '素材包.zip', {type: 'application/zip'}),
|
||||
[],
|
||||
'task-archive',
|
||||
);
|
||||
const videoTask = createUploadTask(
|
||||
new File(['video'], '演示.mp4', {type: 'video/mp4'}),
|
||||
[],
|
||||
'task-video',
|
||||
);
|
||||
const audioTask = createUploadTask(
|
||||
new File(['audio'], '片头.mp3', {type: 'audio/mpeg'}),
|
||||
[],
|
||||
'task-audio',
|
||||
);
|
||||
const designTask = createUploadTask(
|
||||
new File(['design'], '首页.fig', {type: 'application/octet-stream'}),
|
||||
[],
|
||||
'task-design',
|
||||
);
|
||||
const fontTask = createUploadTask(
|
||||
new File(['font'], 'Brand.woff2', {type: 'font/woff2'}),
|
||||
[],
|
||||
'task-font',
|
||||
);
|
||||
const appTask = createUploadTask(
|
||||
new File(['binary'], 'installer.exe', {type: 'application/vnd.microsoft.portable-executable'}),
|
||||
[],
|
||||
'task-app',
|
||||
);
|
||||
const ebookTask = createUploadTask(
|
||||
new File(['ebook'], '小说.epub', {type: 'application/epub+zip'}),
|
||||
[],
|
||||
'task-ebook',
|
||||
);
|
||||
const codeTask = createUploadTask(
|
||||
new File(['json'], 'manifest.json', {type: 'application/json'}),
|
||||
[],
|
||||
'task-code',
|
||||
);
|
||||
|
||||
assert.equal(spreadsheetTask.type, 'spreadsheet');
|
||||
assert.equal(presentationTask.type, 'presentation');
|
||||
assert.equal(archiveTask.type, 'archive');
|
||||
assert.equal(videoTask.type, 'video');
|
||||
assert.equal(audioTask.type, 'audio');
|
||||
assert.equal(designTask.type, 'design');
|
||||
assert.equal(fontTask.type, 'font');
|
||||
assert.equal(appTask.type, 'application');
|
||||
assert.equal(ebookTask.type, 'ebook');
|
||||
assert.equal(codeTask.type, 'code');
|
||||
});
|
||||
|
||||
test('formatTransferSpeed chooses a readable unit', () => {
|
||||
assert.equal(formatTransferSpeed(800), '800 B/s');
|
||||
assert.equal(formatTransferSpeed(2048), '2.0 KB/s');
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { getNextAvailableName } from './files-state';
|
||||
import { resolveFileType, type FileTypeKind } from '@/src/lib/file-type';
|
||||
|
||||
export type UploadTaskStatus = 'uploading' | 'completed' | 'error';
|
||||
|
||||
@@ -9,7 +10,8 @@ export interface UploadTask {
|
||||
speed: string;
|
||||
destination: string;
|
||||
status: UploadTaskStatus;
|
||||
type: string;
|
||||
type: FileTypeKind;
|
||||
typeLabel: string;
|
||||
errorMessage?: string;
|
||||
noticeMessage?: string;
|
||||
}
|
||||
@@ -28,22 +30,10 @@ export interface PendingUploadEntry {
|
||||
}
|
||||
|
||||
function getUploadType(file: File) {
|
||||
const extension = file.name.includes('.') ? file.name.split('.').pop()?.toLowerCase() : '';
|
||||
|
||||
if (file.type.startsWith('image/')) {
|
||||
return 'image';
|
||||
}
|
||||
if (file.type.includes('pdf') || extension === 'pdf') {
|
||||
return 'pdf';
|
||||
}
|
||||
if (extension === 'doc' || extension === 'docx') {
|
||||
return 'word';
|
||||
}
|
||||
if (extension === 'xls' || extension === 'xlsx' || extension === 'csv') {
|
||||
return 'excel';
|
||||
}
|
||||
|
||||
return extension || 'document';
|
||||
return resolveFileType({
|
||||
fileName: file.name,
|
||||
contentType: file.type,
|
||||
});
|
||||
}
|
||||
|
||||
function createTaskId() {
|
||||
@@ -174,6 +164,8 @@ export function createUploadTask(
|
||||
taskId: string = createTaskId(),
|
||||
noticeMessage?: string,
|
||||
): UploadTask {
|
||||
const fileType = getUploadType(file);
|
||||
|
||||
return {
|
||||
id: taskId,
|
||||
fileName: file.name,
|
||||
@@ -181,7 +173,8 @@ export function createUploadTask(
|
||||
speed: '等待上传...',
|
||||
destination: getUploadDestination(pathParts),
|
||||
status: 'uploading',
|
||||
type: getUploadType(file),
|
||||
type: fileType.kind,
|
||||
typeLabel: fileType.label,
|
||||
noticeMessage,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user