feat: 去掉antd

This commit is contained in:
2025-03-20 21:47:50 +08:00
parent c206add7eb
commit cfd263a1e7
36 changed files with 1369 additions and 769 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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