Compare commits
9 Commits
a857afbb2c
...
main
Author | SHA1 | Date | |
---|---|---|---|
59d53bb1e6 | |||
ba10b53377 | |||
e8d95a6798 | |||
bdf6243bd9 | |||
6d52707ad3 | |||
82cc4dab87 | |||
967c2f94f2 | |||
b8b649e694 | |||
c7763cddc3 |
13
package.json
13
package.json
@@ -15,10 +15,13 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.14.0",
|
"@emotion/styled": "^11.14.0",
|
||||||
"@mui/material": "^6.4.7",
|
"@mui/material": "^7.1.1",
|
||||||
"react": "19.0.0",
|
"re-resizable": "^6.11.2",
|
||||||
"react-dom": "19.0.0",
|
"react": "19.1.0",
|
||||||
"react-hook-form": "^7.54.2"
|
"react-dom": "19.1.0",
|
||||||
|
"react-draggable": "^4.4.6",
|
||||||
|
"react-hook-form": "^7.58.1",
|
||||||
|
"react-i18next": "^15.5.3"
|
||||||
},
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./src/index.tsx",
|
".": "./src/index.tsx",
|
||||||
@@ -26,7 +29,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"tailwind-merge": "^3.0.2"
|
"tailwind-merge": "^3.3.1"
|
||||||
},
|
},
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import MuiButton, { ButtonProps } from '@mui/material/Button';
|
import { Button as MuiButton, ButtonProps } from '@mui/material';
|
||||||
|
|
||||||
export const Button = (props: ButtonProps) => {
|
export const Button = (props: ButtonProps) => {
|
||||||
return <MuiButton {...props} />;
|
return <MuiButton {...props} />;
|
||||||
@@ -7,7 +7,7 @@ export const Button = (props: ButtonProps) => {
|
|||||||
export const IconButton = (props: ButtonProps) => {
|
export const IconButton = (props: ButtonProps) => {
|
||||||
const { variant = 'contained', color = 'primary', sx, children, ...rest } = props;
|
const { variant = 'contained', color = 'primary', sx, children, ...rest } = props;
|
||||||
return (
|
return (
|
||||||
<MuiButton variant={variant} color={color} {...rest} sx={{ color: 'white', minWidth: '32px', padding: '8px', ...sx }}>
|
<MuiButton variant={variant} color={color} {...rest} sx={{ color: 'white', minWidth: '24px', padding: '8px', ...sx }}>
|
||||||
{children}
|
{children}
|
||||||
</MuiButton>
|
</MuiButton>
|
||||||
);
|
);
|
||||||
|
117
src/drag-modal/index.tsx
Normal file
117
src/drag-modal/index.tsx
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import Draggable from 'react-draggable';
|
||||||
|
import { clsxMerge } from '../clsx';
|
||||||
|
import { Resizable } from 're-resizable';
|
||||||
|
import { X } from 'lucide-react';
|
||||||
|
|
||||||
|
type DragModalProps = {
|
||||||
|
title?: React.ReactNode;
|
||||||
|
content?: React.ReactNode;
|
||||||
|
onClose?: () => void;
|
||||||
|
containerClassName?: string;
|
||||||
|
handleClassName?: string;
|
||||||
|
contentClassName?: string;
|
||||||
|
focus?: boolean;
|
||||||
|
/**
|
||||||
|
* 默认大小, 单位为px
|
||||||
|
* width: defaultSize.width || 320
|
||||||
|
* height: defaultSize.height || 400
|
||||||
|
*/
|
||||||
|
defaultSize?: {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
};
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
};
|
||||||
|
export const DragModal = (props: DragModalProps) => {
|
||||||
|
const dragRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Draggable
|
||||||
|
nodeRef={dragRef as any}
|
||||||
|
onStop={(e, data) => {
|
||||||
|
// console.log(e, data);
|
||||||
|
}}
|
||||||
|
handle='.handle'
|
||||||
|
grid={[1, 1]}
|
||||||
|
scale={1}
|
||||||
|
bounds='parent'
|
||||||
|
defaultPosition={{
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
}}>
|
||||||
|
<div
|
||||||
|
className={clsxMerge('absolute top-0 left-0 bg-white rounded-md border border-gray-200 shadow-sm', props.focus ? 'z-30' : '', props.containerClassName)}
|
||||||
|
ref={dragRef}
|
||||||
|
style={props.style}>
|
||||||
|
<div className={clsxMerge('handle cursor-move border-b border-gray-200 py-2 px-4', props.handleClassName)}>{props.title || 'Move'}</div>
|
||||||
|
<Resizable
|
||||||
|
className={clsxMerge('', props.contentClassName)}
|
||||||
|
defaultSize={{
|
||||||
|
width: props.defaultSize?.width || 600,
|
||||||
|
height: props.defaultSize?.height || 400,
|
||||||
|
}}
|
||||||
|
onResizeStop={(e, direction, ref, d) => {
|
||||||
|
// console.log(e, direction, ref, d);
|
||||||
|
}}
|
||||||
|
enable={{
|
||||||
|
bottom: true,
|
||||||
|
right: true,
|
||||||
|
bottomRight: true,
|
||||||
|
}}>
|
||||||
|
{props.content}
|
||||||
|
</Resizable>
|
||||||
|
</div>
|
||||||
|
</Draggable>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type DragModalTitleProps = {
|
||||||
|
title?: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
onClose?: () => void;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
onClick?: () => void;
|
||||||
|
};
|
||||||
|
export const DragModalTitle = (props: DragModalTitleProps) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsxMerge('flex flex-row items-center justify-between', props.className)}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
props.onClick?.();
|
||||||
|
}}>
|
||||||
|
<div className='text-sm font-medium text-gray-700'>
|
||||||
|
{props.title}
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className='text-gray-500 cursor-pointer p-2 hover:bg-gray-100 rounded-md'
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
props.onClose?.();
|
||||||
|
}}>
|
||||||
|
<X className='w-4 h-4 ' />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getComputedHeight = () => {
|
||||||
|
const height = window.innerHeight;
|
||||||
|
const width = window.innerWidth;
|
||||||
|
return { height, width };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useComputedHeight = () => {
|
||||||
|
const [computedHeight, setComputedHeight] = useState({
|
||||||
|
height: 0,
|
||||||
|
width: 0,
|
||||||
|
});
|
||||||
|
useEffect(() => {
|
||||||
|
const height = window.innerHeight;
|
||||||
|
const width = window.innerWidth;
|
||||||
|
setComputedHeight({ height, width });
|
||||||
|
}, []);
|
||||||
|
return computedHeight;
|
||||||
|
};
|
40
src/render/ReactRender.tsx
Normal file
40
src/render/ReactRender.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { createRoot } from 'react-dom/client';
|
||||||
|
|
||||||
|
export class ReactRenderer {
|
||||||
|
component: any;
|
||||||
|
element: HTMLElement;
|
||||||
|
ref: React.RefObject<any>;
|
||||||
|
props: any;
|
||||||
|
root: any;
|
||||||
|
|
||||||
|
constructor(component: any, { props }: any) {
|
||||||
|
this.component = component;
|
||||||
|
this.element = document.createElement('div');
|
||||||
|
this.ref = React.createRef();
|
||||||
|
this.props = {
|
||||||
|
...props,
|
||||||
|
ref: this.ref,
|
||||||
|
};
|
||||||
|
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;
|
107
src/router/index.tsx
Normal file
107
src/router/index.tsx
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
// 路由上下文
|
||||||
|
type RouterContextType = {
|
||||||
|
pathname: string;
|
||||||
|
navigate: (to: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RouterContext = createContext<RouterContextType | null>(null);
|
||||||
|
|
||||||
|
// 使用路由上下文的Hook
|
||||||
|
export const useRouter = () => {
|
||||||
|
const context = useContext(RouterContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useRouter必须在RouterProvider内部使用');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 路由项定义
|
||||||
|
export type RouterItem = {
|
||||||
|
path: string;
|
||||||
|
element: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Router组件 - 路由系统的根组件
|
||||||
|
type RouterProps = {
|
||||||
|
routes?: RouterItem[];
|
||||||
|
children?: React.ReactNode;
|
||||||
|
};
|
||||||
|
export const Route = ({ path, element }: RouteProps) => {
|
||||||
|
return <>{element}</>;
|
||||||
|
};
|
||||||
|
export const Router = ({ routes, children }: RouterProps) => {
|
||||||
|
const [pathname, setPathname] = useState(window.location.pathname);
|
||||||
|
|
||||||
|
// 导航方法
|
||||||
|
const navigate = (to: string) => {
|
||||||
|
window.history.pushState(null, '', to);
|
||||||
|
setPathname(to);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听浏览器前进后退
|
||||||
|
useEffect(() => {
|
||||||
|
const handlePopState = () => {
|
||||||
|
setPathname(window.location.pathname);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('popstate', handlePopState);
|
||||||
|
return () => window.removeEventListener('popstate', handlePopState);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RouterContext.Provider value={{ pathname, navigate }}>
|
||||||
|
<Routes>
|
||||||
|
{routes?.map((route, index) => (
|
||||||
|
<Route key={index} path={route.path} element={route.element} />
|
||||||
|
))}
|
||||||
|
{children}
|
||||||
|
</Routes>
|
||||||
|
</RouterContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Route组件 - 定义单个路由
|
||||||
|
type RouteProps = {
|
||||||
|
path: string;
|
||||||
|
element: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Routes组件 - 渲染匹配的路由
|
||||||
|
type RoutesProps = {
|
||||||
|
children: React.ReactNode | React.ReactNode[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Routes = ({ children }: RoutesProps) => {
|
||||||
|
const { pathname } = useRouter();
|
||||||
|
|
||||||
|
// 将children转换为数组处理
|
||||||
|
const childrenArray = React.Children.toArray(children);
|
||||||
|
|
||||||
|
// 查找匹配的路由
|
||||||
|
const matchedRoute = childrenArray.find((child) => {
|
||||||
|
if (React.isValidElement(child) && typeof child.type === 'function') {
|
||||||
|
const routeProps = child.props as RouteProps;
|
||||||
|
|
||||||
|
// 简单路径匹配,可以扩展为支持参数等更复杂功能
|
||||||
|
if (routeProps.path === pathname || (routeProps.path === '/' && pathname === '')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 支持通配符路由
|
||||||
|
if (routeProps.path.endsWith('*') && pathname.startsWith(routeProps.path.slice(0, -1))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 返回匹配的路由元素或空
|
||||||
|
if (matchedRoute && React.isValidElement(matchedRoute)) {
|
||||||
|
// @ts-ignore
|
||||||
|
return <>{matchedRoute.props?.element}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
@@ -8,8 +8,9 @@ type TagsInputProps = {
|
|||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
label?: any;
|
label?: any;
|
||||||
showLabel?: boolean;
|
showLabel?: boolean;
|
||||||
|
options?: string[];
|
||||||
};
|
};
|
||||||
export const TagsInput = ({ value, onChange, placeholder = '', label = '', showLabel = false }: TagsInputProps) => {
|
export const TagsInput = ({ value, onChange, placeholder = '', label = '', showLabel = false, options = [] }: TagsInputProps) => {
|
||||||
const [tags, setTags] = useState<string[]>(value);
|
const [tags, setTags] = useState<string[]>(value);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTags(value);
|
setTags(value);
|
||||||
@@ -21,7 +22,7 @@ export const TagsInput = ({ value, onChange, placeholder = '', label = '', showL
|
|||||||
<Autocomplete
|
<Autocomplete
|
||||||
multiple
|
multiple
|
||||||
freeSolo
|
freeSolo
|
||||||
options={[]}
|
options={options || []}
|
||||||
value={tags}
|
value={tags}
|
||||||
onChange={(event, newValue) => {
|
onChange={(event, newValue) => {
|
||||||
// setTags(newValue as string[]);
|
// setTags(newValue as string[]);
|
||||||
|
@@ -7,7 +7,6 @@ type SelectProps = {
|
|||||||
|
|
||||||
export const Select = React.forwardRef((props: SelectProps, ref) => {
|
export const Select = React.forwardRef((props: SelectProps, ref) => {
|
||||||
const { options, ...rest } = props;
|
const { options, ...rest } = props;
|
||||||
console.log(props, 'props');
|
|
||||||
return (
|
return (
|
||||||
<MuiSelect {...rest} ref={ref}>
|
<MuiSelect {...rest} ref={ref}>
|
||||||
{options?.map((option) => (
|
{options?.map((option) => (
|
||||||
|
22
src/theme/Provider.tsx
Normal file
22
src/theme/Provider.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { ToastContainer } from 'react-toastify';
|
||||||
|
import { CustomThemeProvider } from '.';
|
||||||
|
|
||||||
|
type ToastProviderProps = {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
};
|
||||||
|
export const ToastProvider = ({ children }: ToastProviderProps) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{children}
|
||||||
|
<ToastContainer />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export const Provider = ({ children }) => {
|
||||||
|
return (
|
||||||
|
<CustomThemeProvider>
|
||||||
|
{children}
|
||||||
|
<ToastProvider />
|
||||||
|
</CustomThemeProvider>
|
||||||
|
);
|
||||||
|
};
|
@@ -1,7 +1,7 @@
|
|||||||
import { createTheme, Shadows, ThemeOptions } from '@mui/material/styles';
|
import { createTheme, Shadows, ThemeOptions } from '@mui/material';
|
||||||
import { useTheme as useMuiTheme, Theme } from '@mui/material/styles';
|
import { useTheme as useMuiTheme, Theme } from '@mui/material';
|
||||||
import { amber, red } from '@mui/material/colors';
|
import { amber, red } from '@mui/material/colors';
|
||||||
import { ThemeProvider } from '@mui/material/styles';
|
import { ThemeProvider } from '@mui/material';
|
||||||
const generateShadows = (color: string): Shadows => {
|
const generateShadows = (color: string): Shadows => {
|
||||||
return [
|
return [
|
||||||
'none',
|
'none',
|
||||||
|
44
src/toast/ToastLogin.tsx
Normal file
44
src/toast/ToastLogin.tsx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { toast } from 'react-toastify';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
// Custom message component
|
||||||
|
const LoginMessage = (props: ToastLoginProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const handleClick = () => {
|
||||||
|
const currentUrl = window.location.href;
|
||||||
|
const redirect = encodeURIComponent(props?.redirectUrl || currentUrl);
|
||||||
|
const loginUrl = props?.loginUrl || '/user/login/';
|
||||||
|
const newUrl = location.origin + loginUrl + '?redirect=' + redirect;
|
||||||
|
window.open(newUrl, '_self');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='msg-container' onClick={handleClick} style={{ cursor: 'pointer' }}>
|
||||||
|
<p className='msg-title'>{t('Please login')}</p>
|
||||||
|
<p className='msg-description'>{t('Click here to go to the login page.')}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
type ToastLoginProps = {
|
||||||
|
/**
|
||||||
|
* 登录页面地址, /user/login
|
||||||
|
*/
|
||||||
|
loginUrl?: string;
|
||||||
|
/**
|
||||||
|
* 登录成功后跳转的地址, 默认是当前页面
|
||||||
|
*/
|
||||||
|
redirectUrl?: string;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* 登录提示
|
||||||
|
* @param props
|
||||||
|
* @example
|
||||||
|
* toastLogin({
|
||||||
|
* loginUrl: '/user/login/',
|
||||||
|
* redirectUrl: window.location.href,
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export const toastLogin = (props: ToastLoginProps = {}) => {
|
||||||
|
toast.info(<LoginMessage {...props} />, {
|
||||||
|
autoClose: 5000 * 3,
|
||||||
|
});
|
||||||
|
};
|
Reference in New Issue
Block a user