update resources

This commit is contained in:
2025-03-27 19:41:28 +08:00
parent d649666379
commit 43d612fff3
35 changed files with 1862 additions and 764 deletions

View File

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

View File

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

View File

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

View 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>
);
};

View File

@@ -3,3 +3,5 @@ export * from './tools/to-file';
export * from './utils/upload';
export * from './utils/upload-chunk';
export * from './utils/create-directory';

View File

@@ -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) => {

View File

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

View 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;
};
};

View File

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

View File

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