feat: add resources

This commit is contained in:
xion 2025-03-18 13:10:40 +08:00
parent cc76842582
commit 25def8c245
31 changed files with 1432 additions and 119 deletions

View File

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

View File

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

View File

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

View File

@ -0,0 +1,6 @@
export const config = {
api: () => location.origin,
apps: {
login: '/user/login',
},
};

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

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

View File

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

View File

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

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

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

View File

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

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

View 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: `![${resource?.name}](${url})`,
});
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>
);
};

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 || '更新元数据失败');
}
},
}));

View File

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

View File

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

View File

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

View File

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

View File

@ -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
View File

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