feat: 添加i18n,美化界面

This commit is contained in:
2025-03-20 02:29:01 +08:00
parent 27d9bdf54e
commit c206add7eb
56 changed files with 2743 additions and 928 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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