mirror of
https://github.com/DrizzleTime/Foxel.git
synced 2026-07-07 07:11:42 +08:00
feat: Add the ability to obtain direct file links
This commit is contained in:
@@ -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: '下载',
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user