feat: add user manager

This commit is contained in:
xion 2025-02-28 15:31:32 +08:00
parent 5f7bd0de5f
commit bd58d5983c
26 changed files with 149 additions and 57 deletions

View File

@ -20,6 +20,7 @@
"@kevisual/codemirror": "^0.0.2",
"@kevisual/container": "1.0.0",
"@kevisual/query": "^0.0.6",
"@kevisual/system-ui": "^0.0.3",
"@kevisual/ui": "^0.0.2",
"@monaco-editor/react": "^4.7.0",
"@tailwindcss/vite": "^4.0.8",

12
pnpm-lock.yaml generated
View File

@ -29,6 +29,9 @@ importers:
'@kevisual/query':
specifier: ^0.0.6
version: 0.0.6
'@kevisual/system-ui':
specifier: ^0.0.3
version: 0.0.3
'@kevisual/ui':
specifier: ^0.0.2
version: 0.0.2
@ -631,6 +634,9 @@ packages:
'@kevisual/query@0.0.6':
resolution: {integrity: sha512-J+poZwIx4r956GDN5zI8zM0YyyxJqARZVVOFYplRThaQLqpsbRNYJoYiT5DekvBftxtFjs/OlI3RzcHDOv3Fjw==}
'@kevisual/system-ui@0.0.3':
resolution: {integrity: sha512-zRtUnL6wNe6R1W7X6eirDADZWeTmxZCNpLwxCLu30yeNuIhpFJdxHyOg0nX9aOZn6F0Kb6lB3Li2fZpKwdpk0w==}
'@kevisual/ui@0.0.2':
resolution: {integrity: sha512-MDZDQTrYToLyj3WhiVJQLJ0PUHiN4D0Z5yJIyGzzPewPGpP2xwNgKO1BFX37J95cGZckzCdZwTKP0XKAOq0QtA==}
@ -3166,6 +3172,12 @@ snapshots:
'@kevisual/query@0.0.6': {}
'@kevisual/system-ui@0.0.3':
dependencies:
dayjs: 1.11.13
lodash-es: 4.17.21
style-to-object: 1.0.8
'@kevisual/ui@0.0.2':
dependencies:
dayjs: 1.11.13

View File

@ -4,6 +4,7 @@ body {
height: 100%;
font-size: 16px;
font-family: 'Montserrat', sans-serif;
overflow: hidden;
}
#root {

View File

