feat: Add the ability to obtain direct file links

This commit is contained in:
shiyu
2025-08-27 12:23:26 +08:00
parent 202c2ed5af
commit d5d597a582
10 changed files with 322 additions and 64 deletions

View File

@@ -4,7 +4,7 @@ import type { VfsEntry } from '../../../api/client';
import { getAppsForEntry, getDefaultAppForEntry } from '../../../apps/registry';
import {
FolderFilled, AppstoreOutlined, AppstoreAddOutlined, DownloadOutlined,
EditOutlined, DeleteOutlined, InfoCircleOutlined, UploadOutlined, PlusOutlined, ShareAltOutlined
EditOutlined, DeleteOutlined, InfoCircleOutlined, UploadOutlined, PlusOutlined, ShareAltOutlined, LinkOutlined
} from '@ant-design/icons';
interface ContextMenuProps {
@@ -25,6 +25,7 @@ interface ContextMenuProps {
onUpload: () => void;
onCreateDir: () => void;
onShare: (entries: VfsEntry[]) => void;
onGetDirectLink: (entry: VfsEntry) => void;
}
export const ContextMenu: React.FC<ContextMenuProps> = (props) => {
@@ -86,6 +87,13 @@ export const ContextMenu: React.FC<ContextMenuProps> = (props) => {
icon: <ShareAltOutlined />,
onClick: () => actions.onShare(targetEntries),
},
{
key: 'directLink',
label: '获取直链',
icon: <LinkOutlined />,
disabled: targetEntries.length !== 1 || targetEntries[0].is_dir,
onClick: () => actions.onGetDirectLink(targetEntries[0]),
},
{
key: 'download',
label: '下载',

View File

@@ -7,7 +7,6 @@ interface HeaderProps {
navKey: string;
path: string;
loading: boolean;
uploading: boolean;
viewMode: ViewMode;
onGoUp: () => void;
onNavigate: (path: string) => void;
@@ -20,7 +19,6 @@ interface HeaderProps {
export const Header: React.FC<HeaderProps> = ({
path,
loading,
uploading,
viewMode,
onGoUp,
onNavigate,
@@ -101,7 +99,7 @@ export const Header: React.FC<HeaderProps> = ({
<Space size={8} wrap>
<Button size="small" icon={<ReloadOutlined />} onClick={onRefresh} loading={loading}></Button>
<Button size="small" icon={<PlusOutlined />} onClick={onCreateDir}></Button>
<Button size="small" icon={<UploadOutlined />} loading={uploading} onClick={onUpload}></Button>
<Button size="small" icon={<UploadOutlined />} onClick={onUpload}></Button>
<Segmented
size="small"
value={viewMode}

View File

@@ -0,0 +1,79 @@
import { memo, useState, useEffect } from 'react';
import { Modal, Radio, message, Button, Typography, Input } from 'antd';
import { CopyOutlined } from '@ant-design/icons';
import type { VfsEntry } from '../../../../api/client';
import { vfsApi } from '../../../../api/client';
interface DirectLinkModalProps {
entry: VfsEntry | null;
path: string;
open: boolean;
onCancel: () => void;
}
export const DirectLinkModal = memo(function DirectLinkModal({ entry, path, open, onCancel }: DirectLinkModalProps) {
const [loading, setLoading] = useState(false);
const [expiresIn, setExpiresIn] = useState(3600);
const [link, setLink] = useState('');
useEffect(() => {
if (open && entry) {
setLink('');
generateLink();
}
}, [open, entry, expiresIn]);
const generateLink = async () => {
if (!entry) return;
setLoading(true);
try {
const fullPath = (path === '/' ? '' : path) + '/' + entry.name;
const res = await vfsApi.getTempLinkToken(fullPath, expiresIn);
const tempLink = `${window.location.origin}/api/fs/public/${res.token}`;
setLink(tempLink);
} catch (e: any) {
message.error(e.message || '生成链接失败');
} finally {
setLoading(false);
}
};
const handleCopy = (text: string) => {
navigator.clipboard.writeText(text);
message.success('已复制到剪贴板');
};
const handleExpiresChange = (e: any) => {
setExpiresIn(e.target.value);
};
return (
<Modal
title="获取直链"
open={open}
onCancel={onCancel}
footer={[
<Button key="back" onClick={onCancel}>
</Button>,
]}
>
<Typography.Paragraph>
<strong>{entry?.name}</strong> 访
</Typography.Paragraph>
<Radio.Group value={expiresIn} onChange={handleExpiresChange} style={{ marginBottom: 16 }}>
<Radio.Button value={3600}>1 </Radio.Button>
<Radio.Button value={86400}>1 </Radio.Button>
<Radio.Button value={604800}>7 </Radio.Button>
<Radio.Button value={0}></Radio.Button>
</Radio.Group>
<div style={{ display: 'flex', gap: 8 }}>
<Input readOnly value={link} disabled={loading} placeholder={loading ? "正在生成链接..." : "链接将显示在这里"} />
<Button icon={<CopyOutlined />} onClick={() => handleCopy(link)} disabled={!link || loading}>
</Button>
</div>
</Modal>
);
});

View File

@@ -0,0 +1,92 @@
import React, { useEffect } from 'react';
import { Modal, Button, List, Progress, Typography, message, Flex } from 'antd';
import { CopyOutlined, CheckCircleFilled, CloseCircleFilled } from '@ant-design/icons';
import type { UploadFile } from '../../hooks/useUploader';
interface UploadModalProps {
visible: boolean;
files: UploadFile[];
onClose: () => void;
onStartUpload: () => void;
}
const UploadModal: React.FC<UploadModalProps> = ({ visible, files, onClose, onStartUpload }) => {
const allSuccess = files.every(f => f.status === 'success');
useEffect(() => {
if (visible && files.length > 0 && files.every(f => f.status === 'pending')) {
onStartUpload();
}
}, [visible, files, onStartUpload]);
const handleCopy = (text: string) => {
navigator.clipboard.writeText(text);
message.success('链接已复制到剪贴板');
};
const renderStatus = (file: UploadFile) => {
switch (file.status) {
case 'uploading':
return <Progress percent={Math.round(file.progress)} size="small" />;
case 'success':
return (
<Flex align="center" gap={8}>
<CheckCircleFilled style={{ color: '#52c41a' }} />
<Typography.Text type="secondary" style={{ verticalAlign: 'middle' }}></Typography.Text>
<Button icon={<CopyOutlined />} size="small" onClick={() => handleCopy(file.permanentLink!)} type="text" />
</Flex>
);
case 'error':
return (
<Flex align="center" gap={8}>
<CloseCircleFilled style={{ color: '#ff4d4f' }} />
<Typography.Text type="danger" title={file.error}></Typography.Text>
</Flex>
);
default:
return <Typography.Text type="secondary"></Typography.Text>;
}
};
return (
<Modal
open={visible}
title="上传文件"
width={600}
onCancel={onClose}
footer={[
<Button key="close" onClick={onClose} disabled={!allSuccess && files.some(f => f.status === 'uploading')}>
{allSuccess ? '关闭' : '完成'}
</Button>,
]}
>
<List
dataSource={files}
itemLayout="horizontal"
renderItem={file => (
<List.Item
style={{
padding: '12px 8px',
borderRadius: 8,
transition: 'background-color 0.2s',
}}
onMouseEnter={(e) => { e.currentTarget.style.backgroundColor = '#f0f0f0'; }}
onMouseLeave={(e) => { e.currentTarget.style.backgroundColor = 'transparent'; }}
>
<Flex justify="space-between" align="center" style={{ width: '100%' }}>
<Typography.Text ellipsis={{ tooltip: file.file.name }} style={{ maxWidth: '60%' }}>
{file.file.name}
</Typography.Text>
<div style={{ minWidth: 180, textAlign: 'right', flexShrink: 0 }}>
{renderStatus(file)}
</div>
</Flex>
</List.Item>
)}
/>
</Modal>
);
};
export default UploadModal;