update resources
This commit is contained in:
@@ -2,4 +2,6 @@ export { KeyParse, keysTips } from './pages/file/modules/key-parse';
|
||||
export { PermissionManager } from './pages/file/modules/PermissionManager.tsx';
|
||||
export { PermissionModal, usePermissionModal } from './pages/file/modules/PermissionModal.tsx';
|
||||
export { iText } from './i-text/index.ts';
|
||||
export * from './pages/upload/app';
|
||||
|
||||
export { uploadFiles, uploadFileChunked, getDirectoryAndName, toFile, createDirectory } from './pages/upload/app';
|
||||
export { DialogDirectory, DialogDeleteDirectory } from './pages/upload/DialogDirectory';
|
||||
|
||||
@@ -1,34 +1,39 @@
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useResourceStore } from '../store/resource';
|
||||
import { Box, Button, Typography, ButtonGroup } from '@mui/material';
|
||||
import { FileText, Table, Grid } from 'lucide-react';
|
||||
import { Box, Button, Typography, ButtonGroup, Tooltip } from '@mui/material';
|
||||
import { FileText, Table, Grid, Trash, Upload, FolderPlus, RefreshCw } from 'lucide-react';
|
||||
import { FileTable } from './list/FileTable';
|
||||
import { FileCard } from './list/FileCard';
|
||||
import { PrefixRedirect } from './modules/PrefixRedirect';
|
||||
import { UploadButton } from '../upload';
|
||||
import { FileDrawer } from './draw/FileDrawer';
|
||||
import { useResourceFileStore } from '../store/resource-file';
|
||||
import { IconButtonItem } from '@kevisual/components/button/index.tsx';
|
||||
import { IconButtonItem, IconButton } from '@kevisual/components/button/index.tsx';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { DialogDeleteDirectory, DialogDirectory } from '../upload/DialogDirectory';
|
||||
export const FileApp = () => {
|
||||
const { getList, prefix, setListType, listType } = useResourceStore();
|
||||
const { getList, prefix, setListType, listType, onOpenPrefix } = useResourceStore();
|
||||
const { getStatFile, prefix: statPrefix, openDrawer } = useResourceFileStore();
|
||||
useEffect(() => {
|
||||
getList();
|
||||
}, []);
|
||||
const directory = useMemo(() => {
|
||||
const _prefix = prefix.split('/');
|
||||
let dir = _prefix.slice(2).join('/');
|
||||
if (dir.endsWith('/')) {
|
||||
dir = dir.slice(0, -1);
|
||||
}
|
||||
return dir;
|
||||
}, [prefix]);
|
||||
|
||||
useEffect(() => {
|
||||
if (statPrefix && openDrawer) {
|
||||
getStatFile();
|
||||
}
|
||||
}, [statPrefix, openDrawer]);
|
||||
const handleUpload = (res: any) => {
|
||||
|
||||
const paths = ['root', ...prefix.split('/').filter((item) => item)];
|
||||
const [_mockUsername, appKey, version, ...directory] = paths;
|
||||
const directoryPath = directory.join('/');
|
||||
const [dialogDeleteDirectory, setDialogDeleteDirectory] = useState(false);
|
||||
const [dialogDirectory, setDialogDirectory] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const onDirectoryClick = (prefix: string) => {
|
||||
onOpenPrefix(prefix);
|
||||
};
|
||||
const onUloadFinish = (res: any) => {
|
||||
getList();
|
||||
};
|
||||
return (
|
||||
@@ -55,7 +60,75 @@ export const FileApp = () => {
|
||||
</div>
|
||||
<Box className='flex items-center gap-2 mb-4'>
|
||||
<PrefixRedirect />
|
||||
<UploadButton prefix={directory} onUpload={handleUpload} />
|
||||
<Tooltip title={t('refresh')} placement='bottom'>
|
||||
<IconButton
|
||||
color='primary'
|
||||
onClick={() => {
|
||||
onDirectoryClick(prefix);
|
||||
}}>
|
||||
<RefreshCw />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
{true && (
|
||||
<>
|
||||
<Tooltip title={t('create_directory')} placement='bottom'>
|
||||
<IconButton color='primary' onClick={() => setDialogDirectory(true)}>
|
||||
<FolderPlus />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<DialogDirectory
|
||||
open={dialogDirectory}
|
||||
onClose={() => setDialogDirectory(false)}
|
||||
onSuccess={(newPrefix) => {
|
||||
const currentPath = [appKey, version, newPrefix].filter(Boolean).join('/');
|
||||
onDirectoryClick(currentPath + '/');
|
||||
setDialogDirectory(false);
|
||||
}}
|
||||
prefix={directoryPath}
|
||||
opts={{
|
||||
appKey,
|
||||
version,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{true && (
|
||||
<>
|
||||
<Tooltip title={t('uploadDirectory')} placement='bottom'>
|
||||
<IconButton color='primary'>
|
||||
<UploadButton onlyIcon uploadDirectory icon={<Upload />} directory={directoryPath} onUpload={onUloadFinish} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={t('upload')} placement='bottom'>
|
||||
<IconButton color='primary'>
|
||||
<UploadButton onlyIcon directory={directoryPath} onUpload={onUloadFinish} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
{directoryPath !== '' && (
|
||||
<>
|
||||
<Tooltip title={t('deleteDirectory')} placement='bottom'>
|
||||
<IconButton
|
||||
color='primary'
|
||||
onClick={() => {
|
||||
setDialogDeleteDirectory(true);
|
||||
}}>
|
||||
<Trash />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<DialogDeleteDirectory
|
||||
open={dialogDeleteDirectory}
|
||||
onClose={() => setDialogDeleteDirectory(false)}
|
||||
onSuccess={(prefix) => {
|
||||
const newPrefix = prefix.split('/').slice(0, -1).join('/');
|
||||
setDialogDeleteDirectory(false);
|
||||
onDirectoryClick(newPrefix + '/');
|
||||
}}
|
||||
prefix={appKey + '/' + version + '/' + directoryPath}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<ButtonGroup className='ml-auto' variant='contained' color='primary' sx={{ color: 'white' }}>
|
||||
<Button
|
||||
variant={listType === 'table' ? 'contained' : 'outlined'}
|
||||
|
||||
@@ -16,7 +16,20 @@ interface ResourceFileStore {
|
||||
*/
|
||||
getStatFile: () => Promise<any>;
|
||||
updateMeta: (metadata: any) => Promise<any>;
|
||||
/**
|
||||
* 删除文件
|
||||
* @param resource 文件
|
||||
* @param opts 选项
|
||||
* @returns
|
||||
*/
|
||||
deleteFile: (resource: Resource, opts?: { onSuccess?: (res: any) => void }) => Promise<void>;
|
||||
/**
|
||||
* 删除目录
|
||||
* @param prefix 目录
|
||||
* @param opts 选项
|
||||
* @returns
|
||||
*/
|
||||
deleteDirectory: (prefix: string, opts?: { onSuccess?: (res: any) => void }) => Promise<void>;
|
||||
once: ((data: any) => any) | null;
|
||||
setOnce: (data: any) => void;
|
||||
}
|
||||
@@ -81,6 +94,30 @@ export const useResourceFileStore = create<ResourceFileStore>((set, get) => ({
|
||||
toast.error(res.message || 'Request failed');
|
||||
}
|
||||
},
|
||||
deleteDirectory: async (prefix: string, opts?: { onSuccess?: (res: any) => void }) => {
|
||||
if (!prefix) {
|
||||
toast.error('Directory name is required');
|
||||
return;
|
||||
}
|
||||
if (prefix.endsWith('/')) {
|
||||
toast.error('Directory name cannot end with a slash');
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await query.post({
|
||||
path: 'file',
|
||||
key: 'delete-all',
|
||||
data: {
|
||||
directory: prefix,
|
||||
},
|
||||
});
|
||||
if (res.code === 200) {
|
||||
toast.success('Delete directory success');
|
||||
opts?.onSuccess?.(res);
|
||||
} else {
|
||||
toast.error(res.message || 'Request failed');
|
||||
}
|
||||
},
|
||||
once: null,
|
||||
setOnce: (data: any) => set({ once: data }),
|
||||
}));
|
||||
|
||||
113
packages/resources/src/pages/upload/DialogDirectory.tsx
Normal file
113
packages/resources/src/pages/upload/DialogDirectory.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import { Button, Dialog, DialogActions, DialogContent, DialogTitle, TextField } from '@mui/material';
|
||||
import { useTheme } from '@kevisual/components/theme/index.js';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { createDirectory } from './utils/create-directory';
|
||||
import { ConvertOpts } from './utils/upload-chunk';
|
||||
import { useResourceFileStore } from '../store/resource-file';
|
||||
type DialogDirectoryProps = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSuccess: (directory: string) => void;
|
||||
prefix?: string;
|
||||
opts?: ConvertOpts;
|
||||
};
|
||||
|
||||
/**
|
||||
* 创建目录
|
||||
* @param props
|
||||
* @returns
|
||||
*/
|
||||
export const DialogDirectory = (props: DialogDirectoryProps) => {
|
||||
const { open, onClose, onSuccess, prefix, opts } = props;
|
||||
const theme = useTheme();
|
||||
const [directory, setDirectory] = useState('');
|
||||
const defaultProps = theme.components?.MuiTextField?.defaultProps as any;
|
||||
const { t } = useTranslation();
|
||||
useEffect(() => {
|
||||
setDirectory('');
|
||||
}, [open]);
|
||||
const onClick = async () => {
|
||||
if (!directory) {
|
||||
toast.error(t('directory_name_required'));
|
||||
return;
|
||||
}
|
||||
if (directory.startsWith('.') || directory.endsWith('.')) {
|
||||
toast.error(t('directory_name_invalid'));
|
||||
return;
|
||||
}
|
||||
if (directory.startsWith('/') || directory.endsWith('/')) {
|
||||
toast.error(t('directory_name_invalid'));
|
||||
return;
|
||||
}
|
||||
if (directory.includes('//')) {
|
||||
toast.error(t('directory_name_invalid'));
|
||||
return;
|
||||
}
|
||||
const res = await createDirectory(prefix ? `${prefix}/${directory}` : directory, opts);
|
||||
if (res?.code === 200) {
|
||||
if (onSuccess) {
|
||||
onSuccess(prefix ? `${prefix}/${directory}` : directory);
|
||||
} else {
|
||||
toast.success(t('create_directory_success'));
|
||||
onClose();
|
||||
}
|
||||
} else {
|
||||
toast.error(res.message);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose}>
|
||||
<DialogTitle>{t('create_directory')}</DialogTitle>
|
||||
<DialogContent>
|
||||
<div className='min-w-[400px] py-4'>
|
||||
<TextField {...defaultProps} value={directory} onChange={(e) => setDirectory(e.target.value)} label='目录名' />
|
||||
</div>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose}>{t('Cancel')}</Button>
|
||||
<Button onClick={onClick}>{t('Submit')}</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
type DialogDeleteDirectoryProps = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSuccess: (directory: string) => void;
|
||||
prefix?: string;
|
||||
};
|
||||
export const DialogDeleteDirectory = (props: DialogDeleteDirectoryProps) => {
|
||||
const { open, onClose, onSuccess, prefix } = props;
|
||||
const { t } = useTranslation();
|
||||
const { deleteDirectory } = useResourceFileStore();
|
||||
const onClick = async () => {
|
||||
if (!prefix) {
|
||||
toast.error(t('directory_name_required'));
|
||||
return;
|
||||
}
|
||||
await deleteDirectory(prefix!, {
|
||||
onSuccess: () => {
|
||||
if (onSuccess) {
|
||||
onSuccess(prefix!);
|
||||
} else {
|
||||
onClose();
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose}>
|
||||
<DialogTitle>{t('delete_directory')}</DialogTitle>
|
||||
<DialogContent>
|
||||
<div className='min-w-[400px]'>确认删除目录: {`${prefix}/`}</div>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose}>{t('Cancel')}</Button>
|
||||
<Button onClick={onClick}>{t('Submit')}</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -3,3 +3,5 @@ export * from './tools/to-file';
|
||||
export * from './utils/upload';
|
||||
|
||||
export * from './utils/upload-chunk';
|
||||
|
||||
export * from './utils/create-directory';
|
||||
@@ -4,7 +4,7 @@ import { uploadFiles } from './utils/upload';
|
||||
import { FileText, CloudUpload as UploadIcon } from 'lucide-react';
|
||||
import { uploadFileChunked } from './utils/upload-chunk';
|
||||
import { filterFiles } from './utils/filter-files';
|
||||
|
||||
import { IconButton } from '@kevisual/components/button/index.tsx';
|
||||
type UploadButtonProps = {
|
||||
/**
|
||||
* 前缀
|
||||
@@ -77,18 +77,7 @@ export const UploadButton = (props: UploadButtonProps) => {
|
||||
if (onlyIcon) {
|
||||
return uploadCom;
|
||||
}
|
||||
return (
|
||||
<Box>
|
||||
<Button
|
||||
color='primary'
|
||||
sx={{
|
||||
minWidth: 'unset',
|
||||
padding: '2px',
|
||||
}}>
|
||||
{uploadCom}
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
return <IconButton color='primary'>{uploadCom}</IconButton>;
|
||||
};
|
||||
export const Upload = ({ uploadDirectory = false }: { uploadDirectory?: boolean }) => {
|
||||
const onDrop = async (acceptedFiles) => {
|
||||
|
||||
@@ -32,6 +32,11 @@ const getFileType = (extension: string) => {
|
||||
const checkIsBase64 = (content: string) => {
|
||||
return content.startsWith('data:');
|
||||
};
|
||||
/**
|
||||
* 获取文件的目录和文件名
|
||||
* @param filename 文件名
|
||||
* @returns 目录和文件名
|
||||
*/
|
||||
export const getDirectoryAndName = (filename: string) => {
|
||||
if (!filename) {
|
||||
return null;
|
||||
@@ -87,3 +92,14 @@ export const toFile = (content: string, filename: string) => {
|
||||
return new File([blob], filename, { type });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 把字符串转为文本文件
|
||||
* @param content 字符串
|
||||
* @param filename 文件名
|
||||
* @returns 文件流
|
||||
*/
|
||||
export const toTextFile = (content: string = 'keep directory exist', filename: string = 'keep.txt') => {
|
||||
const file = toFile(content, filename);
|
||||
return file;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { toTextFile } from '../app';
|
||||
import { ConvertOpts, uploadFileChunked } from './upload-chunk';
|
||||
/**
|
||||
* 对创建的directory的路径进行解析,
|
||||
* 如果是 nameA/nameB/nameC 则创建 nameA/nameB/nameC
|
||||
*
|
||||
* 不能以.开头,不能以.结尾,不能以/开头,不能以/结尾。
|
||||
* @param directory
|
||||
* @param opts
|
||||
* @returns
|
||||
*/
|
||||
export const createDirectory = async (directory: string = '', opts?: ConvertOpts) => {
|
||||
const directoryPath = directory || opts?.directory;
|
||||
const error = '目录名不能以.开头,不能以.结尾,不能以/开头,不能以/结尾,不能包含//';
|
||||
const errorMsg = () => {
|
||||
return {
|
||||
code: 400,
|
||||
message: error,
|
||||
success: false,
|
||||
};
|
||||
};
|
||||
if (directoryPath) {
|
||||
if (directoryPath.startsWith('.')) {
|
||||
return errorMsg();
|
||||
}
|
||||
if (directoryPath.endsWith('.')) {
|
||||
return errorMsg();
|
||||
}
|
||||
if (directoryPath.startsWith('/')) {
|
||||
return errorMsg();
|
||||
}
|
||||
if (directoryPath.endsWith('/')) {
|
||||
return errorMsg();
|
||||
}
|
||||
if (directoryPath.includes('//')) {
|
||||
return errorMsg();
|
||||
}
|
||||
}
|
||||
const res = await uploadFileChunked(toTextFile('keep directory exist', 'keep.txt'), {
|
||||
directory,
|
||||
...opts,
|
||||
});
|
||||
return res as {
|
||||
code: number;
|
||||
message?: string;
|
||||
success?: boolean;
|
||||
data?: any;
|
||||
[key: string]: any;
|
||||
};
|
||||
};
|
||||
@@ -4,7 +4,7 @@ import { toast } from 'react-toastify';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { toastLogin } from '@kevisual/resources/pages/message/ToastLogin';
|
||||
|
||||
type ConvertOpts = {
|
||||
export type ConvertOpts = {
|
||||
appKey?: string;
|
||||
version?: string;
|
||||
username?: string;
|
||||
|
||||
@@ -11,6 +11,19 @@ type ConvertOpts = {
|
||||
};
|
||||
export const uploadFiles = async (files: File[], opts: ConvertOpts) => {
|
||||
const { directory, appKey, version, username } = opts;
|
||||
const length = files.length;
|
||||
const maxSize = 10 * 1024 * 1024; // 10MB
|
||||
const totalSize = files.reduce((acc, file) => acc + file.size, 0);
|
||||
if (totalSize > maxSize) {
|
||||
toast.error('有文件大小不能超过10MB');
|
||||
return;
|
||||
}
|
||||
const maxCount = 10;
|
||||
if (length > maxCount) {
|
||||
toast.error(`最多只能上传${maxCount}个文件`);
|
||||
return;
|
||||
}
|
||||
toast.info(`上传中,共${length}个文件`);
|
||||
return new Promise((resolve, reject) => {
|
||||
const formData = new FormData();
|
||||
const webkitRelativePath = files[0]?.webkitRelativePath;
|
||||
@@ -43,10 +56,7 @@ export const uploadFiles = async (files: File[], opts: ConvertOpts) => {
|
||||
return;
|
||||
}
|
||||
const taskId = nanoid();
|
||||
// 49.232.155.236:11015
|
||||
// const eventSource = new EventSource('https://kevisual.silkyai.cn/api/s1/events?taskId=' + taskId);
|
||||
const eventSource = new EventSource('/api/s1/events?taskId=' + taskId);
|
||||
// const eventSource = new EventSource('http://49.232.155.236:11015/api/s1/events?taskId=' + taskId);
|
||||
const load = toast.loading('上传中...');
|
||||
NProgress.start();
|
||||
eventSource.onopen = async function (event) {
|
||||
|
||||
Reference in New Issue
Block a user