feat: add resources
This commit is contained in:
parent
cc76842582
commit
25def8c245
@ -1,6 +1,6 @@
|
||||
import { createTheme, Shadows, ThemeOptions } from '@mui/material/styles';
|
||||
import { useTheme as useMuiTheme, Theme } from '@mui/material/styles';
|
||||
import { amber } from '@mui/material/colors';
|
||||
import { amber, red } from '@mui/material/colors';
|
||||
const generateShadows = (color: string): Shadows => {
|
||||
return [
|
||||
'none',
|
||||
@ -56,6 +56,9 @@ export const themeOptions: ThemeOptions = {
|
||||
default: '#ffffff', // 设置默认背景颜色
|
||||
// paper: '#f5f5f5', // 设置纸张背景颜色
|
||||
},
|
||||
error: {
|
||||
main: red[500],
|
||||
},
|
||||
},
|
||||
shadows: generateShadows('rgba(255, 193, 7, 0.2)'),
|
||||
typography: {
|
||||
@ -103,6 +106,24 @@ export const themeOptions: ThemeOptions = {
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiSelect: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
'& .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: amber[300],
|
||||
},
|
||||
'&:hover .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: amber[500],
|
||||
},
|
||||
'&.Mui-focused .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: amber[500],
|
||||
},
|
||||
'& .MuiSelect-icon': {
|
||||
color: amber[500], // Set arrow icon color to primary
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiCard: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
|
@ -20,7 +20,8 @@
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
<script src="/system/lib/app.js"></script>
|
||||
<!-- <script src="/system/lib/app.js"></script> -->
|
||||
<script src="https://kevisual.xiongxiao.me/system/lib/app.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
@ -3,10 +3,15 @@
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"basename": "/root/resources",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build"
|
||||
"build": "vite build",
|
||||
"pub": "envision deploy ./dist -k resources -v 0.0.1 -u -o root"
|
||||
},
|
||||
"files": [
|
||||
"src"
|
||||
],
|
||||
"keywords": [],
|
||||
"author": "abearxiong <xiongxiao@xiongxiao.me>",
|
||||
"license": "MIT",
|
||||
@ -17,7 +22,7 @@
|
||||
"@kevisual/center-components": "workspace:*",
|
||||
"@kevisual/router": "^0.0.9",
|
||||
"@kevisual/store": "^0.0.2",
|
||||
"@mui/material": "^6.4.7",
|
||||
"@mui/material": "^6.4.8",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/nprogress": "^0.2.3",
|
||||
"@vitejs/plugin-basic-ssl": "^2.0.0",
|
||||
@ -25,10 +30,11 @@
|
||||
"immer": "^10.1.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lucide-react": "^0.482.0",
|
||||
"nanoid": "^5.1.3",
|
||||
"nanoid": "^5.1.4",
|
||||
"nprogress": "^0.2.0",
|
||||
"pretty-bytes": "^6.1.1",
|
||||
"react": "19.0.0",
|
||||
"react-datepicker": "^8.2.1",
|
||||
"react-dom": "19.0.0",
|
||||
"react-dropzone": "^14.3.8",
|
||||
"react-toastify": "^11.0.5",
|
||||
@ -37,4 +43,4 @@
|
||||
"devDependencies": {
|
||||
"@kevisual/types": "^0.0.6"
|
||||
}
|
||||
}
|
||||
}
|
6
packages/resources/public/config.js
Normal file
6
packages/resources/public/config.js
Normal file
@ -0,0 +1,6 @@
|
||||
export const config = {
|
||||
api: () => location.origin,
|
||||
apps: {
|
||||
login: '/user/login',
|
||||
},
|
||||
};
|
@ -7,6 +7,8 @@
|
||||
}
|
||||
:root {
|
||||
--scrollbar-color: #ffbf00;
|
||||
--primary-color: #ffc107;
|
||||
--secondary-color: #ffa000;
|
||||
}
|
||||
#root {
|
||||
width: 100%;
|
||||
@ -26,3 +28,39 @@
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--scrollbar-color) #fff;
|
||||
}
|
||||
|
||||
.scrollbar::-webkit-scrollbar {
|
||||
height: 4px;
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
.scrollbar::-webkit-scrollbar-thumb {
|
||||
background-color: var(--scrollbar-color);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.scrollbar::-webkit-scrollbar-track {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.ant-select-outlined.ant-select-multiple .ant-select-selection-item {
|
||||
background: var(--secondary-color);
|
||||
color: white;
|
||||
svg {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
.ant-select-selection-item {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
.ant-select {
|
||||
.ant-select-arrow {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
.ant-picker-input {
|
||||
.ant-picker-suffix,
|
||||
.ant-picker-clear {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
}
|
||||
|
@ -1,50 +1,4 @@
|
||||
import { page, app } from './app.ts';
|
||||
import { basename } from './modules/basename.ts';
|
||||
import './pages/main.tsx';
|
||||
import { bootstrap } from './pages/Bootstrap';
|
||||
|
||||
export const render = ({ renderRoot }) => {
|
||||
renderRoot.innerHTML = `
|
||||
<h1>Hello, World!</h1>
|
||||
`;
|
||||
};
|
||||
console.log('basename', basename, page, app);
|
||||
bootstrap('#ai-root');
|
||||
|
||||
if (page) {
|
||||
page.addPage('/', 'home');
|
||||
page.subscribe('home', () => {
|
||||
render({
|
||||
renderRoot: document.getElementById('ai-root'),
|
||||
});
|
||||
});
|
||||
page.addPage('', 'index');
|
||||
page.subscribe('index', () => {
|
||||
const root = document.getElementById('ai-root') as HTMLElement;
|
||||
root.innerHTML = `
|
||||
<h1>Hello, World!</h1>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
if (app) {
|
||||
app
|
||||
.route({
|
||||
path: 'app-template',
|
||||
key: 'render',
|
||||
})
|
||||
.define(async (ctx) => {
|
||||
let { renderRoot } = ctx.query;
|
||||
if (!renderRoot) {
|
||||
ctx.throw(404, 'renderRoot is required');
|
||||
}
|
||||
if (typeof renderRoot === 'string') {
|
||||
renderRoot = document.querySelector(renderRoot);
|
||||
}
|
||||
if (!renderRoot) {
|
||||
ctx.throw(404, 'renderRoot not found');
|
||||
}
|
||||
render({
|
||||
renderRoot,
|
||||
});
|
||||
})
|
||||
.addTo(app);
|
||||
}
|
||||
|
@ -1,3 +1,15 @@
|
||||
import { toastLogin } from '@/pages/message/ToastLogin';
|
||||
import { QueryClient } from '@kevisual/query';
|
||||
|
||||
export const query = new QueryClient();
|
||||
export const query = new QueryClient();
|
||||
|
||||
query.afterResponse = async (response) => {
|
||||
if (response.code === 401) {
|
||||
toastLogin();
|
||||
return {
|
||||
...response,
|
||||
noMsg: true,
|
||||
};
|
||||
}
|
||||
return response;
|
||||
};
|
||||
|
@ -5,10 +5,47 @@ import { Left } from './layout/Left';
|
||||
import { Main } from './main/index';
|
||||
import { ToastContainer } from 'react-toastify';
|
||||
import { useSettingsStore } from './store/settings';
|
||||
import { CircularProgress } from '@mui/material';
|
||||
import { CircularProgress, useTheme } from '@mui/material';
|
||||
import { useResourceStore } from './store/resource';
|
||||
import dayjs from 'dayjs';
|
||||
import 'dayjs/locale/zh-cn';
|
||||
|
||||
export const App = () => {
|
||||
import zhCN from 'antd/locale/zh_CN';
|
||||
import ConfigProvider from 'antd/es/config-provider';
|
||||
|
||||
dayjs.locale('zh-cn');
|
||||
export const AntdConfigProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const theme = useTheme();
|
||||
const primaryColor = theme.palette.primary.main;
|
||||
const secondaryColor = theme.palette.secondary.main;
|
||||
return (
|
||||
<ConfigProvider
|
||||
locale={zhCN}
|
||||
theme={{
|
||||
token: {
|
||||
colorPrimary: primaryColor,
|
||||
colorPrimaryHover: secondaryColor,
|
||||
colorPrimaryActive: primaryColor,
|
||||
borderRadius: 4,
|
||||
colorBorder: primaryColor,
|
||||
// colorText: primaryColor,
|
||||
colorIcon: primaryColor,
|
||||
colorIconHover: secondaryColor,
|
||||
colorInfoHover: secondaryColor,
|
||||
},
|
||||
components: {
|
||||
DatePicker: {
|
||||
colorPrimary: primaryColor,
|
||||
colorPrimaryHover: secondaryColor,
|
||||
colorPrimaryActive: primaryColor,
|
||||
},
|
||||
},
|
||||
}}>
|
||||
{children}
|
||||
</ConfigProvider>
|
||||
);
|
||||
};
|
||||
export const InitProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const { init, mounted, settings } = useSettingsStore();
|
||||
const { setPrefix, init: initResource } = useResourceStore();
|
||||
useEffect(() => {
|
||||
@ -16,24 +53,46 @@ export const App = () => {
|
||||
initResource();
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
if (settings.prefix && mounted) {
|
||||
if (settings.prefix && mounted === 'success') {
|
||||
setPrefix(settings.prefix);
|
||||
}
|
||||
}, [mounted, settings.prefix]);
|
||||
if (!mounted) {
|
||||
|
||||
const handleRetry = () => {
|
||||
init();
|
||||
initResource();
|
||||
};
|
||||
|
||||
if (mounted === 'loading') {
|
||||
return (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
|
||||
<CircularProgress />
|
||||
</div>
|
||||
);
|
||||
} else if (mounted === 'error') {
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', height: '100vh', color: 'red' }}>
|
||||
<h2>出现问题</h2>
|
||||
<p>加载设置时遇到错误。请重试。</p>
|
||||
<button onClick={handleRetry} style={{ marginTop: '20px', padding: '10px 20px', backgroundColor: '#f44336', color: '#fff', border: 'none', borderRadius: '4px', cursor: 'pointer' }}>
|
||||
重试
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
export const App = () => {
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<Left>
|
||||
<Main />
|
||||
</Left>
|
||||
<ToastContainer />
|
||||
<AntdConfigProvider>
|
||||
<InitProvider>
|
||||
<Left>
|
||||
<Main />
|
||||
</Left>
|
||||
</InitProvider>
|
||||
<ToastContainer />
|
||||
</AntdConfigProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
61
packages/resources/src/pages/Bootstrap.tsx
Normal file
61
packages/resources/src/pages/Bootstrap.tsx
Normal file
@ -0,0 +1,61 @@
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { App } from './App';
|
||||
import React from 'react';
|
||||
|
||||
export class ReactRenderer {
|
||||
component: any;
|
||||
element: HTMLElement;
|
||||
ref: React.RefObject<any>;
|
||||
props: any;
|
||||
root: any;
|
||||
constructor(component: any, { props, className }: any) {
|
||||
this.component = component;
|
||||
const el = document.createElement('div');
|
||||
this.element = el;
|
||||
this.ref = React.createRef();
|
||||
this.props = {
|
||||
...props,
|
||||
ref: this.ref,
|
||||
};
|
||||
el.className = className;
|
||||
this.root = createRoot(this.element);
|
||||
this.render();
|
||||
}
|
||||
|
||||
updateProps(props: any) {
|
||||
this.props = {
|
||||
...this.props,
|
||||
...props,
|
||||
};
|
||||
this.render();
|
||||
}
|
||||
|
||||
render() {
|
||||
this.root.render(React.createElement(this.component, this.props));
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.root.unmount();
|
||||
}
|
||||
}
|
||||
|
||||
export default ReactRenderer;
|
||||
|
||||
export const bootstrap = (el: HTMLElement | string) => {
|
||||
// createRoot(document.getElementById('ai-root')!).render(<App />);
|
||||
const root = typeof el === 'string' ? document.querySelector(el) : el;
|
||||
if (root) {
|
||||
const renderer = new ReactRenderer(App, {
|
||||
props: {},
|
||||
className: 'resources-root w-full h-full',
|
||||
});
|
||||
if (window.context) {
|
||||
window.context.resourcesApp = renderer;
|
||||
} else {
|
||||
window.context = {
|
||||
resourcesApp: renderer,
|
||||
};
|
||||
}
|
||||
root.appendChild(renderer.element);
|
||||
}
|
||||
};
|
@ -90,3 +90,83 @@ export const getIcon = (name: string) => {
|
||||
return <File />;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取文件类型,文件大类
|
||||
* @param name 文件名
|
||||
* @returns 文件类型
|
||||
*/
|
||||
export const getFileType = (name?: string) => {
|
||||
if (!name) {
|
||||
return '';
|
||||
}
|
||||
const extension = getExtension(name);
|
||||
switch (extension) {
|
||||
case 'pdf':
|
||||
return 'pdf';
|
||||
case 'jpg':
|
||||
case 'jpeg':
|
||||
case 'gif':
|
||||
case 'png':
|
||||
return 'image';
|
||||
case 'mp3':
|
||||
case 'wav':
|
||||
case 'ogg':
|
||||
case 'm4a':
|
||||
case 'aac':
|
||||
case 'flac':
|
||||
case 'wma':
|
||||
return 'audio';
|
||||
case 'mp4':
|
||||
return 'video';
|
||||
case 'doc':
|
||||
case 'docx':
|
||||
return 'word';
|
||||
case 'ppt':
|
||||
case 'pptx':
|
||||
return 'ppt';
|
||||
case 'xls':
|
||||
case 'xlsx':
|
||||
return 'excel';
|
||||
case 'zip':
|
||||
case 'rar':
|
||||
case '7z':
|
||||
case 'tar':
|
||||
case 'gz':
|
||||
case 'bz2':
|
||||
return 'zip';
|
||||
case 'txt':
|
||||
case 'md':
|
||||
case 'csv':
|
||||
case 'json':
|
||||
case 'xml':
|
||||
case 'yaml':
|
||||
case 'yml':
|
||||
case 'toml':
|
||||
case 'ini':
|
||||
case 'conf':
|
||||
case 'cfg':
|
||||
case 'config':
|
||||
case 'props':
|
||||
case 'properties':
|
||||
case 'log':
|
||||
case 'sh':
|
||||
case 'bash':
|
||||
case 'zsh':
|
||||
case 'fish':
|
||||
case 'bat':
|
||||
case 'cmd':
|
||||
return 'text';
|
||||
case 'html':
|
||||
case 'htm':
|
||||
return 'html';
|
||||
case 'css':
|
||||
case 'js':
|
||||
case 'ts':
|
||||
case 'jsx':
|
||||
case 'tsx':
|
||||
return 'code';
|
||||
default:
|
||||
return 'other';
|
||||
}
|
||||
};
|
||||
|
@ -1,16 +1,60 @@
|
||||
import { useResourceStore } from '@/pages/store/resource';
|
||||
import { useResourceFileStore } from '@/pages/store/resource-file';
|
||||
import { Drawer } from '@mui/material';
|
||||
import { Box, Divider, Drawer, Tab, Tabs } from '@mui/material';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { QuickValues, QuickTabs } from './QuickTabs';
|
||||
|
||||
export const FileDrawer = () => {
|
||||
const { prefix } = useResourceStore();
|
||||
const { resource, openDrawer, setOpenDrawer } = useResourceFileStore();
|
||||
const [tab, setTab] = useState<string>(QuickValues[0]);
|
||||
const quickCom = useMemo(() => {
|
||||
return QuickTabs.find((item) => item.value === tab)?.component;
|
||||
}, [tab]);
|
||||
return (
|
||||
<Drawer open={openDrawer} onClose={() => setOpenDrawer(false)} anchor='right' {...(!openDrawer && { inert: true })}>
|
||||
<div className='p-4 w-[600px]'>
|
||||
<h2 className='text-2xl font-bold'>{resource?.name ? resource.name.replace(prefix, '') : resource?.prefix?.replace(prefix, '')}</h2>
|
||||
<pre className='flex flex-col gap-2'>{JSON.stringify(resource, null, 2)}</pre>
|
||||
</div>
|
||||
</Drawer>
|
||||
<>
|
||||
<Drawer
|
||||
// aria-label={'文件详情'}
|
||||
open={openDrawer}
|
||||
onClose={() => {
|
||||
// document.getElementById('focus-safe-element')?.focus();
|
||||
const activeElement = document.activeElement as HTMLElement;
|
||||
if (activeElement) {
|
||||
activeElement.blur();
|
||||
}
|
||||
setOpenDrawer(false);
|
||||
}}
|
||||
ModalProps={{
|
||||
keepMounted: true,
|
||||
}}
|
||||
anchor='right'
|
||||
style={{
|
||||
zIndex: 1000,
|
||||
}}>
|
||||
<div className='p-4 w-[400px] max-w-[90%] 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>
|
||||
<Divider />
|
||||
<Box sx={{ borderBottom: 1, mt: 2, borderColor: 'divider' }}>
|
||||
<Tabs value={tab} onChange={(_, value) => setTab(value)}>
|
||||
{QuickTabs.map((item) => (
|
||||
<Tab key={item.value} label={item.label} value={item.value} icon={item.icon} iconPosition='start' />
|
||||
))}
|
||||
</Tabs>
|
||||
</Box>
|
||||
</div>
|
||||
<Box className='' sx={{ p: 2, height: 'calc(100% - 140px)', overflow: 'hidden' }}>
|
||||
<div className='scrollbar' style={{ height: '100%', overflow: 'auto' }}>
|
||||
{quickCom && quickCom()}
|
||||
</div>
|
||||
</Box>
|
||||
</div>
|
||||
</Drawer>
|
||||
<button id='focus-safe-element' style={{ display: 'none' }}>
|
||||
Focus Safe Element
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
27
packages/resources/src/pages/file/draw/QuickTabs.tsx
Normal file
27
packages/resources/src/pages/file/draw/QuickTabs.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import { MetaForm } from './modules/MetaForm';
|
||||
import { ContentForm } from './modules/ContextForm';
|
||||
import { Cpu, File, FileText, Rabbit } from 'lucide-react';
|
||||
import { Quick } from './quick';
|
||||
export const QuickTabs = [
|
||||
{
|
||||
label: 'Quick',
|
||||
value: 'quick',
|
||||
icon: <Rabbit />,
|
||||
index: 99,
|
||||
component: () => <Quick />,
|
||||
},
|
||||
{
|
||||
label: '元数据',
|
||||
value: 'meta',
|
||||
icon: <Cpu />,
|
||||
component: () => <MetaForm />,
|
||||
},
|
||||
{
|
||||
label: '内容',
|
||||
value: 'content',
|
||||
icon: <FileText />,
|
||||
component: () => <ContentForm />,
|
||||
},
|
||||
].sort((a, b) => (b?.index || 0) - (a?.index || 0));
|
||||
|
||||
export const QuickValues = QuickTabs.map((item) => item.value);
|
@ -0,0 +1,29 @@
|
||||
import { useResourceFileStore } from '@/pages/store/resource-file';
|
||||
import { Box, Typography } from '@mui/material';
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
type ContentShowType = {
|
||||
size: number;
|
||||
lastModified: string;
|
||||
etag: string;
|
||||
name?: string;
|
||||
};
|
||||
export const ContentForm = () => {
|
||||
const { resource } = useResourceFileStore();
|
||||
const contentShow = resource as ContentShowType;
|
||||
return (
|
||||
<Box className='p-4 border rounded-md mt-2'>
|
||||
{/* <Typography variant='h6'>{contentShow?.name || 'No Name Available'}</Typography> */}
|
||||
<Typography variant='body1'>
|
||||
<strong>Size:</strong> {contentShow?.size ? prettyBytes(contentShow?.size) : 'N/A'}
|
||||
</Typography>
|
||||
<Typography variant='body1'>
|
||||
<strong>Last Modified:</strong> {contentShow?.lastModified ? dayjs(contentShow?.lastModified).format('YYYY-MM-DD HH:mm:ss') : 'N/A'}
|
||||
</Typography>
|
||||
<Typography variant='body1'>
|
||||
<strong>ETag:</strong> {contentShow?.etag || 'N/A'}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
};
|
@ -0,0 +1,28 @@
|
||||
import ReactDatePicker from 'antd/es/date-picker';
|
||||
import { useTheme } from '@mui/material';
|
||||
import 'antd/es/date-picker/style/index';
|
||||
interface DatePickerProps {
|
||||
value?: Date | null;
|
||||
onChange?: (date: Date | null) => void;
|
||||
}
|
||||
|
||||
export const DatePicker = ({ value, onChange }: DatePickerProps) => {
|
||||
const theme = useTheme();
|
||||
const primaryColor = theme.palette.primary.main;
|
||||
return (
|
||||
<div>
|
||||
<ReactDatePicker
|
||||
placement='topLeft'
|
||||
placeholder='请选择日期'
|
||||
value={value}
|
||||
showNow={false}
|
||||
// showTime={true}
|
||||
onChange={(date) => onChange?.(date)} //
|
||||
style={{
|
||||
color: primaryColor,
|
||||
}}
|
||||
popupStyle={{ zIndex: 2000 }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
39
packages/resources/src/pages/file/draw/modules/DialogKey.tsx
Normal file
39
packages/resources/src/pages/file/draw/modules/DialogKey.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import { Button, Dialog, DialogContent, DialogTitle, FormControlLabel, TextField } from '@mui/material';
|
||||
import { useState } from 'react';
|
||||
import { useMetaStore } from './MetaForm';
|
||||
export const DialogKey = ({ onAdd }: { onAdd: (key: string) => void }) => {
|
||||
const { openPropertyModal, setOpenPropertyModal } = useMetaStore();
|
||||
const [key, setKey] = useState('');
|
||||
return (
|
||||
<Dialog open={openPropertyModal} onClose={() => setOpenPropertyModal(false)}>
|
||||
<DialogTitle>添加元数据key</DialogTitle>
|
||||
<DialogContent>
|
||||
<div className='flex flex-col items-center gap-3 px-4 pb-4'>
|
||||
<FormControlLabel
|
||||
label='key'
|
||||
labelPlacement='top'
|
||||
control={<TextField variant='outlined' size='small' name={key} value={key} onChange={(e) => setKey(e.target.value)} />}
|
||||
sx={{
|
||||
alignItems: 'flex-start',
|
||||
'& .MuiFormControlLabel-label': {
|
||||
textAlign: 'left',
|
||||
width: '100%',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant='contained'
|
||||
color='primary'
|
||||
style={{ color: 'white' }}
|
||||
onClick={() => {
|
||||
onAdd(key);
|
||||
setKey('');
|
||||
setOpenPropertyModal(false);
|
||||
}}>
|
||||
添加
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
368
packages/resources/src/pages/file/draw/modules/MetaForm.tsx
Normal file
368
packages/resources/src/pages/file/draw/modules/MetaForm.tsx
Normal file
@ -0,0 +1,368 @@
|
||||
import { useResourceFileStore } from '@/pages/store/resource-file';
|
||||
import { FormControlLabel, Box, TextField, Button, IconButton, ButtonGroup, Tooltip, Select, MenuItem, Typography, FormGroup } from '@mui/material';
|
||||
import { Info, Plus, Save, Share, Shuffle, Trash } from 'lucide-react';
|
||||
import { useState, useEffect, useMemo } 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';
|
||||
|
||||
export const setShareKeysOperate = (value: 'public' | 'protected' | 'private') => {
|
||||
const keys = ['password', 'usernames', 'expiration-time'];
|
||||
const deleteKeys = keys.map((item) => {
|
||||
return {
|
||||
key: item,
|
||||
operate: 'delete',
|
||||
};
|
||||
});
|
||||
if (value === 'protected') {
|
||||
return deleteKeys.map((item) => {
|
||||
return {
|
||||
...item,
|
||||
operate: 'add',
|
||||
};
|
||||
});
|
||||
}
|
||||
return deleteKeys;
|
||||
};
|
||||
export const keysTips = [
|
||||
{
|
||||
key: 'share',
|
||||
tips: `共享设置
|
||||
1. 设置公共可以直接访问
|
||||
2. 设置受保护需要登录后访问
|
||||
3. 设置私有只有自己可以访问。\n
|
||||
受保护可以设置密码,设置访问的用户名。切换共享状态后,需要重新设置密码和用户名。`,
|
||||
},
|
||||
{
|
||||
key: 'content-type',
|
||||
tips: `内容类型,设置文件的内容类型。默认不要修改。`,
|
||||
},
|
||||
{
|
||||
key: 'app-source',
|
||||
tips: `应用来源,上传方式。默认不要修改。`,
|
||||
},
|
||||
{
|
||||
key: 'cache-control',
|
||||
tips: `缓存控制,设置文件的缓存控制。默认不要修改。`,
|
||||
},
|
||||
{
|
||||
key: 'password',
|
||||
tips: `密码,设置文件的密码。不设置默认是所有人都可以访问。`,
|
||||
},
|
||||
{
|
||||
key: 'usernames',
|
||||
tips: `用户名,设置文件的用户名。不设置默认是所有人都可以访问。`,
|
||||
parse: (value: string) => {
|
||||
if (!value) {
|
||||
return [];
|
||||
}
|
||||
return value.split(',');
|
||||
},
|
||||
stringify: (value: string[]) => {
|
||||
if (!value) {
|
||||
return '';
|
||||
}
|
||||
return value.join(',');
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'expiration-time',
|
||||
tips: `过期时间,设置文件的过期时间。不设置默认是永久。`,
|
||||
parse: (value: Date) => {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
return dayjs(value);
|
||||
},
|
||||
stringify: (value?: dayjs.Dayjs) => {
|
||||
if (!value) {
|
||||
return '';
|
||||
}
|
||||
return value.toISOString();
|
||||
},
|
||||
},
|
||||
];
|
||||
export class KeyParse {
|
||||
static parse(metadata: Record<string, any>) {
|
||||
const keys = Object.keys(metadata);
|
||||
const newMetadata = {};
|
||||
keys.forEach((key) => {
|
||||
const tip = keysTips.find((item) => item.key === key);
|
||||
if (tip && tip.parse) {
|
||||
newMetadata[key] = tip.parse(metadata[key]);
|
||||
} else {
|
||||
newMetadata[key] = metadata[key];
|
||||
}
|
||||
});
|
||||
return newMetadata;
|
||||
}
|
||||
static stringify(metadata: Record<string, any>) {
|
||||
const keys = Object.keys(metadata);
|
||||
const newMetadata = {};
|
||||
keys.forEach((key) => {
|
||||
const tip = keysTips.find((item) => item.key === key);
|
||||
if (tip && tip.stringify) {
|
||||
newMetadata[key] = tip.stringify(metadata[key]);
|
||||
} else {
|
||||
newMetadata[key] = metadata[key];
|
||||
}
|
||||
});
|
||||
return newMetadata;
|
||||
}
|
||||
}
|
||||
export const useMetaOperate = ({
|
||||
onSave,
|
||||
metaStore,
|
||||
handleFormDataChange,
|
||||
}: {
|
||||
onSave: () => void;
|
||||
metaStore: MetaStore;
|
||||
handleFormDataChange: (key: string, value: string | Date | null | string[]) => void;
|
||||
}) => {
|
||||
const { keys, setKeys, openPropertyModal, setOpenPropertyModal } = metaStore;
|
||||
const hasShare = keys.includes('share');
|
||||
const hasPassword = keys.includes('password');
|
||||
const addMeta = (key: string) => {
|
||||
setKeys(uniq([...keys, key]));
|
||||
};
|
||||
const defaultBtnList = [
|
||||
{
|
||||
icon: <Save />,
|
||||
key: 'save',
|
||||
tooltip: '保存元数据, 修改后需要手动保存',
|
||||
onClick: () => onSave(),
|
||||
},
|
||||
{
|
||||
icon: <Plus />,
|
||||
key: 'add',
|
||||
tooltip: '添加元数据',
|
||||
onClick: () => setOpenPropertyModal(true),
|
||||
},
|
||||
];
|
||||
if (!hasShare) {
|
||||
defaultBtnList.push({
|
||||
icon: <Share />,
|
||||
key: 'share',
|
||||
tooltip: '开启共享',
|
||||
onClick: () => addMeta('share'),
|
||||
});
|
||||
}
|
||||
if (hasShare && hasPassword) {
|
||||
defaultBtnList.push({
|
||||
icon: <Shuffle />,
|
||||
key: 'password',
|
||||
tooltip: '随机生成密码',
|
||||
onClick: () => {
|
||||
const password = Math.random().toString(36).substring(2, 8);
|
||||
handleFormDataChange('password', password);
|
||||
},
|
||||
});
|
||||
}
|
||||
return defaultBtnList;
|
||||
};
|
||||
type MetaStore = {
|
||||
keys: string[];
|
||||
setKeys: (keys: string[]) => void;
|
||||
openPropertyModal: boolean;
|
||||
setOpenPropertyModal: (openPropertyModal: boolean) => void;
|
||||
};
|
||||
export const useMetaStore = create<MetaStore>((set) => ({
|
||||
keys: [],
|
||||
setKeys: (keys) => set({ keys }),
|
||||
openPropertyModal: false,
|
||||
setOpenPropertyModal: (openPropertyModal) => set({ openPropertyModal }),
|
||||
}));
|
||||
export const MetaForm = () => {
|
||||
const { resource, updateMeta } = useResourceFileStore();
|
||||
const [formData, setFormData] = useState<any>({});
|
||||
const metaStore = useMetaStore();
|
||||
const { keys, setKeys } = metaStore;
|
||||
useEffect(() => {
|
||||
// setFormData(resource?.metaData || {});
|
||||
setFormData(KeyParse.parse(resource?.metaData || {}));
|
||||
}, [resource]);
|
||||
useEffect(() => {
|
||||
setKeys(Object.keys(resource?.metaData || {}));
|
||||
}, [resource]);
|
||||
if (!keys.length) {
|
||||
return <div className='text-center text-gray-500'>没有元数据</div>;
|
||||
}
|
||||
|
||||
const handleFormDataChange = (key: string, value: string | Date | null | string[]) => {
|
||||
// setFormData({ ...formData, [key]: value });
|
||||
const _formData = { ...formData };
|
||||
if (key === 'share') {
|
||||
const shareKeysOperate = setShareKeysOperate(value as 'public' | 'protected' | 'private');
|
||||
shareKeysOperate.forEach((item) => {
|
||||
if (item.operate === 'add') {
|
||||
_formData[item.key] = '';
|
||||
} else if (item.operate === 'delete') {
|
||||
delete _formData[item.key];
|
||||
}
|
||||
});
|
||||
_formData.share = value;
|
||||
setFormData(_formData);
|
||||
const newKeys = keys
|
||||
.map((item) => {
|
||||
const operate = shareKeysOperate.find((item2) => item2.key === item);
|
||||
if (operate && operate.operate === 'delete') {
|
||||
return null;
|
||||
}
|
||||
return item;
|
||||
})
|
||||
.filter((item) => item !== null);
|
||||
const addKeys = shareKeysOperate.filter((item) => item.operate === 'add').map((item) => item.key);
|
||||
const _newKeys = uniq([...newKeys, ...addKeys]);
|
||||
setKeys(_newKeys);
|
||||
console.log(_newKeys);
|
||||
return;
|
||||
} else {
|
||||
_formData[key] = value;
|
||||
}
|
||||
setFormData(_formData);
|
||||
};
|
||||
const deleteMeta = (key: string) => {
|
||||
setKeys(keys.filter((item) => item !== key));
|
||||
delete formData[key];
|
||||
setFormData({ ...formData });
|
||||
};
|
||||
const onSave = () => {
|
||||
const newMetadata = KeyParse.stringify(formData);
|
||||
updateMeta(newMetadata);
|
||||
};
|
||||
const addMetaKey = (key: string) => {
|
||||
if (keys.includes(key)) {
|
||||
toast.error('元数据key已存在');
|
||||
return;
|
||||
}
|
||||
formData[key] = '';
|
||||
setKeys([...keys, key]);
|
||||
setFormData({ ...formData });
|
||||
};
|
||||
const btnList = useMetaOperate({ onSave, metaStore, handleFormDataChange });
|
||||
|
||||
return (
|
||||
<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' }}>
|
||||
{btnList.map((item) => {
|
||||
const icon = (
|
||||
<IconButton color='secondary' onClick={item.onClick}>
|
||||
{item.icon}
|
||||
</IconButton>
|
||||
);
|
||||
if (item.tooltip) {
|
||||
return (
|
||||
<Tooltip key={item.key} title={item.tooltip} placement='top' arrow>
|
||||
{icon}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
return <>{icon}</>;
|
||||
})}
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</div>
|
||||
</Box>
|
||||
<FormGroup>
|
||||
{keys.map((key) => {
|
||||
let control: React.ReactNode | null = null;
|
||||
if (key === 'share') {
|
||||
control = <KeyShareSelect name={key} value={formData[key] || ''} onChange={(value) => handleFormDataChange(key, value)} />;
|
||||
} else if (key === 'expiration-time') {
|
||||
control = <DatePicker value={formData[key]} onChange={(date) => handleFormDataChange(key, date)} />;
|
||||
} else if (key === 'usernames') {
|
||||
control = <SelectPicker value={formData[key] || []} onChange={(value) => handleFormDataChange(key, value)} />;
|
||||
} else {
|
||||
control = <KeyTextField name={key} value={formData[key] || ''} onChange={(value) => handleFormDataChange(key, value)} />;
|
||||
}
|
||||
const Label = () => {
|
||||
const tip = keysTips.find((item) => item.key === key);
|
||||
return (
|
||||
<div className='flex justify-between items-center gap-2'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Typography variant='caption' color='primary'>
|
||||
{key}
|
||||
</Typography>
|
||||
{tip && (
|
||||
<Tooltip title={tip?.tips} placement='top' arrow>
|
||||
<Info size={12} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<IconButton color='error' onClick={() => deleteMeta(key)}>
|
||||
<Trash />
|
||||
</IconButton>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<div key={key} className='flex flex-col gap-2'>
|
||||
<FormControlLabel
|
||||
key={key}
|
||||
label={<Label />}
|
||||
labelPlacement='top'
|
||||
control={control}
|
||||
sx={{
|
||||
alignItems: 'flex-start',
|
||||
'& .MuiFormControlLabel-label': {
|
||||
textAlign: 'left',
|
||||
width: '100%',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</FormGroup>
|
||||
<DialogKey onAdd={addMetaKey} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const KeyTextField = ({ name, value, onChange }: { name: string; value: string; onChange?: (value: string) => void }) => {
|
||||
return (
|
||||
<TextField
|
||||
variant='outlined'
|
||||
size='small'
|
||||
name={name}
|
||||
defaultValue={value}
|
||||
// value={formData[key] || ''}
|
||||
onChange={(e) => onChange?.(e.target.value)}
|
||||
sx={{
|
||||
width: '100%',
|
||||
marginBottom: '16px',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
const KeyShareSelect = ({ name, value, onChange }: { name: string; value: string; onChange?: (value: string) => void }) => {
|
||||
return (
|
||||
<Select
|
||||
variant='outlined'
|
||||
size='small'
|
||||
name={name}
|
||||
value={value}
|
||||
onChange={(e) => onChange?.(e.target.value)}
|
||||
sx={{
|
||||
width: '100%',
|
||||
marginBottom: '16px',
|
||||
}}>
|
||||
<MenuItem value='public' title='公开'>
|
||||
公开
|
||||
</MenuItem>
|
||||
<MenuItem value='protected' title='受保护'>
|
||||
受保护
|
||||
</MenuItem>
|
||||
<MenuItem value='private' title='私有'>
|
||||
私有
|
||||
</MenuItem>
|
||||
</Select>
|
||||
);
|
||||
};
|
@ -0,0 +1,18 @@
|
||||
import { styled } from '@mui/material';
|
||||
import Select from 'antd/es/select';
|
||||
import 'antd/es/select/style/index';
|
||||
|
||||
interface SelectPickerProps {
|
||||
value: string[];
|
||||
onChange: (value: string[]) => void;
|
||||
}
|
||||
|
||||
export const SelectPickerCom = ({ value, onChange }: SelectPickerProps) => {
|
||||
return <Select style={{ width: '100%' }} showSearch={false} mode='tags' value={value} onChange={onChange} />;
|
||||
};
|
||||
|
||||
export const SelectPicker = styled(SelectPickerCom)({
|
||||
'& .ant-select-selector': {
|
||||
color: 'var(--primary-color)',
|
||||
},
|
||||
});
|
11
packages/resources/src/pages/file/draw/quick/QuickLink.tsx
Normal file
11
packages/resources/src/pages/file/draw/quick/QuickLink.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
/**
|
||||
* 链接生成,markdown超链接, 多选项。
|
||||
* 1. markdown
|
||||
* 2. HTML image
|
||||
* 3. URL https url
|
||||
* @returns
|
||||
*/
|
||||
export const QuickLink = () => {
|
||||
const url = 'https://ab.c.com/a/b/c.jpg';
|
||||
return <div>QuickLink</div>;
|
||||
};
|
131
packages/resources/src/pages/file/draw/quick/QuickPreview.tsx
Normal file
131
packages/resources/src/pages/file/draw/quick/QuickPreview.tsx
Normal file
@ -0,0 +1,131 @@
|
||||
import { useResourceFileStore } from '@/pages/store/resource-file';
|
||||
import { useSettingsStore } from '@/pages/store/settings';
|
||||
import { Button, Tooltip, useTheme } from '@mui/material';
|
||||
import { useShallow } from 'zustand/shallow';
|
||||
import { Accordion, AccordionSummary, AccordionDetails } from '@mui/material';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
import { useMemo } from 'react';
|
||||
import { getFileType } from '../../FileIcon';
|
||||
import { toast } from 'react-toastify';
|
||||
import clsx from 'clsx';
|
||||
type AccordionItem = {
|
||||
title?: string;
|
||||
key?: string;
|
||||
url?: string;
|
||||
content?: any;
|
||||
clickCopy?: boolean;
|
||||
};
|
||||
export const QuickPreview = () => {
|
||||
const { resource } = useResourceFileStore(
|
||||
useShallow((state) => ({
|
||||
resource: state.resource,
|
||||
})),
|
||||
);
|
||||
const { settings, baseUrl } = useSettingsStore(
|
||||
useShallow((state) => ({
|
||||
settings: state.settings,
|
||||
baseUrl: state.baseUrl,
|
||||
})),
|
||||
);
|
||||
const fileType = useMemo(() => getFileType(resource?.name), [resource]);
|
||||
const accordionList = useMemo(() => {
|
||||
const username = settings?.username;
|
||||
if (!username) {
|
||||
toast.error('请先登录');
|
||||
return [];
|
||||
}
|
||||
const _url = new URL(`${baseUrl}/api/s1/share/${username}/${resource?.name}`);
|
||||
const meta = resource?.metaData ?? {};
|
||||
if (meta.password) {
|
||||
_url.searchParams.set('p', meta.password);
|
||||
}
|
||||
const url = _url.toString();
|
||||
let accordionList: AccordionItem[] = [];
|
||||
const encodeUrl = encodeURIComponent(url);
|
||||
const previewUrl = `${baseUrl}/app/preview?fileUrl=${encodeUrl}&fileType=${fileType}`;
|
||||
accordionList.push({
|
||||
title: '文件预览',
|
||||
key: 'preview-file',
|
||||
url: previewUrl,
|
||||
content: (
|
||||
<div className=''>
|
||||
<div className='text-sm break-words'>{previewUrl}</div>
|
||||
<Button variant='contained' color='primary' style={{ color: 'white' }} onClick={() => window.open(previewUrl, '_blank')}>
|
||||
点击查看
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
|
||||
if (fileType === 'image') {
|
||||
accordionList.push({
|
||||
title: '预览图片',
|
||||
key: 'preview-image',
|
||||
url: url,
|
||||
content: <img className='w-full h-full border-2 border-gray-300 rounded-md' src={url} alt={resource?.name} />,
|
||||
});
|
||||
accordionList.push({
|
||||
title: 'markdown链接',
|
||||
key: 'markdown-link',
|
||||
url: url,
|
||||
clickCopy: true,
|
||||
content: ``,
|
||||
});
|
||||
accordionList.push({
|
||||
title: 'HTML图片',
|
||||
key: 'html-image',
|
||||
url: url,
|
||||
clickCopy: true,
|
||||
content: `<img style="width: 100%;height: 100%;" src="${url}" alt="${resource?.name}" />`,
|
||||
});
|
||||
accordionList.push({
|
||||
title: 'HTML超链接',
|
||||
key: 'html-link',
|
||||
url: url,
|
||||
clickCopy: true,
|
||||
content: `<a href="${url}">${resource?.name}</a>`,
|
||||
});
|
||||
}
|
||||
const downloadUrl = new URL(url);
|
||||
downloadUrl.searchParams.set('download', 'true');
|
||||
accordionList.push({
|
||||
title: '下载地址',
|
||||
key: 'download-url',
|
||||
url: downloadUrl.toString(),
|
||||
content: (
|
||||
<div className=''>
|
||||
<div className='text-sm break-words'>{downloadUrl.toString()}</div>
|
||||
<Button variant='contained' color='primary' style={{ color: 'white' }} onClick={() => window.open(downloadUrl.toString(), '_blank')}>
|
||||
点击下载
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
|
||||
return accordionList;
|
||||
}, [resource, baseUrl, settings]);
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<div className='p-4'>
|
||||
{accordionList.map((item) => (
|
||||
<Accordion key={item.key}>
|
||||
<AccordionSummary expandIcon={<ChevronDown color={theme.palette.text.secondary} />}>{item.title}</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<div
|
||||
className={clsx('text-sm whitespace-pre-wrap w-full overflow-ellipsis overflow-hidden', {
|
||||
'cursor-copy': item.clickCopy, // cursor-copy的有吗
|
||||
})}
|
||||
onClick={() => {
|
||||
if (item.clickCopy) {
|
||||
navigator.clipboard.writeText(item.content);
|
||||
toast.success('复制成功');
|
||||
}
|
||||
}}>
|
||||
{item.content}
|
||||
</div>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
41
packages/resources/src/pages/file/draw/quick/index.tsx
Normal file
41
packages/resources/src/pages/file/draw/quick/index.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import { useResourceFileStore } from '@/pages/store/resource-file';
|
||||
import { useShallow } from 'zustand/shallow';
|
||||
import { QuickPreview } from './QuickPreview';
|
||||
import { useMemo } from 'react';
|
||||
import { getFileType } from '../../FileIcon';
|
||||
|
||||
const QuickModules = [
|
||||
{
|
||||
key: 'link',
|
||||
type: 'link',
|
||||
categroy: ['image', 'video', 'audio'],
|
||||
tooltips: `链接生成,markdown超链接`,
|
||||
component: () => <QuickPreview />,
|
||||
},
|
||||
{
|
||||
key: 'preview',
|
||||
type: 'all',
|
||||
tooltips: `预览页面内容`,
|
||||
component: () => <QuickPreview />,
|
||||
},
|
||||
];
|
||||
export const Quick = () => {
|
||||
const { resource } = useResourceFileStore(
|
||||
useShallow((state) => ({
|
||||
resource: state.resource,
|
||||
})),
|
||||
);
|
||||
const quickModule = useMemo(() => {
|
||||
const fileType = getFileType(resource?.name);
|
||||
const allCanUseModule = QuickModules.filter((item) => item.type === 'all');
|
||||
if (!fileType) {
|
||||
return allCanUseModule;
|
||||
}
|
||||
return QuickModules.filter((item) => item.type === fileType);
|
||||
}, [resource]);
|
||||
return (
|
||||
<>
|
||||
<QuickPreview />
|
||||
</>
|
||||
);
|
||||
};
|
@ -11,14 +11,35 @@ export const FileTable = () => {
|
||||
const { list, prefix, download, onOpenPrefix, deleteFile } = useResourceStore();
|
||||
const { setOpenDrawer, setPrefix } = useResourceFileStore();
|
||||
return (
|
||||
<TableContainer component={Paper}>
|
||||
<Table sx={{ minWidth: 650 }} aria-label='simple table'>
|
||||
<TableContainer
|
||||
className='scrollbar'
|
||||
sx={{
|
||||
'&': {
|
||||
// scrollbarWidth: 'none',
|
||||
// scrollbarColor: '#888 #fff',
|
||||
},
|
||||
'&::-webkit-scrollbar': {
|
||||
width: '4px !important',
|
||||
height: '4px !important',
|
||||
background: '#fff',
|
||||
},
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
background: '#888',
|
||||
borderRadius: '2px',
|
||||
},
|
||||
}}
|
||||
component={Paper}>
|
||||
<Table
|
||||
sx={{
|
||||
minWidth: 650,
|
||||
}}
|
||||
aria-label='simple table'>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Name</TableCell>
|
||||
<TableCell sx={{ maxWidth: 100 }}>Size</TableCell>
|
||||
<TableCell sx={{ maxWidth: 100 }}>Last Modified</TableCell>
|
||||
<TableCell sx={{ maxWidth: 100 }}>Actions</TableCell>
|
||||
<TableCell sx={{ minWidth: 100 }}>Size</TableCell>
|
||||
<TableCell sx={{ minWidth: 180 }}>Last Modified</TableCell>
|
||||
<TableCell sx={{ minWidth: 110 }}>Actions</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { App } from './App.tsx';
|
||||
import { bootstrap } from './Bootstrap';
|
||||
|
||||
createRoot(document.getElementById('ai-root')!).render(<App />);
|
||||
bootstrap('#ai-root');
|
||||
|
@ -4,8 +4,11 @@ import { toast } from 'react-toastify';
|
||||
const LoginMessage = () => {
|
||||
const handleClick = () => {
|
||||
const currentUrl = window.location.href;
|
||||
console.log(currentUrl);
|
||||
const redirect = encodeURIComponent(currentUrl);
|
||||
window.location.href = '/user/login?redirect=' + redirect;
|
||||
console.log('redirect', redirect);
|
||||
const newUrl = location.origin + '/user/login/?redirect=' + redirect;
|
||||
window.open(newUrl, '_self');
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -40,7 +40,6 @@ export const Settings = () => {
|
||||
}, [settings]);
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
let { name, value } = e.target;
|
||||
console.log(name, value, e.target);
|
||||
name = name.toLowerCase();
|
||||
setConfig({ ...config, [name]: value });
|
||||
};
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { create } from 'zustand';
|
||||
import { Resource } from './resource';
|
||||
import { query } from '@/modules/query';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
interface ResourceFileStore {
|
||||
resource: Resource | null;
|
||||
@ -10,6 +11,7 @@ interface ResourceFileStore {
|
||||
prefix: string;
|
||||
setPrefix: (prefix: string, replace?: string) => void;
|
||||
getStatFile: () => Promise<any>;
|
||||
updateMeta: (metadata: any) => Promise<any>;
|
||||
}
|
||||
|
||||
export const useResourceFileStore = create<ResourceFileStore>((set, get) => ({
|
||||
@ -32,4 +34,21 @@ export const useResourceFileStore = create<ResourceFileStore>((set, get) => ({
|
||||
set({ resource: { ...res.data, name: prefix } });
|
||||
}
|
||||
},
|
||||
updateMeta: async (metadata: any) => {
|
||||
const { resource, getStatFile } = get();
|
||||
const res = await query.post({
|
||||
path: 'file',
|
||||
key: 'update-metadata',
|
||||
data: {
|
||||
prefix: resource?.name,
|
||||
metadata: metadata,
|
||||
},
|
||||
});
|
||||
if (res.code === 200) {
|
||||
// set({ resource: { ...res.data, name: resource?.name } });
|
||||
getStatFile();
|
||||
} else {
|
||||
toast.error(res.message || '更新元数据失败');
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
@ -2,6 +2,7 @@ import { create } from 'zustand';
|
||||
import { query } from '@/modules/query';
|
||||
import { sortBy } from 'lodash-es';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useSettingsStore } from './settings';
|
||||
|
||||
export type Resource = {
|
||||
name: string;
|
||||
@ -67,9 +68,16 @@ export const useResourceStore = create<ResourceStore>((set, get) => ({
|
||||
}
|
||||
},
|
||||
download: (resource: Resource) => {
|
||||
const { prefix } = get();
|
||||
const url = `${prefix}/${resource.name}`;
|
||||
window.open(url, '_blank');
|
||||
const { baseUrl, settings } = useSettingsStore.getState();
|
||||
const username = settings?.username;
|
||||
const _url = new URL(`${baseUrl}/api/s1/share/${username}/${resource?.name}`);
|
||||
_url.searchParams.set('download', 'true');
|
||||
const downloadUrl = _url.toString();
|
||||
console.log('downloadUrl', downloadUrl);
|
||||
const a = document.createElement('a');
|
||||
a.href = downloadUrl;
|
||||
a.download = resource.name;
|
||||
a.click();
|
||||
},
|
||||
listType: 'table',
|
||||
setListType: (listType: 'table' | 'card') => {
|
||||
|
@ -12,16 +12,18 @@ interface SettingsStore {
|
||||
settings: Settings;
|
||||
setSettings: (settings: Settings) => void;
|
||||
querySettings: () => Promise<void>;
|
||||
mounted: boolean;
|
||||
setMounted: (mounted: boolean) => void;
|
||||
mounted: 'loading' | 'error' | 'success';
|
||||
setMounted: (mounted: 'loading' | 'error' | 'success') => void;
|
||||
init: () => Promise<void>;
|
||||
updateSettings: (settings: Settings) => void;
|
||||
baseUrl: string;
|
||||
setBaseUrl: (baseUrl: string) => void;
|
||||
}
|
||||
|
||||
export const useSettingsStore = create<SettingsStore>((set, get) => ({
|
||||
settings: {},
|
||||
setSettings: (settings) => set({ settings }),
|
||||
mounted: false,
|
||||
mounted: 'loading',
|
||||
setMounted: (mounted) => set({ mounted }),
|
||||
querySettings: async () => {
|
||||
const settings = get().settings;
|
||||
@ -29,37 +31,48 @@ export const useSettingsStore = create<SettingsStore>((set, get) => ({
|
||||
path: 'config',
|
||||
key: 'getUploadConfig',
|
||||
});
|
||||
console.log(res);
|
||||
if (res.code === 200) {
|
||||
const config = res.data;
|
||||
localStorage.setItem('upload-config', JSON.stringify(config));
|
||||
set({ settings: config });
|
||||
if (JSON.stringify(settings) !== JSON.stringify(config)) {
|
||||
set({ mounted: false });
|
||||
set({ mounted: true });
|
||||
set({ mounted: 'success' });
|
||||
}
|
||||
} else {
|
||||
toast.error(res.message || '获取配置失败');
|
||||
!res.noMsg && toast.error(res.message || '获取配置失败');
|
||||
set({ mounted: 'error' });
|
||||
}
|
||||
},
|
||||
updateSettings: async (settings) => {
|
||||
console.log('setinng', settings);
|
||||
// const res = await query.post({
|
||||
// path: 'config',
|
||||
// key: 'updateUploadConfig',
|
||||
// data: settings,
|
||||
// });
|
||||
const res = await query.post({
|
||||
path: 'config',
|
||||
key: 'updateUploadConfig',
|
||||
data: {
|
||||
key: settings.key,
|
||||
version: settings.version,
|
||||
},
|
||||
});
|
||||
if (res.code === 200) {
|
||||
toast.success('更新配置成功');
|
||||
get().querySettings();
|
||||
} else {
|
||||
toast.error(res.message || '更新配置失败');
|
||||
}
|
||||
},
|
||||
init: async () => {
|
||||
const cacheConfig = localStorage.getItem('upload-config');
|
||||
if (cacheConfig) {
|
||||
try {
|
||||
set({ settings: JSON.parse(cacheConfig), mounted: true });
|
||||
set({ settings: JSON.parse(cacheConfig), mounted: 'success' });
|
||||
} catch (error) {
|
||||
toast.error('配置文件损坏');
|
||||
localStorage.removeItem('upload-config');
|
||||
}
|
||||
}
|
||||
get().querySettings();
|
||||
const baseUrl = location.origin;
|
||||
set({ baseUrl });
|
||||
},
|
||||
baseUrl: '',
|
||||
setBaseUrl: (baseUrl) => set({ baseUrl }),
|
||||
}));
|
||||
|
@ -3,7 +3,7 @@ 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 }) => {
|
||||
export const UploadButton = (props: { prefix?: string; onUpload?: (res: any) => void; hasDirectory?: boolean; uploadDirectory?: boolean }) => {
|
||||
const onDrop = async (acceptedFiles) => {
|
||||
console.log(acceptedFiles);
|
||||
if (acceptedFiles.length > 1) {
|
||||
@ -27,11 +27,18 @@ export const UploadButton = (props: { prefix?: string; onUpload?: (res: any) =>
|
||||
}}>
|
||||
<UploadIcon />
|
||||
</Button>
|
||||
<input type='file' style={{ display: 'none' }} {...getInputProps()} />
|
||||
<input
|
||||
type='file'
|
||||
style={{ display: 'none' }}
|
||||
{...getInputProps()}
|
||||
// @ts-ignore
|
||||
webkitdirectory={props.uploadDirectory ? 'true' : undefined}
|
||||
mozdirectory={props.uploadDirectory ? 'true' : undefined}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
export const Upload = () => {
|
||||
export const Upload = ({ uploadDirectory = false }: { uploadDirectory?: boolean }) => {
|
||||
const onDrop = async (acceptedFiles) => {
|
||||
console.log(acceptedFiles);
|
||||
if (acceptedFiles.length > 1) {
|
||||
@ -87,7 +94,14 @@ export const Upload = () => {
|
||||
}}>
|
||||
<UploadIcon style={{ width: '48px', height: '48px', color: theme.palette.secondary.main }} />
|
||||
<label style={{ cursor: 'pointer' }}>
|
||||
<input type='file' style={{ display: 'none' }} {...getInputProps()} />
|
||||
<input
|
||||
type='file'
|
||||
style={{ display: 'none' }}
|
||||
{...getInputProps()}
|
||||
// @ts-ignore
|
||||
webkitdirectory={uploadDirectory ? 'true' : undefined}
|
||||
mozdirectory={uploadDirectory ? 'true' : undefined}
|
||||
/>
|
||||
<Box sx={{ color: theme.palette.secondary.main, '&:hover': { color: theme.palette.primary.main } }}>Click to upload or drag and drop</Box>
|
||||
</label>
|
||||
</Box>
|
||||
|
@ -14,12 +14,25 @@ export const uploadFiles = async (files: File[], opts: ConvertOpts) => {
|
||||
const { directory } = opts;
|
||||
return new Promise((resolve, reject) => {
|
||||
const formData = new FormData();
|
||||
const webkitRelativePath = files[0]?.webkitRelativePath;
|
||||
const keepDirectory = webkitRelativePath !== '';
|
||||
const root = keepDirectory ? webkitRelativePath.split('/')[0] : '';
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
formData.append('file', files[i], files[i].name);
|
||||
const file = files[i];
|
||||
if (keepDirectory) {
|
||||
// relativePath 去除第一级
|
||||
const webkitRelativePath = file.webkitRelativePath.replace(root + '/', '');
|
||||
formData.append('file', file, webkitRelativePath); // 保留文件夹路径
|
||||
} else {
|
||||
formData.append('file', files[i], files[i].name);
|
||||
}
|
||||
}
|
||||
if (directory) {
|
||||
formData.append('directory', directory);
|
||||
}
|
||||
console.log('formData', formData, files);
|
||||
resolve(null);
|
||||
return;
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) {
|
||||
toastLogin();
|
||||
|
@ -15,6 +15,7 @@ if (true) {
|
||||
target: 'https://kevisual.silkyai.cn',
|
||||
changeOrigin: true,
|
||||
ws: true,
|
||||
cookieDomainRewrite: 'localhost',
|
||||
rewrite: (path) => path.replace(/^\/api/, '/api'),
|
||||
},
|
||||
'/api/router': {
|
||||
@ -27,6 +28,7 @@ if (true) {
|
||||
'/user/login': {
|
||||
target: 'https://kevisual.silkyai.cn',
|
||||
changeOrigin: true,
|
||||
cookieDomainRewrite: 'localhost',
|
||||
rewrite: (path) => path.replace(/^\/user/, '/user'),
|
||||
},
|
||||
};
|
||||
@ -41,12 +43,27 @@ export default defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
// 'react/jsx-dev-runtime': 'https://cdn.jsdelivr.net/npm/react/jsx-dev-runtime/+esm',
|
||||
// 'react/jsx-runtime': 'https://cdn.jsdelivr.net/npm/react/jsx-runtime/+esm',
|
||||
// 'react/jsx-runtime': path.resolve(__dirname, './node_modules/react/jsx-runtime'),
|
||||
// 'react/jsx-dev-runtime': path.resolve(__dirname, './node_modules/react/jsx-dev-runtime'),
|
||||
// 'react-dom/client': 'https://cdn.jsdelivr.net/npm/react-dom/client/+esm',
|
||||
// react: 'https://cdn.jsdelivr.net/npm/react@19.0.0/+esm',
|
||||
// 'react-dom': 'https://cdn.jsdelivr.net/npm/react-dom@19.0.0/+esm',
|
||||
|
||||
},
|
||||
},
|
||||
define: {
|
||||
DEV_SERVER: JSON.stringify(process.env.NODE_ENV === 'development'),
|
||||
BASE_NAME: JSON.stringify('/root/resources/'),
|
||||
},
|
||||
base: './',
|
||||
// base: isDev ? '/' : '/root/resources/',
|
||||
build: {
|
||||
rollupOptions: {
|
||||
// external: ['react', 'react-dom'],
|
||||
},
|
||||
},
|
||||
base: isDev ? '/' : '/root/resources/',
|
||||
server: {
|
||||
port: 6022,
|
||||
host: '0.0.0.0',
|
||||
@ -59,6 +76,7 @@ export default defineConfig({
|
||||
target: 'http://localhost:4005',
|
||||
changeOrigin: true,
|
||||
ws: true,
|
||||
cookieDomainRewrite: 'localhost',
|
||||
rewrite: (path) => path.replace(/^\/api/, '/api'),
|
||||
},
|
||||
'/api/router': {
|
||||
@ -68,12 +86,6 @@ export default defineConfig({
|
||||
rewriteWsOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, '/api'),
|
||||
},
|
||||
'/api/s1/events': {
|
||||
target: 'https://kevisual.silkyai.cn',
|
||||
changeOrigin: true,
|
||||
ws: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, '/api'),
|
||||
},
|
||||
...proxy,
|
||||
},
|
||||
},
|
||||
|
256
pnpm-lock.yaml
generated
256
pnpm-lock.yaml
generated
@ -52,7 +52,7 @@ importers:
|
||||
version: 12.4.4(@types/react@19.0.10)(immer@10.1.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
antd:
|
||||
specifier: ^5.24.3
|
||||
version: 5.24.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
version: 5.24.3(date-fns@4.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
classnames:
|
||||
specifier: ^2.5.1
|
||||
version: 2.5.1
|
||||
@ -227,8 +227,8 @@ importers:
|
||||
specifier: ^0.0.2
|
||||
version: 0.0.2(rollup@4.34.7)
|
||||
'@mui/material':
|
||||
specifier: ^6.4.7
|
||||
version: 6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
specifier: ^6.4.8
|
||||
version: 6.4.8(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
'@types/lodash-es':
|
||||
specifier: ^4.17.12
|
||||
version: 4.17.12
|
||||
@ -251,8 +251,8 @@ importers:
|
||||
specifier: ^0.482.0
|
||||
version: 0.482.0(react@19.0.0)
|
||||
nanoid:
|
||||
specifier: ^5.1.3
|
||||
version: 5.1.3
|
||||
specifier: ^5.1.4
|
||||
version: 5.1.4
|
||||
nprogress:
|
||||
specifier: ^0.2.0
|
||||
version: 0.2.0
|
||||
@ -262,6 +262,9 @@ importers:
|
||||
react:
|
||||
specifier: 19.0.0
|
||||
version: 19.0.0
|
||||
react-datepicker:
|
||||
specifier: ^8.2.1
|
||||
version: 8.2.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
react-dom:
|
||||
specifier: 19.0.0
|
||||
version: 19.0.0(react@19.0.0)
|
||||
@ -685,6 +688,27 @@ packages:
|
||||
resolution: {integrity: sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@floating-ui/core@1.6.9':
|
||||
resolution: {integrity: sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==}
|
||||
|
||||
'@floating-ui/dom@1.6.13':
|
||||
resolution: {integrity: sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==}
|
||||
|
||||
'@floating-ui/react-dom@2.1.2':
|
||||
resolution: {integrity: sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==}
|
||||
peerDependencies:
|
||||
react: '>=16.8.0'
|
||||
react-dom: '>=16.8.0'
|
||||
|
||||
'@floating-ui/react@0.27.5':
|
||||
resolution: {integrity: sha512-BX3jKxo39Ba05pflcQmqPPwc0qdNsdNi/eweAFtoIdrJWNen2sVEWMEac3i6jU55Qfx+lOcdMNKYn2CtWmlnOQ==}
|
||||
peerDependencies:
|
||||
react: '>=17.0.0'
|
||||
react-dom: '>=17.0.0'
|
||||
|
||||
'@floating-ui/utils@0.2.9':
|
||||
resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==}
|
||||
|
||||
'@humanfs/core@0.19.1':
|
||||
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
|
||||
engines: {node: '>=18.18.0'}
|
||||
@ -777,6 +801,9 @@ packages:
|
||||
'@mui/core-downloads-tracker@6.4.7':
|
||||
resolution: {integrity: sha512-XjJrKFNt9zAKvcnoIIBquXyFyhfrHYuttqMsoDS7lM7VwufYG4fAPw4kINjBFg++fqXM2BNAuWR9J7XVIuKIKg==}
|
||||
|
||||
'@mui/core-downloads-tracker@6.4.8':
|
||||
resolution: {integrity: sha512-vjP4+A1ybyCRhDZC7r5EPWu/gLseFZxaGyPdDl94vzVvk6Yj6gahdaqcjbhkaCrJjdZj90m3VioltWPAnWF/zw==}
|
||||
|
||||
'@mui/material@6.4.7':
|
||||
resolution: {integrity: sha512-K65StXUeGAtFJ4ikvHKtmDCO5Ab7g0FZUu2J5VpoKD+O6Y3CjLYzRi+TMlI3kaL4CL158+FccMoOd/eaddmeRQ==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
@ -797,6 +824,26 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@mui/material@6.4.8':
|
||||
resolution: {integrity: sha512-5S9UTjKZZBd9GfbcYh/nYfD9cv6OXmj5Y7NgKYfk7JcSoshp8/pW5zP4wecRiroBSZX8wcrywSgogpVNO+5W0Q==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
peerDependencies:
|
||||
'@emotion/react': ^11.5.0
|
||||
'@emotion/styled': ^11.3.0
|
||||
'@mui/material-pigment-css': ^6.4.8
|
||||
'@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
react: ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
peerDependenciesMeta:
|
||||
'@emotion/react':
|
||||
optional: true
|
||||
'@emotion/styled':
|
||||
optional: true
|
||||
'@mui/material-pigment-css':
|
||||
optional: true
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@mui/private-theming@6.4.6':
|
||||
resolution: {integrity: sha512-T5FxdPzCELuOrhpA2g4Pi6241HAxRwZudzAuL9vBvniuB5YU82HCmrARw32AuCiyTfWzbrYGGpZ4zyeqqp9RvQ==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
@ -807,6 +854,16 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@mui/private-theming@6.4.8':
|
||||
resolution: {integrity: sha512-sWwQoNSn6elsPTAtSqCf+w5aaGoh7AASURNmpy+QTTD/zwJ0Jgwt0ZaaP6mXq2IcgHxYnYloM/+vJgHPMkRKTQ==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
peerDependencies:
|
||||
'@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
react: ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@mui/styled-engine@6.4.6':
|
||||
resolution: {integrity: sha512-vSWYc9ZLX46be5gP+FCzWVn5rvDr4cXC5JBZwSIkYk9xbC7GeV+0kCvB8Q6XLFQJy+a62bbqtmdwS4Ghi9NBlQ==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
@ -820,6 +877,19 @@ packages:
|
||||
'@emotion/styled':
|
||||
optional: true
|
||||
|
||||
'@mui/styled-engine@6.4.8':
|
||||
resolution: {integrity: sha512-oyjx1b1FvUCI85ZMO4trrjNxGm90eLN3Ohy0AP/SqK5gWvRQg1677UjNf7t6iETOKAleHctJjuq0B3aXO2gtmw==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
peerDependencies:
|
||||
'@emotion/react': ^11.4.1
|
||||
'@emotion/styled': ^11.3.0
|
||||
react: ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
peerDependenciesMeta:
|
||||
'@emotion/react':
|
||||
optional: true
|
||||
'@emotion/styled':
|
||||
optional: true
|
||||
|
||||
'@mui/system@6.4.7':
|
||||
resolution: {integrity: sha512-7wwc4++Ak6tGIooEVA9AY7FhH2p9fvBMORT4vNLMAysH3Yus/9B9RYMbrn3ANgsOyvT3Z7nE+SP8/+3FimQmcg==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
@ -836,6 +906,22 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@mui/system@6.4.8':
|
||||
resolution: {integrity: sha512-gV7iBHoqlsIenU2BP0wq14BefRoZcASZ/4LeyuQglayBl+DfLX5rEd3EYR3J409V2EZpR0NOM1LATAGlNk2cyA==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
peerDependencies:
|
||||
'@emotion/react': ^11.5.0
|
||||
'@emotion/styled': ^11.3.0
|
||||
'@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
react: ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
peerDependenciesMeta:
|
||||
'@emotion/react':
|
||||
optional: true
|
||||
'@emotion/styled':
|
||||
optional: true
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@mui/types@7.2.21':
|
||||
resolution: {integrity: sha512-6HstngiUxNqLU+/DPqlUJDIPbzUBxIVHb1MmXP0eTWDIROiCR2viugXpEif0PPe2mLqqakPzzRClWAnK+8UJww==}
|
||||
peerDependencies:
|
||||
@ -844,6 +930,14 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@mui/types@7.2.24':
|
||||
resolution: {integrity: sha512-3c8tRt/CbWZ+pEg7QpSwbdxOk36EfmhbKf6AGZsD1EcLDLTSZoxxJ86FVtcjxvjuhdyBiWKSTGZFaXCnidO2kw==}
|
||||
peerDependencies:
|
||||
'@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@mui/utils@6.4.6':
|
||||
resolution: {integrity: sha512-43nZeE1pJF2anGafNydUcYFPtHwAqiBiauRtaMvurdrZI3YrUjHkAu43RBsxef7OFtJMXGiHFvq43kb7lig0sA==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
@ -854,6 +948,16 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@mui/utils@6.4.8':
|
||||
resolution: {integrity: sha512-C86gfiZ5BfZ51KqzqoHi1WuuM2QdSKoFhbkZeAfQRB+jCc4YNhhj11UXFVMMsqBgZ+Zy8IHNJW3M9Wj/LOwRXQ==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
peerDependencies:
|
||||
'@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
react: ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@nodelib/fs.scandir@2.1.5':
|
||||
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
||||
engines: {node: '>= 8'}
|
||||
@ -1676,6 +1780,9 @@ packages:
|
||||
resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
date-fns@4.1.0:
|
||||
resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==}
|
||||
|
||||
dayjs@1.11.13:
|
||||
resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==}
|
||||
|
||||
@ -2189,6 +2296,11 @@ packages:
|
||||
engines: {node: ^18 || >=20}
|
||||
hasBin: true
|
||||
|
||||
nanoid@5.1.4:
|
||||
resolution: {integrity: sha512-GTFcMIDgR7tqji/LpSY8rtg464VnJl/j6ypoehYnuGb+Y8qZUdtKB8WVCXon0UEZgFDbuUxpIl//6FHLHgXSNA==}
|
||||
engines: {node: ^18 || >=20}
|
||||
hasBin: true
|
||||
|
||||
natural-compare@1.4.0:
|
||||
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
||||
|
||||
@ -2550,6 +2662,12 @@ packages:
|
||||
react: '>=16.9.0'
|
||||
react-dom: '>=16.9.0'
|
||||
|
||||
react-datepicker@8.2.1:
|
||||
resolution: {integrity: sha512-1pyALWM9mTZ7DG7tfcApwBy2kkld9Kz/EI++LhPnoXJAASbvuq6fdsDfkoB3q1JrxF7vhghVmQ759H/rOwUNNw==}
|
||||
peerDependencies:
|
||||
react: ^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc
|
||||
react-dom: ^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc
|
||||
|
||||
react-dom@19.0.0:
|
||||
resolution: {integrity: sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==}
|
||||
peerDependencies:
|
||||
@ -2767,6 +2885,9 @@ packages:
|
||||
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
tabbable@6.2.0:
|
||||
resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==}
|
||||
|
||||
tailwind-merge@3.0.2:
|
||||
resolution: {integrity: sha512-l7z+OYZ7mu3DTqrL88RiKrKIqO3NcpEO8V/Od04bNpvk0kiIFndGEoqfuzvj4yuhRkHKjRkII2z+KS2HfPcSxw==}
|
||||
|
||||
@ -3432,6 +3553,31 @@ snapshots:
|
||||
'@eslint/core': 0.12.0
|
||||
levn: 0.4.1
|
||||
|
||||
'@floating-ui/core@1.6.9':
|
||||
dependencies:
|
||||
'@floating-ui/utils': 0.2.9
|
||||
|
||||
'@floating-ui/dom@1.6.13':
|
||||
dependencies:
|
||||
'@floating-ui/core': 1.6.9
|
||||
'@floating-ui/utils': 0.2.9
|
||||
|
||||
'@floating-ui/react-dom@2.1.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
|
||||
dependencies:
|
||||
'@floating-ui/dom': 1.6.13
|
||||
react: 19.0.0
|
||||
react-dom: 19.0.0(react@19.0.0)
|
||||
|
||||
'@floating-ui/react@0.27.5(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
|
||||
dependencies:
|
||||
'@floating-ui/react-dom': 2.1.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
'@floating-ui/utils': 0.2.9
|
||||
react: 19.0.0
|
||||
react-dom: 19.0.0(react@19.0.0)
|
||||
tabbable: 6.2.0
|
||||
|
||||
'@floating-ui/utils@0.2.9': {}
|
||||
|
||||
'@humanfs/core@0.19.1': {}
|
||||
|
||||
'@humanfs/node@0.16.6':
|
||||
@ -3479,7 +3625,7 @@ snapshots:
|
||||
'@emotion/css': 11.13.4
|
||||
crypto-js: 4.2.0
|
||||
eventemitter3: 5.0.1
|
||||
nanoid: 5.1.3
|
||||
nanoid: 5.1.4
|
||||
rollup-plugin-dts: 6.1.1(rollup@4.34.7)(typescript@5.8.2)
|
||||
scheduler: 0.23.2
|
||||
zustand: 4.5.5(@types/react@19.0.10)(immer@10.1.1)(react@19.0.0)
|
||||
@ -3548,6 +3694,8 @@ snapshots:
|
||||
|
||||
'@mui/core-downloads-tracker@6.4.7': {}
|
||||
|
||||
'@mui/core-downloads-tracker@6.4.8': {}
|
||||
|
||||
'@mui/material@6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.0
|
||||
@ -3569,6 +3717,27 @@ snapshots:
|
||||
'@emotion/styled': 11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0)
|
||||
'@types/react': 19.0.10
|
||||
|
||||
'@mui/material@6.4.8(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.0
|
||||
'@mui/core-downloads-tracker': 6.4.8
|
||||
'@mui/system': 6.4.8(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0)
|
||||
'@mui/types': 7.2.24(@types/react@19.0.10)
|
||||
'@mui/utils': 6.4.8(@types/react@19.0.10)(react@19.0.0)
|
||||
'@popperjs/core': 2.11.8
|
||||
'@types/react-transition-group': 4.4.12(@types/react@19.0.10)
|
||||
clsx: 2.1.1
|
||||
csstype: 3.1.3
|
||||
prop-types: 15.8.1
|
||||
react: 19.0.0
|
||||
react-dom: 19.0.0(react@19.0.0)
|
||||
react-is: 19.0.0
|
||||
react-transition-group: 4.4.5(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
optionalDependencies:
|
||||
'@emotion/react': 11.14.0(@types/react@19.0.10)(react@19.0.0)
|
||||
'@emotion/styled': 11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0)
|
||||
'@types/react': 19.0.10
|
||||
|
||||
'@mui/private-theming@6.4.6(@types/react@19.0.10)(react@19.0.0)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.0
|
||||
@ -3578,6 +3747,15 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/react': 19.0.10
|
||||
|
||||
'@mui/private-theming@6.4.8(@types/react@19.0.10)(react@19.0.0)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.0
|
||||
'@mui/utils': 6.4.8(@types/react@19.0.10)(react@19.0.0)
|
||||
prop-types: 15.8.1
|
||||
react: 19.0.0
|
||||
optionalDependencies:
|
||||
'@types/react': 19.0.10
|
||||
|
||||
'@mui/styled-engine@6.4.6(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(react@19.0.0)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.0
|
||||
@ -3591,6 +3769,19 @@ snapshots:
|
||||
'@emotion/react': 11.14.0(@types/react@19.0.10)(react@19.0.0)
|
||||
'@emotion/styled': 11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0)
|
||||
|
||||
'@mui/styled-engine@6.4.8(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(react@19.0.0)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.0
|
||||
'@emotion/cache': 11.14.0
|
||||
'@emotion/serialize': 1.3.3
|
||||
'@emotion/sheet': 1.4.0
|
||||
csstype: 3.1.3
|
||||
prop-types: 15.8.1
|
||||
react: 19.0.0
|
||||
optionalDependencies:
|
||||
'@emotion/react': 11.14.0(@types/react@19.0.10)(react@19.0.0)
|
||||
'@emotion/styled': 11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0)
|
||||
|
||||
'@mui/system@6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.0
|
||||
@ -3607,10 +3798,30 @@ snapshots:
|
||||
'@emotion/styled': 11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0)
|
||||
'@types/react': 19.0.10
|
||||
|
||||
'@mui/system@6.4.8(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.0
|
||||
'@mui/private-theming': 6.4.8(@types/react@19.0.10)(react@19.0.0)
|
||||
'@mui/styled-engine': 6.4.8(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(react@19.0.0)
|
||||
'@mui/types': 7.2.24(@types/react@19.0.10)
|
||||
'@mui/utils': 6.4.8(@types/react@19.0.10)(react@19.0.0)
|
||||
clsx: 2.1.1
|
||||
csstype: 3.1.3
|
||||
prop-types: 15.8.1
|
||||
react: 19.0.0
|
||||
optionalDependencies:
|
||||
'@emotion/react': 11.14.0(@types/react@19.0.10)(react@19.0.0)
|
||||
'@emotion/styled': 11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0)
|
||||
'@types/react': 19.0.10
|
||||
|
||||
'@mui/types@7.2.21(@types/react@19.0.10)':
|
||||
optionalDependencies:
|
||||
'@types/react': 19.0.10
|
||||
|
||||
'@mui/types@7.2.24(@types/react@19.0.10)':
|
||||
optionalDependencies:
|
||||
'@types/react': 19.0.10
|
||||
|
||||
'@mui/utils@6.4.6(@types/react@19.0.10)(react@19.0.0)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.0
|
||||
@ -3623,6 +3834,18 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/react': 19.0.10
|
||||
|
||||
'@mui/utils@6.4.8(@types/react@19.0.10)(react@19.0.0)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.0
|
||||
'@mui/types': 7.2.24(@types/react@19.0.10)
|
||||
'@types/prop-types': 15.7.14
|
||||
clsx: 2.1.1
|
||||
prop-types: 15.8.1
|
||||
react: 19.0.0
|
||||
react-is: 19.0.0
|
||||
optionalDependencies:
|
||||
'@types/react': 19.0.10
|
||||
|
||||
'@nodelib/fs.scandir@2.1.5':
|
||||
dependencies:
|
||||
'@nodelib/fs.stat': 2.0.5
|
||||
@ -4187,7 +4410,7 @@ snapshots:
|
||||
dependencies:
|
||||
color-convert: 2.0.1
|
||||
|
||||
antd@5.24.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
|
||||
antd@5.24.3(date-fns@4.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
|
||||
dependencies:
|
||||
'@ant-design/colors': 7.2.0
|
||||
'@ant-design/cssinjs': 1.23.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
@ -4219,7 +4442,7 @@ snapshots:
|
||||
rc-motion: 2.9.5(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
rc-notification: 5.6.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
rc-pagination: 5.1.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
rc-picker: 4.11.3(dayjs@1.11.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
rc-picker: 4.11.3(date-fns@4.1.0)(dayjs@1.11.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
rc-progress: 4.0.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
rc-rate: 2.13.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
rc-resize-observer: 1.4.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
@ -4542,6 +4765,8 @@ snapshots:
|
||||
d3-transition: 3.0.1(d3-selection@3.0.0)
|
||||
d3-zoom: 3.0.0
|
||||
|
||||
date-fns@4.1.0: {}
|
||||
|
||||
dayjs@1.11.13: {}
|
||||
|
||||
debug@4.3.7:
|
||||
@ -5062,6 +5287,8 @@ snapshots:
|
||||
|
||||
nanoid@5.1.3: {}
|
||||
|
||||
nanoid@5.1.4: {}
|
||||
|
||||
natural-compare@1.4.0: {}
|
||||
|
||||
node-forge@1.3.1: {}
|
||||
@ -5329,7 +5556,7 @@ snapshots:
|
||||
react: 19.0.0
|
||||
react-dom: 19.0.0(react@19.0.0)
|
||||
|
||||
rc-picker@4.11.3(dayjs@1.11.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
|
||||
rc-picker@4.11.3(date-fns@4.1.0)(dayjs@1.11.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.0
|
||||
'@rc-component/trigger': 2.2.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
@ -5340,6 +5567,7 @@ snapshots:
|
||||
react: 19.0.0
|
||||
react-dom: 19.0.0(react@19.0.0)
|
||||
optionalDependencies:
|
||||
date-fns: 4.1.0
|
||||
dayjs: 1.11.13
|
||||
|
||||
rc-progress@4.0.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
|
||||
@ -5505,6 +5733,14 @@ snapshots:
|
||||
react: 19.0.0
|
||||
react-dom: 19.0.0(react@19.0.0)
|
||||
|
||||
react-datepicker@8.2.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
|
||||
dependencies:
|
||||
'@floating-ui/react': 0.27.5(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
clsx: 2.1.1
|
||||
date-fns: 4.1.0
|
||||
react: 19.0.0
|
||||
react-dom: 19.0.0(react@19.0.0)
|
||||
|
||||
react-dom@19.0.0(react@19.0.0):
|
||||
dependencies:
|
||||
react: 19.0.0
|
||||
@ -5737,6 +5973,8 @@ snapshots:
|
||||
|
||||
supports-preserve-symlinks-flag@1.0.0: {}
|
||||
|
||||
tabbable@6.2.0: {}
|
||||
|
||||
tailwind-merge@3.0.2: {}
|
||||
|
||||
tailwindcss-animate@1.0.7(tailwindcss@4.0.12):
|
||||
|
Loading…
x
Reference in New Issue
Block a user