@ -1,7 +1,8 @@
import { useShallow } from 'zustand/react/shallow';
import { useLayoutStore } from './store';
import clsx from 'clsx';
import { Button, Dropdown, message } from 'antd';
import { Button, Dropdown } from 'antd';
import { message } from '@/modules/message';
import {
CloseOutlined,
CodeOutlined,

View File

@ -1,7 +1,8 @@
import { useShallow } from 'zustand/react/shallow';
import { useLayoutStore } from './store';
import clsx from 'clsx';
import { Button, message } from 'antd';
import { Button } from 'antd';
import { message } from '@/modules/message';
import {
AppstoreOutlined,
CloseOutlined,

View File

@ -1,6 +1,6 @@
import { query } from '@/modules/query';
import { message } from 'antd';
import { create } from 'zustand';
import { message } from '@/modules/message';
export const getIsMac = async () => {
// @ts-ignore
const userAgentData = navigator.userAgentData;
@ -37,7 +37,7 @@ export const usePlatformStore = create<PlatfromStore>((set) => {
init: async () => {
const mac = await getIsMac();
// @ts-ignore
const isElectron = getIsElectron()
const isElectron = getIsElectron();
set({ isMac: isElectron && mac, isElectron: isElectron, mount: true });
},
};

3
src/modules/message.ts Normal file
View File

@ -0,0 +1,3 @@
import { message } from '@kevisual/system-ui/dist/message';
export { message };

View File

@ -1,7 +1,7 @@
import { QueryClient } from '@kevisual/query';
import { modal } from './redirect-to-login';
import { create } from 'zustand';
import { message } from 'antd';
import { message } from './message';
export const query = new QueryClient();
query.beforeRequest = async (config) => {
if (config.headers) {

View File

@ -2,11 +2,12 @@ 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, message, Modal, Space, Tooltip } from 'antd';
import { Button, Form, Input, Modal, Space, 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';
const FormModal = () => {
const [form] = Form.useForm();

View File

@ -1,12 +1,13 @@
import { useShallow } from 'zustand/react/shallow';
import { useUserAppStore } from '../store';
import { useEffect } from 'react';
import { Button, Form, Input, message, Space, Modal, Select, Tooltip } from 'antd';
import { Button, Form, Input, Space, Modal, Select, Tooltip } from 'antd';
import { CodeOutlined, DeleteOutlined, EditOutlined, LinkOutlined, PlusOutlined, UnorderedListOutlined } from '@ant-design/icons';
import { isObjectNull } from '@/utils/is-null';
import { useNewNavigate } from '@/modules';
import { marked } from 'marked';
import clsx from 'clsx';
import { message } from '@/modules/message';
const FormModal = () => {
const [form] = Form.useForm();
const containerStore = useUserAppStore(

View File

@ -1,8 +1,8 @@
import { Button, message } from 'antd';
import { Button } from 'antd';
import { useCallback, useRef } from 'react';
import { useAppVersionStore } from '../store';
import { useShallow } from 'zustand/react/shallow';
import { message } from '@/modules/message';
export type FileType = {
name: string;
size: number;

View File

@ -1,8 +1,7 @@
import { create } from 'zustand';
import { query } from '@/modules';
import { message } from 'antd';
import { isObjectNull } from '@/utils/is-null';
import { message } from '@/modules/message';
type AppVersionStore = {
showEdit: boolean;
setShowEdit: (showEdit: boolean) => void;
@ -117,7 +116,7 @@ export const useAppVersionStore = create<AppVersionStore>((set, get) => {
key: 'publish',
data,
});
loaded();
setTimeout(loaded, 200);
if (res.code === 200) {
message.success('Success');
// getList();

View File

@ -1,6 +1,6 @@
import { create } from 'zustand';
import { query } from '@/modules';
import { message } from 'antd';
import { message } from '@/modules/message';
type UserAppStore = {
showEdit: boolean;
setShowEdit: (showEdit: boolean) => void;

View File

@ -1,4 +1,4 @@
import { Button, Input, message, Modal, Select, Space, Switch, Table, Tooltip } from 'antd';
import { Button, Input, Modal, Select, Space, Switch, Table, Tooltip } from 'antd';
import { Fragment, useEffect, useState } from 'react';
import { TextArea } from '../components/TextArea';
import { useContainerStore } from '../store';
@ -6,6 +6,7 @@ import { useShallow } from 'zustand/react/shallow';
import { Form } from 'antd';
import copy from 'copy-to-clipboard';
import { useNewNavigate } from '@/modules';
import { message } from '@/modules/message';
import {
EditOutlined,
SettingOutlined,

View File

@ -1,7 +1,7 @@
import { query } from '@/modules';
import { Select as AntSelect, message, SelectProps } from 'antd';
import { Select as AntSelect, SelectProps } from 'antd';
import { useEffect, useState } from 'react';
import { message } from '@/modules/message';
export const Select = (props: SelectProps) => {
const [options, setOptions] = useState<{ value: string; id: string }[]>([]);
useEffect(() => {

View File

@ -2,8 +2,8 @@ import { Container, RenderData, ContainerOne } from '@kevisual/container';
import { useEffect, useRef, useState } from 'react';
import { replace, useParams } from 'react-router';
import { query, ws, useStore } from '@/modules';
import { message } from 'antd';
import { message } from '@/modules/message';
export const useListener = (id?: string, opts?: any) => {
const { refresh } = opts || {};
const connected = useStore((state) => state.connected);

View File

@ -1,6 +1,6 @@
import { create } from 'zustand';
import { query } from '@/modules';
import { message } from 'antd';
import { message } from '@/modules/message';
import { sortBy } from 'lodash-es';
type FileStore = {
showEdit: boolean;

View File

@ -1,5 +1,6 @@
import { Button, Input, message, Modal, Table, Tooltip } from 'antd';
import { Button, Input, Modal, Table, Tooltip } from 'antd';
import { Fragment, useEffect, useMemo, useState } from 'react';
import { message } from '@/modules/message';
import { useOrgStore } from '../store';
import { useShallow } from 'zustand/react/shallow';
import { Form } from 'antd';
@ -204,7 +205,7 @@ export const List = () => {
<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', 'hidden')}>
<div className={clsx('bg-gray-100 border-gray-200 border-bg-slate-300 w-[600px] shark-0', !codeEdit && 'hidden', 'hidden')}>
<div className='bg-white p-2'>
<div className='mt-2 ml-2 flex gap-2'>
<Button

View File

@ -2,22 +2,24 @@ import { useShallow } from 'zustand/react/shallow';
import { useOrgStore } from '../store';
import { useNavigation, useParams } from 'react-router';
import { useEffect } from 'react';
import { Button, Input, message, Modal, Select, Tooltip } from 'antd';
import { Button, Input, Modal, Select, Space, Tooltip } from 'antd';
import { message } from '@/modules/message';
import { DeleteOutlined, EditOutlined, LeftOutlined, PlusOutlined } from '@ant-design/icons';
import { Form } from 'antd';
import { useNewNavigate } from '@/modules';
import { isObjectNull } from '@/utils/is-null';
import copy from 'copy-to-clipboard';
const FormModal = () => {
const [form] = Form.useForm();
const userStore = useOrgStore(
useShallow((state) => {
return {
showEdit: state.showEdit,
setShowEdit: state.setShowEdit,
formData: state.formData,
updateData: state.updateData,
setFormData: state.setFormData,
showEdit: state.showUserEdit,
setShowEdit: state.setShowUserEdit,
formData: state.userFormData,
setFormData: state.setUserFormData,
addUser: state.addUser,
};
}),
);
@ -31,7 +33,15 @@ const FormModal = () => {
}
}, [userStore.showEdit, userStore.formData]);
const onFinish = async (values: any) => {
userStore.updateData(values);
//
console.log(values);
const username = values.username;
const role = values.role;
if (!username) {
message.error('username is required');
return;
}
userStore.addUser({ username: username, role });
};
const onClose = () => {
userStore.setShowEdit(false);
@ -70,8 +80,8 @@ const FormModal = () => {
value: 'admin',
},
{
label: 'user',
value: 'user',
label: 'member',
value: 'member',
},
]}></Select>
</Form.Item>
@ -91,6 +101,8 @@ const FormModal = () => {
export const UserList = () => {
const param = useParams();
const navicate = useNewNavigate();
const [modal, contextHolder] = Modal.useModal();
const orgStore = useOrgStore(
useShallow((state) => {
return {
@ -98,8 +110,11 @@ export const UserList = () => {
org: state.org,
setOrgId: state.setOrgId,
getOrg: state.getOrg,
setFormData: state.setFormData,
setShowUserEdit: state.setShowUserEdit,
setUserFormData: state.setUserFormData,
setShowEdit: state.setShowEdit,
removeUser: state.removeUser,
addUser: state.addUser,
};
}),
);
@ -109,7 +124,7 @@ export const UserList = () => {
orgStore.getOrg();
}
return () => {
orgStore.setFormData({});
orgStore.setUserFormData({});
};
}, []);
return (
@ -118,8 +133,8 @@ export const UserList = () => {
<Button
icon={<PlusOutlined />}
onClick={() => {
// orgStore.setShowEdit(true);
message.info('Coming soon');
orgStore.setUserFormData({});
orgStore.setShowUserEdit(true);
}}></Button>
</div>
<div className='p-4 pt-12 grow relative'>
@ -132,36 +147,47 @@ export const UserList = () => {
icon={<LeftOutlined />}></Button>
</Tooltip>
</div>
<div className='p-4 bg-white rounded-lg border shadow-md h-full'>
<div className='p-4 bg-white rounded-lg border border-gray-200 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 justify-between p-2 border-b'>
<div className='card-title capitalize'>username: {item.username}</div>
<div key={item.id} className='card w-[300px] border-t border-gray-200 justify-between p-2 border-b'>
<div
className='card-title capitalize truncate cursor-pointer'
onClick={() => {
copy(item.username);
}}>
username: {item.username}
</div>
<div className='flex gap-2 capitalize'>{item.role || '-'}</div>
<div className='mt-2'>
<Button.Group>
<Tooltip title='Edit'>
<Space.Compact>
{/* <Tooltip title='Edit'>
<Button
icon={<EditOutlined />}
disabled={isOwner}
onClick={() => {
// orgStore.setFormData(item);
// orgStore.setShowEdit(true);
message.info('Coming soon');
orgStore.setShowEdit(true);
orgStore.setUserFormData(item);
}}></Button>
</Tooltip>
</Tooltip> */}
<Tooltip title='delete'>
<Button
icon={<DeleteOutlined />}
disabled={isOwner}
onClick={() => {
//
message.info('Coming soon');
// o-NDO62XGeyEQoz_Sytz-1UUB7kw
modal.confirm({
title: 'Delete',
content: 'Are you sure?',
onOk: () => {
orgStore.removeUser(item.id);
},
});
}}></Button>
</Tooltip>
</Button.Group>
</Space.Compact>
</div>
</div>
);
@ -171,6 +197,7 @@ export const UserList = () => {
</div>
<FormModal />
{contextHolder}
</div>
);
};

View File

@ -1,11 +1,15 @@
import { create } from 'zustand';
import { query } from '@/modules';
import { message } from 'antd';
import { message } from '@/modules/message';
type OrgStore = {
showEdit: boolean;
setShowEdit: (showEdit: boolean) => void;
formData: any;
setFormData: (formData: any) => void;
showUserEdit: boolean;
setShowUserEdit: (showUserEdit: boolean) => void;
userFormData: any;
setUserFormData: (userFormData: any) => void;
loading: boolean;
setLoading: (loading: boolean) => void;
list: any[];
@ -18,6 +22,8 @@ type OrgStore = {
orgId: string;
setOrgId: (orgId: string) => void;
getOrg: () => Promise<any>;
addUser: (data: { userId?: string; username?: string; role?: string }) => Promise<void>;
removeUser: (userId: string) => Promise<void>;
};
export const useOrgStore = create<OrgStore>((set, get) => {
return {
@ -27,6 +33,10 @@ export const useOrgStore = create<OrgStore>((set, get) => {
setFormData: (formData) => set({ formData }),
loading: false,
setLoading: (loading) => set({ loading }),
showUserEdit: false,
setShowUserEdit: (showUserEdit) => set({ showUserEdit }),
userFormData: {},
setUserFormData: (userFormData) => set({ userFormData }),
list: [],
getList: async () => {
set({ loading: true });
@ -84,7 +94,7 @@ export const useOrgStore = create<OrgStore>((set, get) => {
key: 'get',
id: orgId,
});
loaded();
setTimeout(loaded, 200);
if (res.code === 200) {
const { org, users } = res.data || {};
set({ org, users });
@ -92,5 +102,37 @@ export const useOrgStore = create<OrgStore>((set, get) => {
message.error(res.message || 'Request failed');
}
},
addUser: async (data) => {
const { orgId } = get();
const res = await query.post({
path: 'org-user',
key: 'operate',
data: { orgId, ...data, action: 'add' },
});
if (res.code === 200) {
message.success('Success');
get().getOrg();
} else {
message.error(res.message || 'Request failed');
}
},
removeUser: async (userId: string) => {
const { orgId } = get();
const res = await query.post({
path: 'org-user',
key: 'operate',
data: {
orgId,
userId,
action: 'remove',
},
});
if (res.code === 200) {
message.success('Success');
get().getOrg();
} else {
message.error(res.message || 'Request failed');
}
},
};
});

View File

@ -1,4 +1,4 @@
import { Button, Input, message, Modal, Space, Table } from 'antd';
import { Button, Input, Modal, Space, Table } from 'antd';
import { Fragment, useEffect, useMemo, useState } from 'react';
import { useUserStore } from '../store';
import { useShallow } from 'zustand/react/shallow';
@ -8,7 +8,7 @@ import { EditOutlined, SettingOutlined, LinkOutlined, SaveOutlined, DeleteOutlin
import clsx from 'clsx';
import { isObjectNull } from '@/utils/is-null';
import { CardBlank } from '@/components/card/CardBlank';
import { message } from '@/modules/message';
const FormModal = () => {
const [form] = Form.useForm();
const userStore = useUserStore(

View File

@ -32,7 +32,7 @@ export const Login = () => {
<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>
<div className='card border-t-2 pt-8 px-8'>
<div className='card border-t-2 border-gray-200 pt-8 px-8'>
<Form
className='mt-2'
form={form}

View File

@ -1,6 +1,7 @@
import { useState } from 'react';
import { LoadingOutlined, PlusOutlined } from '@ant-design/icons';
import { Flex, message, Upload } from 'antd';
import { Flex, Upload } from 'antd';
import { message } from '@/modules/message';
import type { GetProp, UploadProps } from 'antd';
type FileType = Parameters<GetProp<UploadProps, 'beforeUpload'>>[0];
@ -53,7 +54,7 @@ export const AvatarUpload = () => {
};
const customAction = (file) => {
console.log('file', file);
}
};
return (
<Flex gap='middle' wrap>
<Upload

View File

@ -1,4 +1,4 @@
import { message } from 'antd';
import { message } from '@/modules/message';
import { useImperativeHandle, useRef, forwardRef } from 'react';
import type { GetProp, UploadProps } from 'antd';
type FileTypeOrg = Parameters<GetProp<UploadProps, 'beforeUpload'>>[0];

View File

@ -1,6 +1,6 @@
import { create } from 'zustand';
import { query } from '@/modules';
import { message } from 'antd';
import { message } from '@/modules/message';
type UserStore = {
showEdit: boolean;
setShowEdit: (showEdit: boolean) => void;

View File

@ -1,6 +1,6 @@
import { query } from '@/modules';
import { basename } from '@/modules/basename';
import { message } from 'antd';
import { message } from '@/modules/message';
import { create } from 'zustand';
// 如果自己是在iframe中登录需要调用这个方法
export const postLoginInIframe = (token: string) => {
@ -55,7 +55,7 @@ export const useLoginStore = create<LoginStore>((set, get) => {
set({ loading: true });
const loaded = message.loading('loading...', 0);
const res = await query.post({ path: 'user', key: 'login', username, password });
loaded();
setTimeout(loaded, 200);
if (res.code === 200) {
const { token } = res.data;
message.success('Success');
@ -81,7 +81,7 @@ export const useLoginStore = create<LoginStore>((set, get) => {
set({ loading: true });
const loaded = message.loading('loading...', 0);
const res = await query.post({ path: 'user', key: 'register' });
loaded();
setTimeout(loaded, 200);
if (res.code === 200) {
message.success('Success');
// 跳到某一个页面