修改前端图标以及修改oss存储桶位置

This commit is contained in:
yoyuzh
2026-03-24 11:18:51 +08:00
parent b9ab1a7640
commit 00f902f475
10 changed files with 884 additions and 86 deletions

View File

@@ -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>
)}

View File

@@ -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">

View File

@@ -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');

View File

@@ -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,
};
}