feat: 添加i18n,美化界面
This commit is contained in:
@@ -10,7 +10,6 @@ import { Redirect } from './modules/Redirect';
|
||||
import { CustomThemeProvider } from '@kevisual/center-components/theme/index.tsx';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { ToastContainer } from 'react-toastify';
|
||||
// import 'react-toastify/dist/ReactToastify.css';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import 'dayjs/locale/zh-cn';
|
||||
@@ -62,6 +61,7 @@ export const App = () => {
|
||||
<Route path='/container/*' element={<ContainerApp />} />
|
||||
<Route path='/map/*' element={<MapApp />} />
|
||||
<Route path='/user/*' element={<UserApp />} />
|
||||
<Route path='/user1/*' element={<UserApp />} />
|
||||
<Route path='/org/*' element={<OrgApp />} />
|
||||
<Route path='/app/*' element={<UserAppApp />} />
|
||||
<Route path='/file/*' element={<FileApp />} />
|
||||
|
||||
34
src/I18Next.tsx
Normal file
34
src/I18Next.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
import Backend from 'i18next-http-backend'; // 引入 Backend 插件
|
||||
|
||||
type I18NextProviderProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const initI18n = (basename: string) => {
|
||||
// 初始化 i18n
|
||||
i18n
|
||||
.use(Backend) // 使用 Backend 插件
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
backend: {
|
||||
loadPath: `${basename}/locales/{{lng}}/{{ns}}.json`, // 指定 JSON 文件的路径
|
||||
},
|
||||
lng: 'zh', // 默认语言
|
||||
fallbackLng: 'en', // 备用语言
|
||||
interpolation: {
|
||||
escapeValue: false, // react 已经安全地处理了转义
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 国际化组件,初始化
|
||||
* @param props
|
||||
* @returns
|
||||
*/
|
||||
export const I18NextProvider = (props: I18NextProviderProps) => {
|
||||
const { children } = props;
|
||||
return <>{children}</>;
|
||||
};
|
||||
@@ -3,13 +3,6 @@
|
||||
@import './index.css';
|
||||
@import '@kevisual/center-components/theme/wind-theme.css';
|
||||
|
||||
html,
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: 16px;
|
||||
font-family: 'Montserrat', sans-serif;
|
||||
}
|
||||
h1 {
|
||||
@apply text-2xl font-bold;
|
||||
}
|
||||
@@ -38,7 +31,7 @@ h3 {
|
||||
@apply text-gray-700;
|
||||
}
|
||||
@utility card-key {
|
||||
@apply text-xs text-gray-400;
|
||||
@apply text-xs;
|
||||
}
|
||||
@utility card-footer {
|
||||
@apply text-sm text-gray-500;
|
||||
@@ -48,7 +41,7 @@ h3 {
|
||||
}
|
||||
|
||||
@utility layout-menu {
|
||||
@apply bg-secondary p-2 text-white flex justify-between h-12 ;
|
||||
@apply bg-secondary p-2 text-white flex justify-between h-12;
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
@utility no-drag {
|
||||
@@ -64,58 +57,8 @@ h3 {
|
||||
}
|
||||
}
|
||||
|
||||
/* font-family */
|
||||
@utility font-family-mon {
|
||||
font-family: 'Montserrat', sans-serif;
|
||||
}
|
||||
@utility font-family-rob {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
}
|
||||
@utility font-family-int {
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
@utility font-family-orb {
|
||||
font-family: 'Orbitron', sans-serif;
|
||||
}
|
||||
@utility font-family-din {
|
||||
font-family: 'DIN', sans-serif;
|
||||
}
|
||||
|
||||
@utility flex-row-center {
|
||||
@apply flex flex-row items-center justify-center;
|
||||
}
|
||||
@utility flex-col-center {
|
||||
@apply flex flex-col items-center justify-center;
|
||||
}
|
||||
|
||||
@utility scrollbar {
|
||||
overflow: auto;
|
||||
/* 整个滚动条 */
|
||||
&::-webkit-scrollbar {
|
||||
width: 3px;
|
||||
height: 3px;
|
||||
}
|
||||
|
||||
/* 滚动条有滑块的轨道部分 */
|
||||
&::-webkit-scrollbar-track-piece {
|
||||
background-color: transparent;
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
/* 滚动条滑块(竖向:vertical 横向:horizontal) */
|
||||
&::-webkit-scrollbar-thumb {
|
||||
cursor: pointer;
|
||||
background-color: #c1c1c1;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
/* 滚动条滑块hover */
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #999999;
|
||||
}
|
||||
|
||||
/* 同时有垂直和水平滚动条时交汇的部分 */
|
||||
&::-webkit-scrollbar-corner {
|
||||
display: block; /* 修复交汇时出现的白块 */
|
||||
}
|
||||
|
||||
.cm-editor {
|
||||
@apply h-full;
|
||||
}
|
||||
|
||||
13
src/main.tsx
13
src/main.tsx
@@ -1,5 +1,16 @@
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { App } from './App.tsx';
|
||||
import './globals.css';
|
||||
import { initI18n, I18NextProvider } from './I18Next.tsx';
|
||||
import { basename } from './modules/basename.ts';
|
||||
import { Suspense } from 'react';
|
||||
|
||||
createRoot(document.getElementById('root')!).render(<App />);
|
||||
initI18n(basename);
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<I18NextProvider>
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<App />
|
||||
</Suspense>
|
||||
</I18NextProvider>,
|
||||
);
|
||||
|
||||
@@ -1,42 +1,18 @@
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { useLayoutStore } from './store';
|
||||
import clsx from 'clsx';
|
||||
import { Button, Dropdown } from 'antd';
|
||||
import { Menu, MenuItem, Tooltip } from '@mui/material';
|
||||
import { Button } from '@mui/material';
|
||||
import { IconButton } from '@kevisual/center-components/button/index.tsx';
|
||||
import { message } from '@/modules/message';
|
||||
import {
|
||||
CloseOutlined,
|
||||
CodeOutlined,
|
||||
DashboardOutlined,
|
||||
HomeOutlined,
|
||||
LogoutOutlined,
|
||||
MessageOutlined,
|
||||
ReadOutlined,
|
||||
RocketOutlined,
|
||||
SmileOutlined,
|
||||
SwapOutlined,
|
||||
SwitcherOutlined,
|
||||
UserOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { DashboardOutlined, HomeOutlined, LogoutOutlined, SmileOutlined } from '@ant-design/icons';
|
||||
import { useMemo } from 'react';
|
||||
import { query } from '../query';
|
||||
import { useNewNavigate } from '../navicate';
|
||||
const meun = [
|
||||
{
|
||||
title: 'Your profile',
|
||||
icon: <HomeOutlined />,
|
||||
link: '/user/profile',
|
||||
},
|
||||
{
|
||||
title: 'Your orgs',
|
||||
icon: <DashboardOutlined />,
|
||||
link: '/org/edit/list',
|
||||
},
|
||||
{
|
||||
title: 'Site Map',
|
||||
icon: <HomeOutlined />,
|
||||
link: '/map',
|
||||
},
|
||||
];
|
||||
import { Users, X } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import React from 'react';
|
||||
|
||||
export const LayoutUser = () => {
|
||||
const { open, setOpen, ...store } = useLayoutStore(
|
||||
useShallow((state) => ({
|
||||
@@ -47,6 +23,24 @@ export const LayoutUser = () => {
|
||||
})),
|
||||
);
|
||||
const navigate = useNewNavigate();
|
||||
const { t } = useTranslation();
|
||||
const meun = [
|
||||
{
|
||||
title: t('Your profile'),
|
||||
icon: <HomeOutlined />,
|
||||
link: '/user/profile',
|
||||
},
|
||||
{
|
||||
title: t('Your orgs'),
|
||||
icon: <DashboardOutlined />,
|
||||
link: '/org/edit/list',
|
||||
},
|
||||
{
|
||||
title: t('Site Map'),
|
||||
icon: <HomeOutlined />,
|
||||
link: '/map',
|
||||
},
|
||||
];
|
||||
const items = useMemo(() => {
|
||||
const orgs = store.me?.orgs || [];
|
||||
return orgs.map((item) => {
|
||||
@@ -57,30 +51,41 @@ export const LayoutUser = () => {
|
||||
};
|
||||
});
|
||||
}, [store.me]);
|
||||
|
||||
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
|
||||
|
||||
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={clsx('w-full h-full absolute z-20 no-drag', !open && 'hidden')}>
|
||||
<div className={clsx('w-full h-full absolute z-20 no-drag ', !open && 'hidden')}>
|
||||
<div
|
||||
className='bg-white w-full absolute h-full opacity-60 z-0'
|
||||
className='w-full absolute h-full opacity-60 z-0'
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
}}></div>
|
||||
<div className='w-[400px] h-full absolute top-0 right-0 bg-white rounded-l-lg'>
|
||||
<div className='w-[400px] bg-amber-900 text-primary transition-all duration-300 h-full absolute top-0 right-0 rounded-l-lg'>
|
||||
<div className='flex justify-between p-6 mt-4 font-bold items-center border-b'>
|
||||
User: {store.me?.username}
|
||||
<div className='flex items-center gap-2'>
|
||||
{t('User')}: <span className='text-primary'>{store.me?.username}</span>
|
||||
</div>
|
||||
<div className='flex gap-4'>
|
||||
{items.length > 0 && (
|
||||
<Dropdown
|
||||
placement='bottomRight'
|
||||
menu={{
|
||||
items: items,
|
||||
onClick: (item) => {
|
||||
store.switchOrg(item.key, 'org');
|
||||
},
|
||||
}}>
|
||||
<Button icon={<SwapOutlined />} onClick={() => {}}></Button>
|
||||
</Dropdown>
|
||||
<Tooltip title={t('Switch Org')}>
|
||||
<Button aria-controls='switch-org-menu' aria-haspopup='true' onClick={handleClick}>
|
||||
<Users />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Button icon={<CloseOutlined />} onClick={() => setOpen(false)}></Button>
|
||||
|
||||
<Button onClick={() => setOpen(false)}>
|
||||
<X />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className='mt-3 font-medium'>
|
||||
@@ -88,7 +93,7 @@ export const LayoutUser = () => {
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className='flex items-center p-4 hover:bg-gray-100 cursor-pointer'
|
||||
className='flex items-center p-4 hover:bg-secondary hover:text-white cursor-pointer'
|
||||
onClick={() => {
|
||||
if (item.link) {
|
||||
navigate(item.link);
|
||||
@@ -104,7 +109,7 @@ export const LayoutUser = () => {
|
||||
})}
|
||||
</div>
|
||||
<div
|
||||
className='flex items-center p-4 hover:bg-gray-100 cursor-pointer'
|
||||
className='flex items-center p-4 hover:bg-secondary hover:text-white cursor-pointer'
|
||||
onClick={() => {
|
||||
query.removeToken();
|
||||
window.open('/user/login', '_self');
|
||||
@@ -112,9 +117,24 @@ export const LayoutUser = () => {
|
||||
<div className='mr-4'>
|
||||
<LogoutOutlined />
|
||||
</div>
|
||||
<div>Login Out</div>
|
||||
<div>{t('Login Out')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<Menu id='simple-menu' anchorEl={anchorEl} keepMounted open={Boolean(anchorEl)} onClose={handleClose}>
|
||||
{items.map((item, index) => {
|
||||
return (
|
||||
<MenuItem
|
||||
key={index}
|
||||
onClick={() => {
|
||||
store.switchOrg(item.key, 'org');
|
||||
handleClose();
|
||||
}}>
|
||||
<div className='mr-4'>{item.icon}</div>
|
||||
<div>{item.label}</div>
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,53 +3,44 @@ import { useLayoutStore } from './store';
|
||||
import clsx from 'clsx';
|
||||
import { Button } from '@mui/material';
|
||||
import { message } from '@/modules/message';
|
||||
import {
|
||||
AppstoreOutlined,
|
||||
CloseOutlined,
|
||||
CodeOutlined,
|
||||
DashboardOutlined,
|
||||
FolderOutlined,
|
||||
HomeOutlined,
|
||||
MessageOutlined,
|
||||
ReadOutlined,
|
||||
RocketOutlined,
|
||||
SmileOutlined,
|
||||
SwitcherOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { AppstoreOutlined, CodeOutlined, FolderOutlined, HomeOutlined, SmileOutlined, SwitcherOutlined } from '@ant-design/icons';
|
||||
import { X } from 'lucide-react';
|
||||
import { useNewNavigate } from '../navicate';
|
||||
const meun = [
|
||||
{
|
||||
title: 'Home',
|
||||
icon: <HomeOutlined />,
|
||||
link: '/map',
|
||||
},
|
||||
{
|
||||
title: 'User App',
|
||||
icon: <AppstoreOutlined />,
|
||||
link: '/app/edit/list',
|
||||
},
|
||||
{
|
||||
title: 'File App',
|
||||
icon: <FolderOutlined />,
|
||||
link: '/file/edit/list',
|
||||
},
|
||||
{
|
||||
title: 'Container',
|
||||
icon: <CodeOutlined />,
|
||||
link: '/container/edit/list',
|
||||
},
|
||||
{
|
||||
title: 'Org',
|
||||
icon: <SwitcherOutlined />,
|
||||
link: '/org/edit/list',
|
||||
},
|
||||
{
|
||||
title: 'About',
|
||||
icon: <SmileOutlined />,
|
||||
},
|
||||
];
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const LayoutMenu = () => {
|
||||
const { t } = useTranslation();
|
||||
const meun = [
|
||||
{
|
||||
title: t('Home'),
|
||||
icon: <HomeOutlined />,
|
||||
link: '/map',
|
||||
},
|
||||
{
|
||||
title: t('User App'),
|
||||
icon: <AppstoreOutlined />,
|
||||
link: '/app/edit/list',
|
||||
},
|
||||
{
|
||||
title: t('File App'),
|
||||
icon: <FolderOutlined />,
|
||||
link: '/file/edit/list',
|
||||
},
|
||||
{
|
||||
title: t('Container'),
|
||||
icon: <CodeOutlined />,
|
||||
link: '/container/edit/list',
|
||||
},
|
||||
{
|
||||
title: t('Org'),
|
||||
icon: <SwitcherOutlined />,
|
||||
link: '/org/edit/list',
|
||||
},
|
||||
{
|
||||
title: t('About'),
|
||||
icon: <SmileOutlined />,
|
||||
},
|
||||
];
|
||||
const { open, setOpen } = useLayoutStore(useShallow((state) => ({ open: state.open, setOpen: state.setOpen })));
|
||||
const navigate = useNewNavigate();
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { MenuOutlined, SwapOutlined } from '@ant-design/icons';
|
||||
import { Tooltip } from 'antd';
|
||||
import { Tooltip } from '@mui/material';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { LayoutMenu } from './Menu';
|
||||
import { useLayoutStore, usePlatformStore } from './store';
|
||||
@@ -7,9 +7,14 @@ import { useShallow } from 'zustand/react/shallow';
|
||||
import { useEffect, useLayoutEffect, useState } from 'react';
|
||||
import { LayoutUser } from './LayoutUser';
|
||||
import PandaPNG from '@/assets/panda.png';
|
||||
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
|
||||
import { Panel, PanelGroup } from 'react-resizable-panels';
|
||||
import clsx from 'clsx';
|
||||
import { IconButton as Button } from '@mui/material';
|
||||
import { Button, Menu, MenuItem } from '@mui/material';
|
||||
import i18n from 'i18next';
|
||||
|
||||
import { IconButton } from '@kevisual/center-components/button/index.tsx';
|
||||
import { Languages } from 'lucide-react';
|
||||
|
||||
type LayoutMainProps = {
|
||||
title?: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
@@ -46,6 +51,23 @@ export const LayoutMain = (props: LayoutMainProps) => {
|
||||
useEffect(() => {
|
||||
menuStore.getMe();
|
||||
}, []);
|
||||
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
|
||||
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
const changeLanguage = (lng: string) => {
|
||||
i18n.changeLanguage(lng);
|
||||
handleClose();
|
||||
};
|
||||
const currentLanguage = i18n.language;
|
||||
|
||||
return (
|
||||
<div className='flex w-full h-full flex-col relative'>
|
||||
<LayoutMenu />
|
||||
@@ -54,16 +76,31 @@ export const LayoutMain = (props: LayoutMainProps) => {
|
||||
style={{
|
||||
cursor: isElectron ? 'move' : 'default',
|
||||
}}>
|
||||
<Button
|
||||
<IconButton
|
||||
className={clsx('mr-4 cursor-pointer no-drag', isMac && 'ml-16')}
|
||||
onClick={() => {
|
||||
menuStore.setOpen(true);
|
||||
}}>
|
||||
<MenuOutlined />
|
||||
</Button>
|
||||
</IconButton>
|
||||
<div className='flex grow justify-between pl-4 items-center'>
|
||||
{props.title}
|
||||
<div className='mr-4 flex gap-4 items-center no-drag'>
|
||||
<div>
|
||||
<Tooltip title={currentLanguage === 'en' ? 'English' : 'Chinese'}>
|
||||
<IconButton onClick={handleClick} variant='contained'>
|
||||
<Languages />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleClose}>
|
||||
<MenuItem selected={currentLanguage === 'en'} onClick={() => changeLanguage('en')}>
|
||||
English
|
||||
</MenuItem>
|
||||
<MenuItem selected={currentLanguage === 'zh'} onClick={() => changeLanguage('zh')}>
|
||||
中文
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</div>
|
||||
{menuStore.me?.type === 'org' && (
|
||||
<div>
|
||||
<Tooltip title='Switch To User'>
|
||||
@@ -97,16 +134,11 @@ export const LayoutMain = (props: LayoutMainProps) => {
|
||||
<PanelGroup className='w-full h-full panel-layout' autoSaveId='editor-layout-main' direction='horizontal'>
|
||||
<Panel style={{ height: '100%' }}>
|
||||
<div className='h-full overflow-hidden'>
|
||||
<div className='w-full h-full rounded-lg'>
|
||||
<div className='w-full h-full rounded-lg text-primary'>
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
|
||||
{/* <PanelResizeHandle />
|
||||
<Panel style={{ height: '100%' }} defaultSize={25} className={clsx('bg-gray-100')}>
|
||||
侧边栏
|
||||
</Panel> */}
|
||||
</PanelGroup>
|
||||
</div>
|
||||
<LayoutUser />
|
||||
|
||||
@@ -2,14 +2,19 @@ import { useNavigation, useParams } from 'react-router';
|
||||
import { useAppVersionStore } from '../store';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Button, Form, Input, Modal, Space, Tooltip } from 'antd';
|
||||
import { Form, Input, Modal, Tooltip } from 'antd';
|
||||
import { CloudUploadOutlined, DeleteOutlined, EditOutlined, FileOutlined, LeftOutlined, LinkOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import { isObjectNull } from '@/utils/is-null';
|
||||
import { FileUpload } from '../modules/FileUpload';
|
||||
import clsx from 'clsx';
|
||||
import { message } from '@/modules/message';
|
||||
import { useNewNavigate } from '@/modules';
|
||||
import { Button } from '@mui/material';
|
||||
import { Dialog, DialogContent, DialogTitle, ButtonGroup } from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { IconButton } from '@kevisual/center-components/button/index.tsx';
|
||||
const FormModal = () => {
|
||||
const { t } = useTranslation();
|
||||
const [form] = Form.useForm();
|
||||
const containerStore = useAppVersionStore(
|
||||
useShallow((state) => {
|
||||
@@ -68,11 +73,11 @@ const FormModal = () => {
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label=' ' colon={false}>
|
||||
<Button type='primary' htmlType='submit'>
|
||||
Submit
|
||||
<Button type='submit' variant='contained' color='primary'>
|
||||
{t('submit')}
|
||||
</Button>
|
||||
<Button className='ml-2' htmlType='reset' onClick={onClose}>
|
||||
Cancel
|
||||
<Button className='ml-2' onClick={onClose}>
|
||||
{t('cancel')}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
@@ -115,24 +120,25 @@ export const AppVersionList = () => {
|
||||
return (
|
||||
<div className='w-full h-full flex bg-slate-100'>
|
||||
<div className='p-2 bg-white'>
|
||||
<Button
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
versionStore.setFormData({ key: appKey });
|
||||
versionStore.setShowEdit(true);
|
||||
}}
|
||||
icon={<PlusOutlined />}
|
||||
/>
|
||||
}}>
|
||||
<PlusOutlined />
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
<div className='grow h-full relative'>
|
||||
<div className='absolute top-2 left-4'>
|
||||
<Tooltip title='返回' placement='bottom'>
|
||||
<Button
|
||||
<IconButton
|
||||
variant='contained'
|
||||
onClick={() => {
|
||||
navigate('/app/edit/list');
|
||||
}}
|
||||
icon={<LeftOutlined />}
|
||||
/>
|
||||
}}>
|
||||
<LeftOutlined />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
@@ -144,7 +150,7 @@ export const AppVersionList = () => {
|
||||
const color = isPublish ? 'bg-green-500' : '';
|
||||
const isRunning = item.status === 'running';
|
||||
return (
|
||||
<div className='card border-t w-[300px]' key={index}>
|
||||
<div className='card w-[300px]' key={index}>
|
||||
<div className={'flex items-center justify-between'}>
|
||||
{item.version}
|
||||
|
||||
@@ -153,7 +159,10 @@ export const AppVersionList = () => {
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className='mt-4'>
|
||||
<Space.Compact>
|
||||
<ButtonGroup
|
||||
variant='contained'
|
||||
color='primary'
|
||||
sx={{ color: 'white', '& .MuiButton-root': { color: 'white', minWidth: '32px', width: '32px', height: '32px', padding: '6px' } }}>
|
||||
{/* <Button
|
||||
onClick={() => {
|
||||
versionStore.setFormData(item);
|
||||
@@ -172,21 +181,20 @@ export const AppVersionList = () => {
|
||||
},
|
||||
});
|
||||
e.stopPropagation();
|
||||
}}
|
||||
icon={<DeleteOutlined />}
|
||||
/>
|
||||
}}>
|
||||
<DeleteOutlined />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip title='使用当前版本,发布为此版本'>
|
||||
<Button
|
||||
icon={<CloudUploadOutlined />}
|
||||
onClick={() => {
|
||||
versionStore.publishVersion({ id: item.id });
|
||||
}}
|
||||
/>
|
||||
}}>
|
||||
<CloudUploadOutlined />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip title={'To Test App'}>
|
||||
<Button
|
||||
icon={<LinkOutlined />}
|
||||
onClick={() => {
|
||||
if (isRunning) {
|
||||
const link = new URL(`/test/${item.id}`, location.origin);
|
||||
@@ -194,18 +202,20 @@ export const AppVersionList = () => {
|
||||
} else {
|
||||
message.error('The app is not running');
|
||||
}
|
||||
}}></Button>
|
||||
}}>
|
||||
<LinkOutlined />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip title='文件管理'>
|
||||
<Button
|
||||
icon={<FileOutlined />}
|
||||
onClick={() => {
|
||||
versionStore.setFormData(item);
|
||||
setIsUpload(true);
|
||||
}}
|
||||
/>
|
||||
}}>
|
||||
<FileOutlined />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Space.Compact>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -220,12 +230,12 @@ export const AppVersionList = () => {
|
||||
<div className='bg-white p-2 w-[600px] h-full flex flex-col'>
|
||||
<div className='header flex items-center gap-2'>
|
||||
<Tooltip title='返回'>
|
||||
<Button
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setIsUpload(false);
|
||||
}}
|
||||
icon={<LeftOutlined />}
|
||||
/>
|
||||
}}>
|
||||
<LeftOutlined />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<div className='font-bold'>{versionStore.key}</div>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Button } from 'antd';
|
||||
import { Button } from '@mui/material';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { useAppVersionStore } from '../store';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { message } from '@/modules/message';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
export type FileType = {
|
||||
name: string;
|
||||
size: number;
|
||||
@@ -12,6 +13,7 @@ export type FileType = {
|
||||
|
||||
export const FileUpload = () => {
|
||||
const ref = useRef<HTMLInputElement | null>(null);
|
||||
const { t } = useTranslation();
|
||||
const appVersionStore = useAppVersionStore(
|
||||
useShallow((state) => {
|
||||
return {
|
||||
@@ -27,11 +29,21 @@ export const FileUpload = () => {
|
||||
// webkitRelativePath
|
||||
let files = Array.from(e.target.files) as any[];
|
||||
console.log(files);
|
||||
if (files.length === 0) {
|
||||
message.error('请选择文件');
|
||||
return;
|
||||
}
|
||||
|
||||
// 过滤 文件 .DS_Store
|
||||
files = files.filter((file) => {
|
||||
if (file.webkitRelativePath.startsWith('__MACOSX')) {
|
||||
return false;
|
||||
}
|
||||
// 过滤node_modules
|
||||
if (file.webkitRelativePath.includes('node_modules')) {
|
||||
return false;
|
||||
}
|
||||
// 过滤以.开头的文件
|
||||
return !file.name.startsWith('.');
|
||||
});
|
||||
if (files.length === 0) {
|
||||
@@ -89,7 +101,7 @@ export const FileUpload = () => {
|
||||
}
|
||||
ref.current!.click();
|
||||
}}>
|
||||
上传文件夹
|
||||
{t('uploadDirectory')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,30 +1,26 @@
|
||||
import { Input, Modal, Select, Space, Switch } from 'antd';
|
||||
import { Fragment, useEffect, useState } from 'react';
|
||||
import { Input, Modal, Select } from 'antd';
|
||||
import { Fragment, Suspense, useEffect, useState } from 'react';
|
||||
import { TextArea } from '../components/TextArea';
|
||||
import { useContainerStore } from '../store';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { Form } from 'antd';
|
||||
import copy from 'copy-to-clipboard';
|
||||
// import copy from 'copy-to-clipboard';
|
||||
import { useNewNavigate } from '@/modules';
|
||||
import { message } from '@/modules/message';
|
||||
import { Dialog, DialogTitle, DialogContent, Tooltip, Button, ButtonGroup } from '@mui/material';
|
||||
import { IconButton } from '@kevisual/center-components/button/index.tsx';
|
||||
import { getDirectoryAndName, toFile, uploadFileChunked } from '@kevisual/resources/index.ts';
|
||||
import {
|
||||
EditOutlined,
|
||||
SettingOutlined,
|
||||
LinkOutlined,
|
||||
SaveOutlined,
|
||||
DeleteOutlined,
|
||||
LeftOutlined,
|
||||
MessageOutlined,
|
||||
PlusOutlined,
|
||||
DashboardOutlined,
|
||||
CloudUploadOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import clsx from 'clsx';
|
||||
import EditOutlined from '@ant-design/icons/EditOutlined';
|
||||
import DeleteOutlined from '@ant-design/icons/DeleteOutlined';
|
||||
import PlusOutlined from '@ant-design/icons/PlusOutlined';
|
||||
import CloudUploadOutlined from '@ant-design/icons/CloudUploadOutlined';
|
||||
|
||||
import { isObjectNull } from '@/utils/is-null';
|
||||
import { CardBlank } from '@/components/card/CardBlank';
|
||||
import { Settings } from 'lucide-react';
|
||||
import React from 'react';
|
||||
|
||||
const DrawEdit = React.lazy(() => import('../module/DrawEdit'));
|
||||
const FormModal = () => {
|
||||
const [form] = Form.useForm();
|
||||
const containerStore = useContainerStore(
|
||||
@@ -81,9 +77,11 @@ const FormModal = () => {
|
||||
<Form.Item name='tags' label='tags'>
|
||||
<Select mode='tags' />
|
||||
</Form.Item>
|
||||
<Form.Item name='code' label='code'>
|
||||
<TextArea />
|
||||
</Form.Item>
|
||||
{!isEdit && (
|
||||
<Form.Item name='code' label='code'>
|
||||
<TextArea />
|
||||
</Form.Item>
|
||||
)}
|
||||
<Form.Item label=' ' colon={false}>
|
||||
<Button variant='contained' type='submit'>
|
||||
提交
|
||||
@@ -122,10 +120,9 @@ const PublishFormModal = () => {
|
||||
}, [containerStore.showEdit]);
|
||||
const onFinish = async () => {
|
||||
const values = form.getFieldsValue();
|
||||
const success = await containerStore.updateData(values, { closePublish: false });
|
||||
if (success) {
|
||||
const formData = containerStore.formData;
|
||||
const code = formData.code;
|
||||
const containerRes = await containerStore.updateData(values, { closePublish: false });
|
||||
if (containerRes.code === 200) {
|
||||
const code = containerRes.data?.code || '-';
|
||||
const fileName = values['publish']?.['fileName'];
|
||||
let directoryAndName: ReturnType<typeof getDirectoryAndName> | null = null;
|
||||
try {
|
||||
@@ -142,7 +139,6 @@ const PublishFormModal = () => {
|
||||
const key = values['publish']['key'];
|
||||
const version = values['publish']['version'];
|
||||
const file = toFile(code, directoryAndName.name);
|
||||
console.log('key', key, version, directoryAndName.directory, directoryAndName.name);
|
||||
const res = await uploadFileChunked(file, {
|
||||
appKey: key,
|
||||
version,
|
||||
@@ -167,15 +163,15 @@ const PublishFormModal = () => {
|
||||
return (
|
||||
<Dialog open={containerStore.showEdit} onClose={() => containerStore.setShowEdit(false)}>
|
||||
<DialogTitle>Publish</DialogTitle>
|
||||
<DialogContent sx={{ padding: '20px', minWidth: '600px' }}>
|
||||
<DialogContent sx={{ padding: '10px', minWidth: '600px' }}>
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={onFinish}
|
||||
labelCol={{
|
||||
span: 4,
|
||||
span: 6,
|
||||
}}
|
||||
wrapperCol={{
|
||||
span: 20,
|
||||
span: 18,
|
||||
}}>
|
||||
<Form.Item name='id' hidden>
|
||||
<Input />
|
||||
@@ -229,12 +225,14 @@ export const ContainerList = () => {
|
||||
setShowPublish: state.setShowPublish,
|
||||
updateData: state.updateData,
|
||||
formData: state.formData,
|
||||
getOne: state.getOne,
|
||||
setOpenDrawEdit: state.setOpenDrawEdit,
|
||||
openDrawEdit: state.openDrawEdit,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
const [codeEdit, setCodeEdit] = useState(false);
|
||||
const [code, setCode] = useState('');
|
||||
// const [codeEdit, setCodeEdit] = useState(false);
|
||||
useEffect(() => {
|
||||
containerStore.getList();
|
||||
}, []);
|
||||
@@ -248,7 +246,7 @@ export const ContainerList = () => {
|
||||
<div className='w-full h-full flex '>
|
||||
<div className='p-2 flex flex-col gap-2'>
|
||||
<Tooltip title='添加'>
|
||||
<IconButton variant='contained' onClick={onAdd} sx={{ padding: '8px' }}>
|
||||
<IconButton variant='contained' onClick={onAdd}>
|
||||
<PlusOutlined />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
@@ -264,56 +262,47 @@ export const ContainerList = () => {
|
||||
className='flex text-sm gap flex-col w-[400px] max-h-[400px] bg-white p-4 rounded-lg'
|
||||
key={item.id}
|
||||
onClick={() => {
|
||||
setCode(item.code);
|
||||
containerStore.setFormData(item);
|
||||
setCodeEdit(true);
|
||||
// containerStore.setFormData(item);
|
||||
}}>
|
||||
<div className='px-4 cursor-pointer'>
|
||||
<div
|
||||
className='font-bold flex items-center'
|
||||
onClick={(e) => {
|
||||
copy(item.code);
|
||||
e.stopPropagation();
|
||||
message.success('copy code success');
|
||||
}}>
|
||||
{item.title || '-'}
|
||||
<div className='font-thin card-key ml-3 text-xs'>{item.tags ? item.tags.join(', ') : ''}</div>
|
||||
<div className='font-thin card-key ml-3'>{item.tags ? item.tags.join(', ') : ''}</div>
|
||||
</div>
|
||||
<div className='font-light text-xs mt-2'>{item.description ? item.description : '-'}</div>
|
||||
</div>
|
||||
<div className='w-full text-xs'>
|
||||
<TextArea className='max-h-[240px] scrollbar' value={item.code} readonly />
|
||||
</div>
|
||||
|
||||
<div className='flex mt-2 '>
|
||||
<ButtonGroup variant='contained' color='primary'>
|
||||
<Button
|
||||
onClick={() => {
|
||||
// containerStore.publishData(item);
|
||||
}}>
|
||||
<SettingOutlined />
|
||||
</Button>
|
||||
<ButtonGroup
|
||||
variant='contained'
|
||||
color='primary'
|
||||
sx={{ color: 'white', '& .MuiButton-root': { color: 'white', minWidth: '32px', width: '32px', height: '32px', padding: '6px' } }}>
|
||||
<Tooltip title='编辑代码'>
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
containerStore.getOne(item.id);
|
||||
containerStore.setFormData(item);
|
||||
containerStore.setOpenDrawEdit(true);
|
||||
e.stopPropagation();
|
||||
}}>
|
||||
<Settings size={16} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip title='编辑'>
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
containerStore.setFormData(item);
|
||||
containerStore.setShowEdit(true);
|
||||
setCodeEdit(false);
|
||||
e.stopPropagation();
|
||||
}}>
|
||||
<EditOutlined />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
{/* <Tooltip title='预览'>
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
// navicate('/container/preview/' + item.id);
|
||||
window.open('/container/preview/' + item.id);
|
||||
e.stopPropagation();
|
||||
}}>
|
||||
<LinkOutlined />
|
||||
</Button>
|
||||
</Tooltip> */}
|
||||
<Tooltip title='发布到 user app当中'>
|
||||
<Tooltip title='添加到 user app当中'>
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
// containerStore.publishData(item);
|
||||
@@ -355,42 +344,12 @@ export const ContainerList = () => {
|
||||
<CardBlank className='w-[400px]' />
|
||||
</div>
|
||||
</div>
|
||||
<div className={clsx('bg-gray-100 border-l-gray-200 border-bg-slate-300 w-[600px] shark-0', !codeEdit && 'hidden')}>
|
||||
<div className='bg-white p-2'>
|
||||
<div className='mt-2 ml-2 flex gap-2'>
|
||||
<Tooltip title='返回'>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setCodeEdit(false);
|
||||
containerStore.setFormData({});
|
||||
}}>
|
||||
<LeftOutlined />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip title='保存'>
|
||||
<Button
|
||||
onClick={() => {
|
||||
containerStore.updateData({ ...containerStore.formData, code });
|
||||
}}>
|
||||
<SaveOutlined />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div className='h-[94%] p-2 rounded-2 shadow-xs'>
|
||||
<TextArea
|
||||
value={code}
|
||||
onChange={(value) => {
|
||||
setCode(value);
|
||||
}}
|
||||
className='h-full max-h-full scrollbar'
|
||||
style={{ overflow: 'auto' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<FormModal />
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<DrawEdit />
|
||||
</Suspense>
|
||||
<PublishFormModal />
|
||||
<FormModal />
|
||||
{contextHolder}
|
||||
</div>
|
||||
);
|
||||
|
||||
103
src/pages/container/module/DrawEdit.tsx
Normal file
103
src/pages/container/module/DrawEdit.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { BaseEditor } from '@kevisual/codemirror/editor/editor.ts';
|
||||
import { Drawer } from '@mui/material';
|
||||
import { useShallow } from 'zustand/shallow';
|
||||
import { useContainerStore } from '../store';
|
||||
import { Tooltip } from '@mui/material';
|
||||
import { IconButton } from '@kevisual/center-components/button/index.tsx';
|
||||
import { LeftOutlined, SaveOutlined } from '@ant-design/icons';
|
||||
|
||||
export const DrawEdit = () => {
|
||||
const editorElRef = useRef<HTMLDivElement>(null);
|
||||
const editorRef = useRef<BaseEditor>(null);
|
||||
const containerStore = useContainerStore(
|
||||
useShallow((state) => {
|
||||
return {
|
||||
openDrawEdit: state.openDrawEdit,
|
||||
setOpenDrawEdit: state.setOpenDrawEdit,
|
||||
data: state.data,
|
||||
updateData: state.updateData,
|
||||
};
|
||||
}),
|
||||
);
|
||||
const { openDrawEdit, setOpenDrawEdit } = containerStore;
|
||||
const [mount, setMount] = useState(false);
|
||||
useEffect(() => {
|
||||
if (openDrawEdit && editorElRef.current && !mount) {
|
||||
editorRef.current = new BaseEditor();
|
||||
editorRef.current.createEditor(editorElRef.current);
|
||||
setMount(true);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (editorRef.current) {
|
||||
editorRef.current.destroyEditor();
|
||||
}
|
||||
};
|
||||
}, [openDrawEdit]);
|
||||
|
||||
useEffect(() => {
|
||||
if (openDrawEdit && containerStore.data?.id && mount) {
|
||||
const editor = editorRef.current;
|
||||
const formData = containerStore.data;
|
||||
const fileType = editor?.getFileType(formData.title);
|
||||
const language = editor?.language;
|
||||
if (fileType && fileType !== language) {
|
||||
editor?.setLanguage(fileType, editorElRef.current!);
|
||||
} else {
|
||||
editor?.resetEditor(editorElRef.current!);
|
||||
}
|
||||
editor?.setContent(formData.code || '');
|
||||
}
|
||||
}, [openDrawEdit, containerStore.data?.id, mount]);
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
open={openDrawEdit}
|
||||
ModalProps={{
|
||||
keepMounted: true, // 保持挂载
|
||||
hideBackdrop: true, // 移除mask
|
||||
disableEnforceFocus: true, // 允许操作背景内容
|
||||
disableAutoFocus: true, // 防止自动聚焦
|
||||
}}
|
||||
anchor='right'>
|
||||
<div className='bg-secondary w-[600px] h-[48px]'>
|
||||
<div className='text-white flex p-2'>
|
||||
<div className='ml-2 flex gap-2'>
|
||||
<Tooltip title='返回'>
|
||||
<IconButton
|
||||
sx={{
|
||||
'&:hover': {
|
||||
color: 'primary.main',
|
||||
},
|
||||
}}
|
||||
onClick={() => {
|
||||
setOpenDrawEdit(false);
|
||||
}}>
|
||||
<LeftOutlined />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title='保存'>
|
||||
<IconButton
|
||||
sx={{
|
||||
'&:hover': {
|
||||
color: 'primary.main',
|
||||
},
|
||||
}}
|
||||
onClick={() => {
|
||||
const code = editorRef.current?.getContent();
|
||||
containerStore.updateData({ id: containerStore.data.id, code }, { refresh: false });
|
||||
}}>
|
||||
<SaveOutlined />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className='flex-1 ml-2 flex items-center'>{containerStore.data?.title}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='border-primary' style={{ height: 'calc(100% - 50px)' }} ref={editorElRef}></div>
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
|
||||
export default DrawEdit;
|
||||
@@ -12,8 +12,13 @@ type ContainerStore = {
|
||||
setLoading: (loading: boolean) => void;
|
||||
list: any[];
|
||||
getList: () => Promise<void>;
|
||||
updateData: (data: any, opts?: { closePublish?: boolean; closeEdit?: boolean }) => Promise<Boolean>;
|
||||
updateData: (data: any, opts?: { closePublish?: boolean; closeEdit?: boolean, refresh?: boolean }) => Promise<any>;
|
||||
deleteData: (id: string) => Promise<void>;
|
||||
openDrawEdit: boolean;
|
||||
setOpenDrawEdit: (openDrawEdit: boolean) => void;
|
||||
getOne: (id: string) => Promise<void>;
|
||||
data: any;
|
||||
setData: (data: any) => void;
|
||||
};
|
||||
export const useContainerStore = create<ContainerStore>((set, get) => {
|
||||
return {
|
||||
@@ -43,6 +48,7 @@ export const useContainerStore = create<ContainerStore>((set, get) => {
|
||||
const { getList } = get();
|
||||
const closePublish = opts?.closePublish ?? true;
|
||||
const closeEdit = opts?.closeEdit ?? true;
|
||||
const refresh = opts?.refresh ?? true;
|
||||
const res = await query.post({
|
||||
path: 'container',
|
||||
key: 'update',
|
||||
@@ -51,7 +57,9 @@ export const useContainerStore = create<ContainerStore>((set, get) => {
|
||||
if (res.code === 200) {
|
||||
message.success('Success');
|
||||
set({ formData: res.data });
|
||||
getList();
|
||||
if (refresh) {
|
||||
getList();
|
||||
}
|
||||
if (closePublish) {
|
||||
set({ showPublish: false });
|
||||
}
|
||||
@@ -61,7 +69,7 @@ export const useContainerStore = create<ContainerStore>((set, get) => {
|
||||
} else {
|
||||
message.error(res.message || 'Request failed');
|
||||
}
|
||||
return res.code === 200;
|
||||
return res;
|
||||
},
|
||||
deleteData: async (id) => {
|
||||
const { getList } = get();
|
||||
@@ -77,5 +85,23 @@ export const useContainerStore = create<ContainerStore>((set, get) => {
|
||||
message.error(res.message || 'Request failed');
|
||||
}
|
||||
},
|
||||
openDrawEdit: false,
|
||||
setOpenDrawEdit: (openDrawEdit) => set({ openDrawEdit }),
|
||||
getOne: async (id) => {
|
||||
set({ loading: true, data: {} });
|
||||
const res = await query.post({
|
||||
path: 'container',
|
||||
key: 'get',
|
||||
id,
|
||||
});
|
||||
set({ loading: false });
|
||||
if (res.code === 200) {
|
||||
set({ data: res.data });
|
||||
} else {
|
||||
message.error(res.message || 'Request failed');
|
||||
}
|
||||
},
|
||||
data: {},
|
||||
setData: (data) => set({ data }),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,33 +1,35 @@
|
||||
import clsx from 'clsx';
|
||||
import { useNewNavigate } from '@/modules';
|
||||
const serverList = ['container', 'map'];
|
||||
const serverPath = [
|
||||
{
|
||||
path: 'container',
|
||||
links: ['edit/list', 'preview/:id', 'edit/:id'],
|
||||
},
|
||||
{
|
||||
path: 'app',
|
||||
links: ['edit/list', ':app/version/list'],
|
||||
},
|
||||
{
|
||||
path: 'file',
|
||||
links: ['edit/list'],
|
||||
},
|
||||
{
|
||||
path: 'map',
|
||||
links: ['/'],
|
||||
},
|
||||
{
|
||||
path: 'org',
|
||||
links: ['edit/list'],
|
||||
},
|
||||
];
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const ServerPath = () => {
|
||||
const navigate = useNewNavigate();
|
||||
const { t } = useTranslation();
|
||||
const serverPath = [
|
||||
{
|
||||
path: 'container',
|
||||
links: ['edit/list', 'preview/:id', 'edit/:id'],
|
||||
},
|
||||
{
|
||||
path: 'app',
|
||||
links: ['edit/list', ':app/version/list'],
|
||||
},
|
||||
{
|
||||
path: 'file',
|
||||
links: ['edit/list'],
|
||||
},
|
||||
{
|
||||
path: 'map',
|
||||
links: ['/'],
|
||||
},
|
||||
{
|
||||
path: 'org',
|
||||
links: ['edit/list'],
|
||||
},
|
||||
];
|
||||
return (
|
||||
<div className='p-2 w-full h-full bg-gray-200'>
|
||||
<h1 className='p-4 w-1/2 m-auto h1'>Site Map</h1>
|
||||
<div className='p-2 w-full h-full bg-gray-200 text-primary'>
|
||||
<h1 className='p-4 w-1/2 m-auto h1'>{t('Site Map')}</h1>
|
||||
<div className='w-1/2 m-auto bg-white p-4 border rounded-md shadow-md min-w-[700px] max-h-[80vh] overflow-auto scrollbar'>
|
||||
<div className='flex flex-col w-full'>
|
||||
{serverPath.map((item) => {
|
||||
@@ -42,7 +44,6 @@ const ServerPath = () => {
|
||||
if (hasId) {
|
||||
return;
|
||||
}
|
||||
console.log('link', link);
|
||||
if (link === '/') {
|
||||
navigate(`/${item.path}`);
|
||||
return;
|
||||
@@ -69,18 +70,3 @@ const ServerPath = () => {
|
||||
);
|
||||
};
|
||||
export const App = ServerPath;
|
||||
export const ServerList = () => {
|
||||
return (
|
||||
<div className='p-2 w-full h-full bg-gray-200'>
|
||||
<div className='flex flex-col w-1/2 m-auto bg-white p-4 border rounded-md shadow-md'>
|
||||
{serverList.map((item) => {
|
||||
return (
|
||||
<div key={item} className='flex flex-col'>
|
||||
<div>{item}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
import { Button, Input, Modal, Table, Tooltip } from 'antd';
|
||||
import { Fragment, useEffect, useMemo, useState } from 'react';
|
||||
import { message } from '@/modules/message';
|
||||
import { Input, Modal } from 'antd';
|
||||
import { Fragment, useEffect, useState } from 'react';
|
||||
import { useOrgStore } from '../store';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { Form } from 'antd';
|
||||
import { useNewNavigate } from '@/modules';
|
||||
import {
|
||||
EditOutlined,
|
||||
SettingOutlined,
|
||||
LinkOutlined,
|
||||
SaveOutlined,
|
||||
DeleteOutlined,
|
||||
LeftOutlined,
|
||||
PlusOutlined,
|
||||
SwapOutlined,
|
||||
UnorderedListOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Tooltip, Button, ButtonGroup, Dialog, DialogTitle, DialogContent } from '@mui/material';
|
||||
import { IconButton } from '@kevisual/center-components/button/index.tsx';
|
||||
import EditOutlined from '@ant-design/icons/EditOutlined';
|
||||
import SaveOutlined from '@ant-design/icons/SaveOutlined';
|
||||
import DeleteOutlined from '@ant-design/icons/DeleteOutlined';
|
||||
import LeftOutlined from '@ant-design/icons/LeftOutlined';
|
||||
import PlusOutlined from '@ant-design/icons/PlusOutlined';
|
||||
import SwapOutlined from '@ant-design/icons/SwapOutlined';
|
||||
import UnorderedListOutlined from '@ant-design/icons/UnorderedListOutlined';
|
||||
import clsx from 'clsx';
|
||||
import { isObjectNull } from '@/utils/is-null';
|
||||
import { CardBlank } from '@/components/card/CardBlank';
|
||||
@@ -23,6 +21,7 @@ import { useLayoutStore } from '@/modules/layout/store';
|
||||
|
||||
const FormModal = () => {
|
||||
const [form] = Form.useForm();
|
||||
const { t } = useTranslation();
|
||||
const userStore = useOrgStore(
|
||||
useShallow((state) => {
|
||||
return {
|
||||
@@ -53,42 +52,40 @@ const FormModal = () => {
|
||||
};
|
||||
const isEdit = userStore.formData.id;
|
||||
return (
|
||||
<Modal
|
||||
title={isEdit ? 'Edit' : 'Add'}
|
||||
open={userStore.showEdit}
|
||||
onClose={() => userStore.setShowEdit(false)}
|
||||
destroyOnClose
|
||||
footer={false}
|
||||
width={800}
|
||||
onCancel={onClose}>
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={onFinish}
|
||||
labelCol={{
|
||||
span: 4,
|
||||
}}
|
||||
wrapperCol={{
|
||||
span: 20,
|
||||
}}>
|
||||
<Form.Item name='id' hidden>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name='username' label='username'>
|
||||
<Input disabled={isEdit} />
|
||||
</Form.Item>
|
||||
<Form.Item name='description' label='description'>
|
||||
<Input.TextArea rows={4} />
|
||||
</Form.Item>
|
||||
<Form.Item label=' ' colon={false}>
|
||||
<Button type='primary' htmlType='submit'>
|
||||
Submit
|
||||
</Button>
|
||||
<Button className='ml-2' htmlType='reset' onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
<Dialog open={userStore.showEdit} onClose={() => userStore.setShowEdit(false)}>
|
||||
<DialogTitle>{isEdit ? 'Edit' : 'Add'}</DialogTitle>
|
||||
<DialogContent sx={{ padding: '20px', minWidth: '600px' }}>
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={onFinish}
|
||||
labelCol={{
|
||||
span: 4,
|
||||
}}
|
||||
wrapperCol={{
|
||||
span: 20,
|
||||
}}>
|
||||
<Form.Item name='id' hidden>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name='username' label='username'>
|
||||
<Input disabled={isEdit} />
|
||||
</Form.Item>
|
||||
<Form.Item name='description' label='description'>
|
||||
<Input.TextArea rows={4} />
|
||||
</Form.Item>
|
||||
<Form.Item label=' ' colon={false}>
|
||||
<div className='flex gap-2'>
|
||||
<Button variant='contained' type='submit'>
|
||||
{t('Submit')}
|
||||
</Button>
|
||||
<Button className='ml-2' onClick={onClose}>
|
||||
{t('Cancel')}
|
||||
</Button>
|
||||
</div>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
export const List = () => {
|
||||
@@ -125,10 +122,15 @@ export const List = () => {
|
||||
userStore.setFormData({});
|
||||
userStore.setShowEdit(true);
|
||||
};
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className='w-full h-full flex'>
|
||||
<div className='p-2'>
|
||||
<Button onClick={onAdd} icon={<PlusOutlined />}></Button>
|
||||
<Tooltip title={t('Add Org')}>
|
||||
<IconButton onClick={onAdd} variant='contained'>
|
||||
<PlusOutlined />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className='flex grow overflow-hidden h-full'>
|
||||
<div className='grow overflow-auto scrollbar bg-gray-100'>
|
||||
@@ -157,24 +159,29 @@ export const List = () => {
|
||||
<div className='font-light text-xs mt-2'>{item.description ? item.description : '-'}</div>
|
||||
</div>
|
||||
<div className='flex mt-2 '>
|
||||
<Button.Group>
|
||||
<Tooltip title='Edit'>
|
||||
<ButtonGroup
|
||||
variant='contained'
|
||||
color='primary'
|
||||
sx={{ color: 'white', '& .MuiButton-root': { color: 'white', minWidth: '32px', width: '32px', height: '32px', padding: '6px' } }}>
|
||||
<Tooltip title={t('Edit')}>
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
userStore.setFormData(item);
|
||||
userStore.setShowEdit(true);
|
||||
setCodeEdit(false);
|
||||
e.stopPropagation();
|
||||
}}
|
||||
icon={<EditOutlined />}></Button>
|
||||
}}>
|
||||
<EditOutlined />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip title='User List'>
|
||||
<Tooltip title={t('User List')}>
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
navicate(`/org/edit/user/${item.id}`);
|
||||
e.stopPropagation();
|
||||
}}
|
||||
icon={<UnorderedListOutlined />}></Button>
|
||||
}}>
|
||||
<UnorderedListOutlined />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
@@ -186,16 +193,18 @@ export const List = () => {
|
||||
},
|
||||
});
|
||||
e.stopPropagation();
|
||||
}}
|
||||
icon={<DeleteOutlined />}></Button>
|
||||
<Tooltip title='Switch to Org'>
|
||||
}}>
|
||||
<DeleteOutlined />
|
||||
</Button>
|
||||
<Tooltip title={t('Switch to Org')}>
|
||||
<Button
|
||||
icon={<SwapOutlined />}
|
||||
onClick={() => {
|
||||
layoutStore.switchOrg(item.username);
|
||||
}}></Button>
|
||||
}}>
|
||||
<SwapOutlined />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Button.Group>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</div>
|
||||
</Fragment>
|
||||
@@ -212,14 +221,15 @@ export const List = () => {
|
||||
onClick={() => {
|
||||
setCodeEdit(false);
|
||||
userStore.setFormData({});
|
||||
}}
|
||||
icon={<LeftOutlined />}></Button>
|
||||
}}>
|
||||
<LeftOutlined />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
console.log('save', userStore.formData);
|
||||
userStore.updateData({ ...userStore.formData, code });
|
||||
}}
|
||||
icon={<SaveOutlined />}></Button>
|
||||
}}>
|
||||
<SaveOutlined />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className='h-[94%] p-2 rounded-2 shadow-xs'>
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { useOrgStore } from '../store';
|
||||
import { useNavigation, useParams } from 'react-router';
|
||||
import { useParams } from 'react-router';
|
||||
import { useEffect } from 'react';
|
||||
import { Button, Input, Modal, Select, Space, Tooltip } from 'antd';
|
||||
import { Input, Modal, Select } from 'antd';
|
||||
import { message } from '@/modules/message';
|
||||
import { DeleteOutlined, EditOutlined, LeftOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import DeleteOutlined from '@ant-design/icons/DeleteOutlined';
|
||||
import LeftOutlined from '@ant-design/icons/LeftOutlined';
|
||||
import PlusOutlined from '@ant-design/icons/PlusOutlined';
|
||||
import { Tooltip, Button, ButtonGroup, Dialog, DialogTitle, DialogContent } from '@mui/material';
|
||||
import { IconButton } from '@kevisual/center-components/button/index.tsx';
|
||||
import { Form } from 'antd';
|
||||
import { useNewNavigate } from '@/modules';
|
||||
import { isObjectNull } from '@/utils/is-null';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import clsx from 'clsx';
|
||||
|
||||
const FormModal = () => {
|
||||
const [form] = Form.useForm();
|
||||
@@ -48,53 +54,52 @@ const FormModal = () => {
|
||||
userStore.setFormData({});
|
||||
};
|
||||
const isEdit = userStore.formData.id;
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Modal
|
||||
title={isEdit ? 'Edit' : 'Add'}
|
||||
open={userStore.showEdit}
|
||||
onClose={() => userStore.setShowEdit(false)}
|
||||
destroyOnClose
|
||||
footer={false}
|
||||
width={800}
|
||||
onCancel={onClose}>
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={onFinish}
|
||||
labelCol={{
|
||||
span: 4,
|
||||
}}
|
||||
wrapperCol={{
|
||||
span: 20,
|
||||
}}>
|
||||
<Form.Item name='id' hidden>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name='username' label='username'>
|
||||
<Input disabled={isEdit} />
|
||||
</Form.Item>
|
||||
<Form.Item name='role' label='role'>
|
||||
<Select
|
||||
options={[
|
||||
{
|
||||
label: 'admin',
|
||||
value: 'admin',
|
||||
},
|
||||
{
|
||||
label: 'member',
|
||||
value: 'member',
|
||||
},
|
||||
]}></Select>
|
||||
</Form.Item>
|
||||
<Form.Item label=' ' colon={false}>
|
||||
<Button type='primary' htmlType='submit'>
|
||||
Submit
|
||||
</Button>
|
||||
<Button className='ml-2' htmlType='reset' onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
<Dialog open={userStore.showEdit} onClose={() => userStore.setShowEdit(false)}>
|
||||
<DialogTitle>{isEdit ? 'Edit' : 'Add'}</DialogTitle>
|
||||
<DialogContent sx={{ padding: '20px', minWidth: '600px' }}>
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={onFinish}
|
||||
labelCol={{
|
||||
span: 4,
|
||||
}}
|
||||
wrapperCol={{
|
||||
span: 20,
|
||||
}}>
|
||||
<Form.Item name='id' hidden>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name='username' label='username'>
|
||||
<Input disabled={isEdit} />
|
||||
</Form.Item>
|
||||
<Form.Item name='role' label='role'>
|
||||
<Select
|
||||
options={[
|
||||
{
|
||||
label: 'admin',
|
||||
value: 'admin',
|
||||
},
|
||||
{
|
||||
label: 'member',
|
||||
value: 'member',
|
||||
},
|
||||
]}></Select>
|
||||
</Form.Item>
|
||||
<Form.Item label=' ' colon={false}>
|
||||
<div className='flex gap-2'>
|
||||
<Button variant='contained' type='submit'>
|
||||
{t('Submit')}
|
||||
</Button>
|
||||
<Button className='ml-2' onClick={onClose}>
|
||||
{t('Cancel')}
|
||||
</Button>
|
||||
</div>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -102,7 +107,7 @@ export const UserList = () => {
|
||||
const param = useParams();
|
||||
const navicate = useNewNavigate();
|
||||
const [modal, contextHolder] = Modal.useModal();
|
||||
|
||||
const { t } = useTranslation();
|
||||
const orgStore = useOrgStore(
|
||||
useShallow((state) => {
|
||||
return {
|
||||
@@ -128,56 +133,50 @@ export const UserList = () => {
|
||||
};
|
||||
}, []);
|
||||
return (
|
||||
<div className='w-full h-full bg-gray-200 flex'>
|
||||
<div className='w-full h-full bg-gray-100 flex text-primary'>
|
||||
<div className='p-2 bg-white mr-2'>
|
||||
<Button
|
||||
icon={<PlusOutlined />}
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
orgStore.setUserFormData({});
|
||||
orgStore.setShowUserEdit(true);
|
||||
}}></Button>
|
||||
}}>
|
||||
<PlusOutlined />
|
||||
</IconButton>
|
||||
</div>
|
||||
<div className='p-4 pt-12 grow relative'>
|
||||
<div className='absolute top-2'>
|
||||
<Tooltip title='返回'>
|
||||
<Button
|
||||
<Tooltip title={t('Back')}>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
navicate(-1);
|
||||
}}
|
||||
icon={<LeftOutlined />}></Button>
|
||||
}}>
|
||||
<LeftOutlined />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className='p-4 bg-white rounded-lg border border-gray-200 shadow-md h-full'>
|
||||
<div className='p-4 bg-white rounded-lg border shadow-md h-full'>
|
||||
<div className='flex gap-4'>
|
||||
{orgStore.users.map((item) => {
|
||||
const isOwner = item.role === 'owner';
|
||||
return (
|
||||
<div key={item.id} className='card w-[300px] border-t border-gray-200 justify-between p-2 border-b'>
|
||||
<div key={item.id} className='card w-[300px] justify-between p-2 '>
|
||||
<div
|
||||
className='card-title capitalize truncate cursor-pointer'
|
||||
onClick={() => {
|
||||
copy(item.username);
|
||||
}}>
|
||||
username: {item.username}
|
||||
{t('Username')}: {item.username}
|
||||
</div>
|
||||
<div className='flex gap-2 capitalize'>{item.role || '-'}</div>
|
||||
<div className='mt-2'>
|
||||
<Space.Compact>
|
||||
{/* <Tooltip title='Edit'>
|
||||
<Button
|
||||
icon={<EditOutlined />}
|
||||
disabled={isOwner}
|
||||
onClick={() => {
|
||||
orgStore.setShowEdit(true);
|
||||
orgStore.setUserFormData(item);
|
||||
}}></Button>
|
||||
</Tooltip> */}
|
||||
<ButtonGroup
|
||||
variant='contained'
|
||||
color='primary'
|
||||
sx={{ color: 'white', '& .MuiButton-root': { color: 'white', minWidth: '32px', width: '32px', height: '32px', padding: '6px' } }}>
|
||||
<Tooltip title='delete'>
|
||||
<Button
|
||||
icon={<DeleteOutlined />}
|
||||
<IconButton
|
||||
disabled={isOwner}
|
||||
onClick={() => {
|
||||
// o-NDO62XGeyEQoz_Sytz-1UUB7kw
|
||||
modal.confirm({
|
||||
title: 'Delete',
|
||||
content: 'Are you sure?',
|
||||
@@ -185,9 +184,11 @@ export const UserList = () => {
|
||||
orgStore.removeUser(item.id);
|
||||
},
|
||||
});
|
||||
}}></Button>
|
||||
}}>
|
||||
<DeleteOutlined />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Space.Compact>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -96,7 +96,6 @@ export const useOrgStore = create<OrgStore>((set, get) => {
|
||||
if (res.code === 200) {
|
||||
const { org, users } = res.data || {};
|
||||
set({ org, users });
|
||||
message.success('load success');
|
||||
} else {
|
||||
message.error(res.message || 'Request failed');
|
||||
}
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { Button, Input, Modal, Space } from 'antd';
|
||||
import { Fragment, useEffect, useMemo, useState } from 'react';
|
||||
import { Input, Modal } from 'antd';
|
||||
import { Fragment, useEffect, useState } from 'react';
|
||||
import { useUserStore } from '../store';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { Form } from 'antd';
|
||||
import { useNewNavigate } from '@/modules';
|
||||
import EditOutlined from '@ant-design/icons/EditOutlined';
|
||||
import SettingOutlined from '@ant-design/icons/SettingOutlined';
|
||||
import LinkOutlined from '@ant-design/icons/LinkOutlined';
|
||||
import SaveOutlined from '@ant-design/icons/SaveOutlined';
|
||||
import DeleteOutlined from '@ant-design/icons/DeleteOutlined';
|
||||
import LeftOutlined from '@ant-design/icons/LeftOutlined';
|
||||
@@ -14,8 +11,9 @@ import PlusOutlined from '@ant-design/icons/PlusOutlined';
|
||||
import clsx from 'clsx';
|
||||
import { isObjectNull } from '@/utils/is-null';
|
||||
import { CardBlank } from '@kevisual/center-components/card/CardBlank.tsx';
|
||||
import { message } from '@/modules/message';
|
||||
import { Dialog } from '@mui/material';
|
||||
import { Dialog, ButtonGroup, Button, DialogContent, DialogTitle } from '@mui/material';
|
||||
import { IconButton } from '@kevisual/center-components/button/index.tsx';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
const FormModal = () => {
|
||||
const [form] = Form.useForm();
|
||||
const userStore = useUserStore(
|
||||
@@ -47,9 +45,9 @@ const FormModal = () => {
|
||||
userStore.setFormData({});
|
||||
};
|
||||
const isEdit = userStore.formData.id;
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Dialog
|
||||
title={isEdit ? 'Edit' : 'Add'}
|
||||
open={userStore.showEdit}
|
||||
onClose={() => userStore.setShowEdit(false)}
|
||||
sx={{
|
||||
@@ -57,33 +55,36 @@ const FormModal = () => {
|
||||
width: '800px',
|
||||
},
|
||||
}}>
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={onFinish}
|
||||
labelCol={{
|
||||
span: 4,
|
||||
}}
|
||||
wrapperCol={{
|
||||
span: 20,
|
||||
}}>
|
||||
<Form.Item name='id' hidden>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name='username' label='username'>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name='description' label='description'>
|
||||
<Input.TextArea rows={4} />
|
||||
</Form.Item>
|
||||
<Form.Item label=' ' colon={false}>
|
||||
<Button type='primary' htmlType='submit'>
|
||||
Submit
|
||||
</Button>
|
||||
<Button className='ml-2' htmlType='reset' onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<DialogTitle>{isEdit ? t('Edit') : t('Add')}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={onFinish}
|
||||
labelCol={{
|
||||
span: 4,
|
||||
}}
|
||||
wrapperCol={{
|
||||
span: 20,
|
||||
}}>
|
||||
<Form.Item name='id' hidden>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name='username' label='username'>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name='description' label='description'>
|
||||
<Input.TextArea rows={4} />
|
||||
</Form.Item>
|
||||
<Form.Item label=' ' colon={false}>
|
||||
<Button type='submit' variant='contained'>
|
||||
{t('Submit')}
|
||||
</Button>
|
||||
<Button className='ml-2' type='reset' onClick={onClose}>
|
||||
{t('Cancel')}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -116,7 +117,9 @@ export const List = () => {
|
||||
return (
|
||||
<div className='w-full h-full flex'>
|
||||
<div className='p-2'>
|
||||
<Button onClick={onAdd} icon={<PlusOutlined />}></Button>
|
||||
<IconButton onClick={onAdd} variant='contained'>
|
||||
<PlusOutlined />
|
||||
</IconButton>
|
||||
</div>
|
||||
<div className='flex grow overflow-hidden h-full'>
|
||||
<div className='grow overflow-auto scrollbar bg-gray-100'>
|
||||
@@ -145,15 +148,19 @@ export const List = () => {
|
||||
<div className='font-light text-xs mt-2'>{item.description ? item.description : '-'}</div>
|
||||
</div>
|
||||
<div className='flex mt-2 '>
|
||||
<Space.Compact>
|
||||
<ButtonGroup
|
||||
variant='contained'
|
||||
color='primary'
|
||||
sx={{ color: 'white', '& .MuiButton-root': { color: 'white', minWidth: '32px', width: '32px', height: '32px', padding: '6px' } }}>
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
userStore.setFormData(item);
|
||||
userStore.setShowEdit(true);
|
||||
setCodeEdit(false);
|
||||
e.stopPropagation();
|
||||
}}
|
||||
icon={<EditOutlined />}></Button>
|
||||
}}>
|
||||
<EditOutlined />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
modal.confirm({
|
||||
@@ -164,9 +171,10 @@ export const List = () => {
|
||||
},
|
||||
});
|
||||
e.stopPropagation();
|
||||
}}
|
||||
icon={<DeleteOutlined />}></Button>
|
||||
</Space.Compact>
|
||||
}}>
|
||||
<DeleteOutlined />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</div>
|
||||
</Fragment>
|
||||
@@ -187,14 +195,16 @@ export const List = () => {
|
||||
onClick={() => {
|
||||
setCodeEdit(false);
|
||||
userStore.setFormData({});
|
||||
}}
|
||||
icon={<LeftOutlined />}></Button>
|
||||
}}>
|
||||
<LeftOutlined />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
console.log('save', userStore.formData);
|
||||
userStore.updateData({ ...userStore.formData, code });
|
||||
}}
|
||||
icon={<SaveOutlined />}></Button>
|
||||
}}>
|
||||
<SaveOutlined />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className='h-[94%] p-2 rounded-2 shadow-xs'>
|
||||
|
||||
@@ -1,15 +1,25 @@
|
||||
import { Button, Form, Input } from 'antd';
|
||||
import { TextField } from '@mui/material';
|
||||
import { useForm, Controller } from 'react-hook-form';
|
||||
import { Button } from '@mui/material';
|
||||
import { useUserStore } from '../store';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { isObjectNull } from '@/utils/is-null';
|
||||
import { useLayoutStore } from '@/modules/layout/store';
|
||||
import { AvatarUpload } from '../module/AvatarUpload';
|
||||
import UploadOutlined from '@ant-design/icons/UploadOutlined';
|
||||
import PandaPNG from '@/assets/panda.png';
|
||||
import { FileUpload } from '../module/FileUpload';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const Profile = () => {
|
||||
const [form] = Form.useForm();
|
||||
const { t } = useTranslation();
|
||||
const { control, handleSubmit, setValue, reset, formState, getValues } = useForm({
|
||||
defaultValues: {
|
||||
username: '',
|
||||
avatar: '',
|
||||
description: '',
|
||||
},
|
||||
});
|
||||
const ref = useRef<any>(null);
|
||||
const userStore = useUserStore(
|
||||
useShallow((state) => {
|
||||
@@ -32,21 +42,29 @@ export const Profile = () => {
|
||||
};
|
||||
}),
|
||||
);
|
||||
// const avatar = layoutStore.me?.avatar;
|
||||
useEffect(() => {
|
||||
const fromData = layoutStore.me;
|
||||
if (isObjectNull(fromData)) {
|
||||
form.setFieldsValue({});
|
||||
reset({
|
||||
username: '',
|
||||
avatar: '',
|
||||
description: '',
|
||||
});
|
||||
} else {
|
||||
form.setFieldsValue(fromData);
|
||||
reset({
|
||||
username: fromData.username,
|
||||
avatar: fromData.avatar || '',
|
||||
description: fromData.description || '',
|
||||
});
|
||||
}
|
||||
setAvatar(fromData.avatar || '');
|
||||
}, [layoutStore.me]);
|
||||
}, [layoutStore.me, setValue]);
|
||||
const onChange = (path: string) => {
|
||||
let url = '/resources/' + path;
|
||||
console.log('path', url);
|
||||
form.setFieldsValue({ avatar: url });
|
||||
setAvatar(url + '?t=' + new Date().getTime());
|
||||
const url = path + '?t=' + new Date().getTime();
|
||||
setAvatar(url);
|
||||
setValue('avatar', url);
|
||||
const values = getValues();
|
||||
onFinish(values);
|
||||
};
|
||||
const onFinish = async (values) => {
|
||||
const newMe = await userStore.updateSelf(values);
|
||||
@@ -55,48 +73,51 @@ export const Profile = () => {
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className='w-full h-full bg-gray-200 p-4'>
|
||||
<div className='border shadow-lg p-4 bg-white rounded-lg'>
|
||||
<div className='text-2xl'>Profile</div>
|
||||
<div className='text-sm text-gray-500'>Edit your profile</div>
|
||||
<div className='w-full h-full bg-amber-50 p-4 text-primary'>
|
||||
<div className=' shadow-lg p-4 bg-white rounded-lg'>
|
||||
<div className='text-2xl'>{t('Profile')}</div>
|
||||
<div className='text-sm text-secondary'>{t('Edit your profile')}</div>
|
||||
<div className='flex gap-4'>
|
||||
<div className='w-[600px] p-4 border mt-2 '>
|
||||
<div className='w-full my-4'>
|
||||
{avatar && <img className='w-20 h-20 mx-auto rounded-full' src={avatar} alt='avatar' />}
|
||||
{!avatar && <img className='w-20 h-20 mx-auto rounded-full' src={PandaPNG} alt='avatar' />}
|
||||
</div>
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={onFinish}
|
||||
labelCol={{
|
||||
span: 4,
|
||||
}}>
|
||||
<Form.Item label='Name' name='username'>
|
||||
<Input className='w-full border rounded-lg p-2' disabled />
|
||||
</Form.Item>
|
||||
<Form.Item label='Avatar' name='avatar'>
|
||||
<Input
|
||||
addonAfter={
|
||||
<div>
|
||||
<UploadOutlined
|
||||
onClick={() => {
|
||||
ref.current?.open?.();
|
||||
}}
|
||||
/>
|
||||
<FileUpload ref={ref} onChange={onChange} />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label='Description' name='description'>
|
||||
<Input.TextArea rows={4} />
|
||||
</Form.Item>
|
||||
<Form.Item label=' ' colon={false}>
|
||||
<Button className='' htmlType='submit'>
|
||||
保存
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<form onSubmit={handleSubmit(onFinish)} className='flex flex-col gap-4'>
|
||||
<Controller
|
||||
name='username'
|
||||
control={control}
|
||||
render={({ field }) => <TextField {...field} label={t('Name')} className='w-full border rounded-lg p-2' disabled />}
|
||||
/>
|
||||
<Controller
|
||||
name='avatar'
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
label={t('Avatar')}
|
||||
slotProps={{
|
||||
input: {
|
||||
endAdornment: (
|
||||
<div>
|
||||
<UploadOutlined
|
||||
onClick={() => {
|
||||
ref.current?.open?.();
|
||||
}}
|
||||
/>
|
||||
<FileUpload ref={ref} onChange={onChange} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller name='description' control={control} render={({ field }) => <TextField {...field} label={t('Description')} multiline rows={4} />} />
|
||||
<Button className='' type='submit' variant='contained'>
|
||||
{t('Save')}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { Button, Form, Input } from 'antd';
|
||||
import { useLoginStore } from '../store/login';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { useEffect } from 'react';
|
||||
import { isObjectNull } from '@/utils/is-null';
|
||||
import { LockOutlined, UserOutlined } from '@ant-design/icons';
|
||||
import LockOutlined from '@ant-design/icons/LockOutlined';
|
||||
import UserOutlined from '@ant-design/icons/UserOutlined';
|
||||
import { Button } from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TextField, InputAdornment } from '@mui/material';
|
||||
import { useForm, Controller } from 'react-hook-form';
|
||||
export const Login = () => {
|
||||
const [form] = Form.useForm();
|
||||
const { t } = useTranslation();
|
||||
const { control, handleSubmit } = useForm();
|
||||
const loginStore = useLoginStore(
|
||||
useShallow((state) => {
|
||||
return {
|
||||
@@ -15,50 +18,68 @@ export const Login = () => {
|
||||
};
|
||||
}),
|
||||
);
|
||||
useEffect(() => {
|
||||
const isNull = isObjectNull(loginStore.formData);
|
||||
if (isNull) {
|
||||
form.setFieldsValue({});
|
||||
} else {
|
||||
form.setFieldsValue(loginStore.formData);
|
||||
}
|
||||
}, [loginStore.formData]);
|
||||
|
||||
const onFinish = (values: any) => {
|
||||
loginStore.setFormData(values);
|
||||
loginStore.login();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='bg-slate-200 text-slate-900 w-full h-full overflow-hidden'>
|
||||
<div className='bg-gray-100 text-primary w-full h-full overflow-hidden'>
|
||||
<div className='w-full h-full absolute top-[10%] xl:top-[15%] 2xl:top-[18%] 3xl:top-[20%] '>
|
||||
<div className='w-[400px] mx-auto'>
|
||||
<h1 className='mb-4 tracking-widest text-center'>Login</h1>
|
||||
<h1 className='mb-4 tracking-widest text-center'>{t('Login')}</h1>
|
||||
<div className='card border-t-2 border-gray-200 pt-8 px-8'>
|
||||
<Form
|
||||
className='mt-2'
|
||||
form={form}
|
||||
onFinish={onFinish}
|
||||
labelCol={{
|
||||
span: 6,
|
||||
}}>
|
||||
<Form.Item label='' name='username'>
|
||||
<Input addonBefore={<UserOutlined />} placeholder='Username' />
|
||||
</Form.Item>
|
||||
<Form.Item label='' name='password'>
|
||||
<Input type='password' addonBefore={<LockOutlined />} placeholder='Password' />
|
||||
</Form.Item>
|
||||
<Form.Item label='' colon={false}>
|
||||
<div className='flex gap-2'>
|
||||
<Button
|
||||
type='primary'
|
||||
htmlType='submit'
|
||||
style={{
|
||||
background: '#84d5e8',
|
||||
}}>
|
||||
Login
|
||||
</Button>
|
||||
</div>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<form className='mt-2 flex flex-col gap-8' onSubmit={handleSubmit(onFinish)}>
|
||||
<Controller
|
||||
name='username'
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
label={t('Username')}
|
||||
variant='outlined'
|
||||
fullWidth
|
||||
slotProps={{
|
||||
input: {
|
||||
startAdornment: (
|
||||
<InputAdornment position='start'>
|
||||
<UserOutlined className='text-primary!' />
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name='password'
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
type='password'
|
||||
label={t('Password')}
|
||||
variant='outlined'
|
||||
fullWidth
|
||||
slotProps={{
|
||||
input: {
|
||||
startAdornment: (
|
||||
<InputAdornment position='start'>
|
||||
<LockOutlined className='text-primary!' />
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<div className='flex gap-2'>
|
||||
<Button type='submit' variant='contained'>
|
||||
{t('Login')}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { LoadingOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import { Flex, Upload } from 'antd';
|
||||
import { message } from '@/modules/message';
|
||||
import type { GetProp, UploadProps } from 'antd';
|
||||
|
||||
type FileType = Parameters<GetProp<UploadProps, 'beforeUpload'>>[0];
|
||||
|
||||
const getBase64 = (img: FileType, callback: (url: string) => void) => {
|
||||
const reader = new FileReader();
|
||||
reader.addEventListener('load', () => callback(reader.result as string));
|
||||
reader.readAsDataURL(img);
|
||||
};
|
||||
|
||||
const beforeUpload = (file: FileType) => {
|
||||
const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png';
|
||||
if (!isJpgOrPng) {
|
||||
message.error('You can only upload JPG/PNG file!');
|
||||
}
|
||||
const isLt2M = file.size / 1024 / 1024 < 2;
|
||||
if (!isLt2M) {
|
||||
message.error('Image must smaller than 2MB!');
|
||||
}
|
||||
return isJpgOrPng && isLt2M;
|
||||
};
|
||||
|
||||
export const AvatarUpload = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [imageUrl, setImageUrl] = useState<string>();
|
||||
|
||||
const handleChange: UploadProps['onChange'] = (info) => {
|
||||
if (info.file.status === 'uploading') {
|
||||
setLoading(true);
|
||||
return;
|
||||
}
|
||||
if (info.file.status === 'done') {
|
||||
// Get this url from response in real world.
|
||||
getBase64(info.file.originFileObj as FileType, (url) => {
|
||||
setLoading(false);
|
||||
setImageUrl(url);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const uploadButton = (
|
||||
<button style={{ border: 0, background: 'none' }} type='button'>
|
||||
{loading ? <LoadingOutlined /> : <PlusOutlined />}
|
||||
<div style={{ marginTop: 8 }}>Upload</div>
|
||||
</button>
|
||||
);
|
||||
const onAciton = async (file) => {
|
||||
console.log('file', file);
|
||||
return '';
|
||||
};
|
||||
const customAction = (file) => {
|
||||
console.log('file', file);
|
||||
};
|
||||
return (
|
||||
<Flex gap='middle' wrap>
|
||||
<Upload
|
||||
name='avatar'
|
||||
listType='picture-circle'
|
||||
className='avatar-uploader'
|
||||
multiple={false}
|
||||
showUploadList={false}
|
||||
action={onAciton}
|
||||
customRequest={customAction}
|
||||
beforeUpload={beforeUpload}
|
||||
onChange={handleChange}>
|
||||
{imageUrl ? <img src={imageUrl} alt='avatar' style={{ width: '100%' }} /> : uploadButton}
|
||||
</Upload>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
@@ -1,7 +1,6 @@
|
||||
import { message } from '@/modules/message';
|
||||
import { useImperativeHandle, useRef, forwardRef } from 'react';
|
||||
import type { GetProp, UploadProps } from 'antd';
|
||||
type FileTypeOrg = Parameters<GetProp<UploadProps, 'beforeUpload'>>[0];
|
||||
import { uploadFileChunked } from '@kevisual/resources/index.ts';
|
||||
|
||||
export type FileType = {
|
||||
name: string;
|
||||
@@ -10,7 +9,7 @@ export type FileType = {
|
||||
webkitRelativePath: string; // 包含name
|
||||
};
|
||||
|
||||
const beforeUpload = (file: FileTypeOrg) => {
|
||||
const beforeUpload = (file: any) => {
|
||||
const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png';
|
||||
if (!isJpgOrPng) {
|
||||
message.error('You can only upload JPG/PNG file!');
|
||||
@@ -33,22 +32,15 @@ export const FileUpload = forwardRef<any, Props>((props, ref) => {
|
||||
console.log(e.target.files);
|
||||
const file = e.target.files[0];
|
||||
const endType = file.name.split('.').pop();
|
||||
const formData = new FormData();
|
||||
formData.append('file', file, `avatar.${endType}`); // 保留文件夹路径
|
||||
const res = await fetch('/api/upload', {
|
||||
method: 'POST',
|
||||
body: formData, //
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + localStorage.getItem('token'),
|
||||
},
|
||||
}).then((res) => res.json());
|
||||
const filename = `avatar.${endType}`;
|
||||
const res = (await uploadFileChunked(file, {
|
||||
isPublic: true,
|
||||
filename,
|
||||
})) as any;
|
||||
console.log('res', res);
|
||||
if (res?.code === 200) {
|
||||
console.log('res', res);
|
||||
//
|
||||
const [file] = res.data;
|
||||
const { path } = file || {};
|
||||
props?.onChange?.(path);
|
||||
//
|
||||
const resource = res.data?.resource;
|
||||
props?.onChange?.(resource);
|
||||
} else {
|
||||
message.error(res.message || 'Request failed');
|
||||
}
|
||||
|
||||
@@ -53,13 +53,11 @@ export const useUserStore = create<UserStore>((set, get) => {
|
||||
}
|
||||
},
|
||||
updateSelf: async (data) => {
|
||||
const loaded = message.loading('Action in progress..', 0);
|
||||
const res = await query.post({
|
||||
path: 'user',
|
||||
key: 'updateSelf',
|
||||
data,
|
||||
});
|
||||
loaded();
|
||||
if (res.code === 200) {
|
||||
message.success('Success');
|
||||
set({ formData: res.data });
|
||||
|
||||
Reference in New Issue
Block a user