feat: 去掉antd
This commit is contained in:
@@ -1,12 +1,14 @@
|
||||
import { useResourceStore } from '@kevisual/resources/pages/store/resource';
|
||||
import { useResourceFileStore } from '@kevisual/resources/pages/store/resource-file';
|
||||
import { Box, Divider, Drawer, Tab, Tabs } from '@mui/material';
|
||||
import { Box, Button, Divider, Drawer, Tab, Tabs } from '@mui/material';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { QuickValues, QuickTabs } from './QuickTabs';
|
||||
import { Delete, Trash } from 'lucide-react';
|
||||
import { InitProvider } from '../../App';
|
||||
|
||||
export const FileDrawer = () => {
|
||||
const { prefix } = useResourceStore();
|
||||
const { resource, openDrawer, setOpenDrawer } = useResourceFileStore();
|
||||
const { prefix, getList } = useResourceStore();
|
||||
const { resource, openDrawer, setOpenDrawer, deleteFile } = useResourceFileStore();
|
||||
const [tab, setTab] = useState<string>(QuickValues[0]);
|
||||
const quickCom = useMemo(() => {
|
||||
return QuickTabs.find((item) => item.value === tab)?.component;
|
||||
@@ -31,10 +33,26 @@ export const FileDrawer = () => {
|
||||
style={{
|
||||
zIndex: 1000,
|
||||
}}>
|
||||
<div className='p-4 w-[400px] max-w-[90%] overflow-hidden h-full sm:w-[600px]'>
|
||||
<div className='p-4 overflow-hidden h-full sm:w-[600px]'>
|
||||
<div style={{ height: '140px' }}>
|
||||
<h2 className='text-2xl font-bold truncate py-2 pb-6 '>
|
||||
{resource?.name ? resource.name.replace(prefix, '') : resource?.prefix?.replace(prefix, '')}
|
||||
<h2 className='text-2xl font-bold py-2 pb-6 flex '>
|
||||
<div className='grow truncate'>{resource?.name ? resource.name.replace(prefix, '') : resource?.prefix?.replace(prefix, '')}</div>
|
||||
<Button
|
||||
sx={{ ml: 2 }}
|
||||
color='primary'
|
||||
size='small'
|
||||
onClick={() => {
|
||||
if (resource) {
|
||||
deleteFile(resource, {
|
||||
onSuccess: () => {
|
||||
getList();
|
||||
setOpenDrawer(false);
|
||||
},
|
||||
});
|
||||
}
|
||||
}}>
|
||||
<Trash />
|
||||
</Button>
|
||||
</h2>
|
||||
<Divider />
|
||||
<Box sx={{ borderBottom: 1, mt: 2, borderColor: 'divider' }}>
|
||||
@@ -58,3 +76,11 @@ export const FileDrawer = () => {
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const FileDrawerApp = () => {
|
||||
return (
|
||||
<InitProvider>
|
||||
<FileDrawer />
|
||||
</InitProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { useResourceFileStore } from '@kevisual/resources/pages/store/resource-file';
|
||||
import { FormControlLabel, Box, TextField, Button, IconButton, ButtonGroup, Tooltip, Select, MenuItem, Typography, FormGroup } from '@mui/material';
|
||||
import { FormControlLabel, Box, ButtonGroup, Tooltip, Typography } from '@mui/material';
|
||||
import { IconButton } from '@kevisual/center-components/button/index.tsx';
|
||||
import { Info, Plus, Save, Share, Shuffle, Trash } from 'lucide-react';
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { create } from 'zustand';
|
||||
import { uniq } from 'lodash-es';
|
||||
import { DatePicker } from './DatePicker';
|
||||
import { SelectPicker } from './SelectPicker';
|
||||
import dayjs from 'dayjs';
|
||||
import { DialogKey } from './DialogKey';
|
||||
import { keysTips, KeyParse } from '../../modules/key-parse';
|
||||
import { KeyShareSelect, KeyTextField } from '../../modules/PermissionManager';
|
||||
@@ -139,7 +139,7 @@ export const MetaForm = () => {
|
||||
} else {
|
||||
_formData[key] = value;
|
||||
}
|
||||
setFormData(_formData);
|
||||
setFormData({ ..._formData });
|
||||
};
|
||||
const deleteMeta = (key: string) => {
|
||||
setKeys(keys.filter((item) => item !== key));
|
||||
@@ -165,14 +165,15 @@ export const MetaForm = () => {
|
||||
<div className='relative w-full h-full'>
|
||||
<Box className='sticky top-0 z-10 pointer-events-none'>
|
||||
<div className='flex justify-end mr-20'>
|
||||
<div className=' pointer-events-auto'>
|
||||
<ButtonGroup className='bg-white' variant='contained' sx={{ color: 'white' }}>
|
||||
<div className=' pointer-events-auto '>
|
||||
<ButtonGroup
|
||||
variant='contained'
|
||||
sx={{
|
||||
mt: 1,
|
||||
backgroundColor: 'primary.main',
|
||||
}}>
|
||||
{btnList.map((item) => {
|
||||
const icon = (
|
||||
<IconButton color='secondary' onClick={item.onClick}>
|
||||
{item.icon}
|
||||
</IconButton>
|
||||
);
|
||||
const icon = <IconButton onClick={item.onClick}>{item.icon}</IconButton>;
|
||||
if (item.tooltip) {
|
||||
return (
|
||||
<Tooltip key={item.key} title={item.tooltip} placement='top' arrow>
|
||||
@@ -212,7 +213,14 @@ export const MetaForm = () => {
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<IconButton color='error' onClick={() => deleteMeta(key)}>
|
||||
<IconButton
|
||||
variant='text'
|
||||
sx={{
|
||||
color: 'primay.main',
|
||||
}}
|
||||
color='error'
|
||||
size='small'
|
||||
onClick={() => deleteMeta(key)}>
|
||||
<Trash />
|
||||
</IconButton>
|
||||
</div>
|
||||
@@ -241,5 +249,3 @@ export const MetaForm = () => {
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -6,12 +6,12 @@ import { getIcon } from '../FileIcon';
|
||||
import { Download, Trash } from 'lucide-react';
|
||||
import clsx from 'clsx';
|
||||
import { useResourceFileStore } from '@kevisual/resources/pages/store/resource-file';
|
||||
import { useConfirm } from '@kevisual/center-components/modal/Confirm.tsx';
|
||||
import { useModal } from '@kevisual/center-components/modal/Confirm.tsx';
|
||||
|
||||
export const FileTable = () => {
|
||||
const { list, prefix, download, onOpenPrefix, deleteFile } = useResourceStore();
|
||||
const { setOpenDrawer, setPrefix } = useResourceFileStore();
|
||||
const { confirm, contextHolder } = useConfirm();
|
||||
const { list, prefix, download, onOpenPrefix, getList } = useResourceStore();
|
||||
const { setOpenDrawer, setPrefix, deleteFile } = useResourceFileStore();
|
||||
const [modal, contextHolder] = useModal();
|
||||
return (
|
||||
<>
|
||||
{contextHolder}
|
||||
@@ -95,9 +95,15 @@ export const FileTable = () => {
|
||||
className='ml-2!'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
confirm('删除文件', '确定删除该文件吗?', {
|
||||
onConfirm: () => {
|
||||
deleteFile(row);
|
||||
modal.confirm({
|
||||
title: '删除文件',
|
||||
content: '确定删除该文件吗?',
|
||||
onOk: () => {
|
||||
deleteFile(row, {
|
||||
onSuccess: () => {
|
||||
getList();
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
}}
|
||||
|
||||
@@ -4,6 +4,8 @@ import { FormControlLabel, TextField, Select, MenuItem, FormGroup, Tooltip } fro
|
||||
import { DatePicker } from '../draw/modules/DatePicker';
|
||||
import { SelectPicker } from '../draw/modules/SelectPicker';
|
||||
import { HelpCircle } from 'lucide-react';
|
||||
import clsx from 'clsx';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
export const KeyShareSelect = ({ name, value, onChange }: { name: string; value: string; onChange?: (value: string) => void }) => {
|
||||
return (
|
||||
<Select
|
||||
@@ -34,8 +36,7 @@ export const KeyTextField = ({ name, value, onChange }: { name: string; value: s
|
||||
variant='outlined'
|
||||
size='small'
|
||||
name={name}
|
||||
defaultValue={value}
|
||||
// value={formData[key] || ''}
|
||||
value={value}
|
||||
onChange={(e) => onChange?.(e.target.value)}
|
||||
sx={{
|
||||
width: '100%',
|
||||
@@ -48,8 +49,10 @@ export const KeyTextField = ({ name, value, onChange }: { name: string; value: s
|
||||
type PermissionManagerProps = {
|
||||
value: Record<string, any>;
|
||||
onChange: (value: Record<string, any>) => void;
|
||||
className?: string;
|
||||
};
|
||||
export const PermissionManager = ({ value, onChange }: PermissionManagerProps) => {
|
||||
export const PermissionManager = ({ value, onChange, className }: PermissionManagerProps) => {
|
||||
const { t, i18n } = useTranslation();
|
||||
const [formData, setFormData] = useState<any>(value);
|
||||
const [keys, setKeys] = useState<any>([]);
|
||||
useEffect(() => {
|
||||
@@ -80,26 +83,19 @@ export const PermissionManager = ({ value, onChange }: PermissionManagerProps) =
|
||||
onChange(KeyParse.stringify(newFormData));
|
||||
}
|
||||
};
|
||||
const tips = getTips('share', i18n.language);
|
||||
return (
|
||||
<form className='w-[400px] flex flex-col gap-2'>
|
||||
<form className={clsx('flex flex-col gap-2', className)}>
|
||||
<FormControlLabel
|
||||
labelPlacement='top'
|
||||
control={<KeyShareSelect name='share' value={formData?.share} onChange={(value) => onChangeValue('share', value)} />}
|
||||
label={
|
||||
<div className='flex items-center gap-1'>
|
||||
Share
|
||||
<Tooltip title={getTips('share')}>
|
||||
{t('Share')}
|
||||
<Tooltip title={tips}>
|
||||
<HelpCircle size={16} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
}
|
||||
sx={{
|
||||
alignItems: 'flex-start',
|
||||
'& .MuiFormControlLabel-label': {
|
||||
textAlign: 'left',
|
||||
width: '100%',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{keys.map((item: any) => {
|
||||
let control: React.ReactNode | null = null;
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import dayjs from 'dayjs';
|
||||
export const getTips = (key: string) => {
|
||||
return keysTips.find((item) => item.key === key)?.tips;
|
||||
export const getTips = (key: string, lang?: string) => {
|
||||
const tip = keysTips.find((item) => item.key === key);
|
||||
if (tip) {
|
||||
if (lang === 'en') {
|
||||
return tip.enTips;
|
||||
}
|
||||
return tip.tips;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
export const keysTips = [
|
||||
{
|
||||
@@ -10,26 +17,35 @@ export const keysTips = [
|
||||
2. 设置受保护需要登录后访问
|
||||
3. 设置私有只有自己可以访问。\n
|
||||
受保护可以设置密码,设置访问的用户名。切换共享状态后,需要重新设置密码和用户名。 不设置,默认是只能自己访问。`,
|
||||
enTips: `1. Set public to directly access
|
||||
2. Set protected to access after login
|
||||
3. Set private to access only yourself.
|
||||
Protected can set a password and set the username for access. After switching the shared state, you need to reset the password and username. If not set, it defaults to only being accessible to yourself.`,
|
||||
},
|
||||
{
|
||||
key: 'content-type',
|
||||
tips: `内容类型,设置文件的内容类型。默认不要修改。`,
|
||||
enTips: `Content type, set the content type of the file. Default do not modify.`,
|
||||
},
|
||||
{
|
||||
key: 'app-source',
|
||||
tips: `应用来源,上传方式。默认不要修改。`,
|
||||
enTips: `App source, upload method. Default do not modify.`,
|
||||
},
|
||||
{
|
||||
key: 'cache-control',
|
||||
tips: `缓存控制,设置文件的缓存控制。默认不要修改。`,
|
||||
enTips: `Cache control, set the cache control of the file. Default do not modify.`,
|
||||
},
|
||||
{
|
||||
key: 'password',
|
||||
tips: `密码,设置文件的密码。不设置默认是所有人都可以访问。`,
|
||||
enTips: `Password, set the password of the file. If not set, it defaults to everyone can access.`,
|
||||
},
|
||||
{
|
||||
key: 'usernames',
|
||||
tips: `用户名,设置文件的用户名。不设置默认是所有人都可以访问。`,
|
||||
enTips: `Username, set the username of the file. If not set, it defaults to everyone can access.`,
|
||||
parse: (value: string) => {
|
||||
if (!value) {
|
||||
return [];
|
||||
@@ -46,6 +62,7 @@ export const keysTips = [
|
||||
{
|
||||
key: 'expiration-time',
|
||||
tips: `过期时间,设置文件的过期时间。不设置默认是永久。`,
|
||||
enTips: `Expiration time, set the expiration time of the file. If not set, it defaults to permanent.`,
|
||||
parse: (value: Date) => {
|
||||
if (!value) {
|
||||
return null;
|
||||
|
||||
@@ -10,8 +10,15 @@ interface ResourceFileStore {
|
||||
setOpenDrawer: (openDrawer: boolean) => void;
|
||||
prefix: string;
|
||||
setPrefix: (prefix: string, replace?: string) => void;
|
||||
/**
|
||||
* 需要先设置prefix
|
||||
* @returns
|
||||
*/
|
||||
getStatFile: () => Promise<any>;
|
||||
updateMeta: (metadata: any) => Promise<any>;
|
||||
deleteFile: (resource: Resource, opts?: { onSuccess?: (res: any) => void }) => Promise<void>;
|
||||
once: ((data: any) => any) | null;
|
||||
setOnce: (data: any) => void;
|
||||
}
|
||||
|
||||
export const useResourceFileStore = create<ResourceFileStore>((set, get) => ({
|
||||
@@ -45,10 +52,35 @@ export const useResourceFileStore = create<ResourceFileStore>((set, get) => ({
|
||||
},
|
||||
});
|
||||
if (res.code === 200) {
|
||||
// set({ resource: { ...res.data, name: resource?.name } });
|
||||
toast.success('Update metadata success');
|
||||
getStatFile();
|
||||
} else {
|
||||
toast.error(res.message || '更新元数据失败');
|
||||
}
|
||||
},
|
||||
deleteFile: async (resource: Resource, opts?: { onSuccess?: (res: any) => void }) => {
|
||||
const { once, setOnce } = get();
|
||||
const name = resource.name;
|
||||
if (!name) {
|
||||
toast.error('Resource is not a file');
|
||||
return;
|
||||
}
|
||||
const res = await query.post({
|
||||
path: 'file',
|
||||
key: 'delete',
|
||||
data: {
|
||||
prefix: name,
|
||||
},
|
||||
});
|
||||
if (res.code === 200) {
|
||||
toast.success('Delete file success');
|
||||
opts?.onSuccess?.(res);
|
||||
once?.(res);
|
||||
setOnce(null);
|
||||
} else {
|
||||
toast.error(res.message || 'Request failed');
|
||||
}
|
||||
},
|
||||
once: null,
|
||||
setOnce: (data: any) => set({ once: data }),
|
||||
}));
|
||||
|
||||
@@ -3,30 +3,67 @@ import { useDropzone } from 'react-dropzone';
|
||||
import { uploadFiles } from './utils/upload';
|
||||
import { FileText, CloudUpload as UploadIcon } from 'lucide-react';
|
||||
import { uploadFileChunked } from './utils/upload-chunk';
|
||||
export const UploadButton = (props: { prefix?: string; onUpload?: (res: any) => void; hasDirectory?: boolean; uploadDirectory?: boolean }) => {
|
||||
import { filterFiles } from './utils/filter-files';
|
||||
|
||||
type UploadButtonProps = {
|
||||
/**
|
||||
* 前缀
|
||||
*/
|
||||
directory?: string;
|
||||
/**
|
||||
* 应用key
|
||||
*/
|
||||
appKey?: string;
|
||||
/**
|
||||
* 版本
|
||||
*/
|
||||
version?: string;
|
||||
/**
|
||||
* 用户名
|
||||
*/
|
||||
username?: string;
|
||||
/**
|
||||
* 上传回调
|
||||
*/
|
||||
onUpload?: (res: any) => void;
|
||||
/**
|
||||
* 是否上传文件夹
|
||||
*/
|
||||
uploadDirectory?: boolean;
|
||||
/**
|
||||
* 是否只显示图标
|
||||
*/
|
||||
onlyIcon?: boolean;
|
||||
/**
|
||||
* 上传图标
|
||||
*/
|
||||
icon?: React.ReactNode;
|
||||
};
|
||||
/**
|
||||
* 上传按钮
|
||||
* @param props
|
||||
* @returns
|
||||
*/
|
||||
export const UploadButton = (props: UploadButtonProps) => {
|
||||
const { onlyIcon = false, icon } = props;
|
||||
const { appKey, version, username, directory } = props;
|
||||
const onDrop = async (acceptedFiles) => {
|
||||
console.log(acceptedFiles);
|
||||
acceptedFiles = filterFiles(acceptedFiles);
|
||||
if (acceptedFiles.length > 1) {
|
||||
const res = await uploadFiles(acceptedFiles, { directory: props.prefix });
|
||||
const res = await uploadFiles(acceptedFiles, { directory, appKey, version, username });
|
||||
console.log('uploadFiles res', res);
|
||||
props.onUpload?.(res);
|
||||
} else if (acceptedFiles.length === 1) {
|
||||
const res = await uploadFileChunked(acceptedFiles[0], { directory: props.prefix });
|
||||
const res = await uploadFileChunked(acceptedFiles[0], { directory, appKey, version, username });
|
||||
console.log('uploadFiles res', res);
|
||||
props.onUpload?.(res);
|
||||
}
|
||||
};
|
||||
const { getRootProps, getInputProps } = useDropzone({ onDrop });
|
||||
return (
|
||||
<Box {...getRootProps()}>
|
||||
<Button
|
||||
color='primary'
|
||||
sx={{
|
||||
minWidth: 'unset',
|
||||
padding: '2px',
|
||||
}}>
|
||||
<UploadIcon />
|
||||
</Button>
|
||||
const uploadCom = (
|
||||
<div {...getRootProps()}>
|
||||
{icon || <UploadIcon />}
|
||||
<input
|
||||
type='file'
|
||||
style={{ display: 'none' }}
|
||||
@@ -35,12 +72,27 @@ export const UploadButton = (props: { prefix?: string; onUpload?: (res: any) =>
|
||||
webkitdirectory={props.uploadDirectory ? 'true' : undefined}
|
||||
mozdirectory={props.uploadDirectory ? 'true' : undefined}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
if (onlyIcon) {
|
||||
return uploadCom;
|
||||
}
|
||||
return (
|
||||
<Box>
|
||||
<Button
|
||||
color='primary'
|
||||
sx={{
|
||||
minWidth: 'unset',
|
||||
padding: '2px',
|
||||
}}>
|
||||
{uploadCom}
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
export const Upload = ({ uploadDirectory = false }: { uploadDirectory?: boolean }) => {
|
||||
const onDrop = async (acceptedFiles) => {
|
||||
console.log(acceptedFiles);
|
||||
acceptedFiles = filterFiles(acceptedFiles);
|
||||
if (acceptedFiles.length > 1) {
|
||||
const res = await uploadFiles(acceptedFiles, {});
|
||||
console.log('uploadFiles res', res);
|
||||
|
||||
@@ -21,12 +21,8 @@ const getFileType = (extension: string) => {
|
||||
return 'image/gif';
|
||||
case 'svg':
|
||||
return 'image/svg+xml';
|
||||
case 'ico':
|
||||
return 'image/x-icon';
|
||||
case 'webp':
|
||||
return 'image/webp';
|
||||
case 'gif':
|
||||
return 'image/gif';
|
||||
case 'ico':
|
||||
return 'image/x-icon';
|
||||
default:
|
||||
|
||||
23
packages/resources/src/pages/upload/utils/filter-files.ts
Normal file
23
packages/resources/src/pages/upload/utils/filter-files.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* 过滤文件, 过滤 .DS_Store, node_modules, 以.开头的文件, 过滤 __开头的文件
|
||||
* @param files
|
||||
* @returns
|
||||
*/
|
||||
export const filterFiles = (files: File[]) => {
|
||||
files = files.filter((file) => {
|
||||
if (file.webkitRelativePath.startsWith('__MACOSX')) {
|
||||
return false;
|
||||
}
|
||||
// 过滤node_modules
|
||||
if (file.webkitRelativePath.includes('node_modules')) {
|
||||
return false;
|
||||
}
|
||||
// 过滤文件 .DS_Store
|
||||
if (file.name === '.DS_Store') {
|
||||
return false;
|
||||
}
|
||||
// 过滤以.开头的文件
|
||||
return !file.name.startsWith('.');
|
||||
});
|
||||
return files;
|
||||
};
|
||||
@@ -18,6 +18,7 @@ export const uploadFileChunked = async (file: File, opts: ConvertOpts) => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) {
|
||||
console.log('uploadFileChunked token', token);
|
||||
toastLogin();
|
||||
return;
|
||||
}
|
||||
@@ -33,6 +34,7 @@ export const uploadFileChunked = async (file: File, opts: ConvertOpts) => {
|
||||
searchParams.set('public', 'true');
|
||||
}
|
||||
const eventSource = new EventSource('/api/s1/events?' + searchParams.toString());
|
||||
let isError = false;
|
||||
// 监听服务器推送的进度更新
|
||||
eventSource.onmessage = function (event) {
|
||||
console.log('Progress update:', event.data);
|
||||
@@ -55,6 +57,7 @@ export const uploadFileChunked = async (file: File, opts: ConvertOpts) => {
|
||||
};
|
||||
eventSource.onerror = function (event) {
|
||||
console.log('eventSource.onerror', event);
|
||||
isError = true;
|
||||
reject(event);
|
||||
};
|
||||
|
||||
@@ -91,6 +94,15 @@ export const uploadFileChunked = async (file: File, opts: ConvertOpts) => {
|
||||
},
|
||||
}).then((response) => response.json());
|
||||
fetch('/api/s1/events/close?taskId=' + taskId);
|
||||
if (res?.code !== 200) {
|
||||
toast.error('上传失败');
|
||||
isError = true;
|
||||
NProgress.done();
|
||||
eventSource.close();
|
||||
toast.dismiss(load);
|
||||
reject(new Error(res?.message || '上传失败'));
|
||||
return;
|
||||
}
|
||||
if (isLast) {
|
||||
NProgress.done();
|
||||
eventSource.close();
|
||||
|
||||
@@ -3,7 +3,6 @@ import 'nprogress/nprogress.css';
|
||||
import { toast } from 'react-toastify';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { toastLogin } from '@kevisual/resources/pages/message/ToastLogin';
|
||||
|
||||
type ConvertOpts = {
|
||||
appKey?: string;
|
||||
version?: string;
|
||||
@@ -39,6 +38,7 @@ export const uploadFiles = async (files: File[], opts: ConvertOpts) => {
|
||||
}
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) {
|
||||
console.log('uploadFiles token', token);
|
||||
toastLogin();
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user