feat: add change user-config
This commit is contained in:
parent
1e10c8529e
commit
521255c1af
3
.gitmodules
vendored
3
.gitmodules
vendored
@ -25,3 +25,6 @@
|
||||
[submodule "submodules/query-upload"]
|
||||
path = submodules/query-upload
|
||||
url = git@git.xiongxiao.me:kevisual/kevisual-query-upload.git
|
||||
[submodule "packages/ticket"]
|
||||
path = packages/ticket
|
||||
url = git@git.xiongxiao.me:kevisual/kevisual-ticket.git
|
||||
|
38
package.json
38
package.json
@ -23,16 +23,16 @@
|
||||
"@kevisual/codemirror": "workspace:*",
|
||||
"@kevisual/components": "workspace:*",
|
||||
"@kevisual/container": "1.0.0",
|
||||
"@kevisual/query": "^0.0.14",
|
||||
"@kevisual/query": "^0.0.15",
|
||||
"@kevisual/query-config": "workspace:*",
|
||||
"@kevisual/query-login": "workspace:*",
|
||||
"@kevisual/resources": "workspace:*",
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@mui/material": "^6.4.8",
|
||||
"@mui/material": "^7.0.1",
|
||||
"@stackblitz/sdk": "^1.11.0",
|
||||
"@tailwindcss/vite": "^4.0.15",
|
||||
"@tailwindcss/vite": "^4.1.1",
|
||||
"@uiw/react-textarea-code-editor": "^3.1.0",
|
||||
"antd": "^5.24.4",
|
||||
"antd": "^5.24.6",
|
||||
"clsx": "^2.1.1",
|
||||
"copy-to-clipboard": "^3.3.3",
|
||||
"dayjs": "^1.11.13",
|
||||
@ -45,13 +45,13 @@
|
||||
"marked": "^15.0.7",
|
||||
"nanoid": "^5.1.5",
|
||||
"qrcode": "^1.5.4",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"react-hook-form": "^7.55.0",
|
||||
"react-i18next": "^15.4.1",
|
||||
"react-resizable-panels": "^2.1.7",
|
||||
"react-router": "^7.4.0",
|
||||
"react-router-dom": "^7.4.0",
|
||||
"react-router": "^7.4.1",
|
||||
"react-router-dom": "^7.4.1",
|
||||
"react-toastify": "^11.0.5",
|
||||
"vite-plugin-tsconfig-paths": "^1.4.1",
|
||||
"zustand": "^5.0.3"
|
||||
@ -60,11 +60,11 @@
|
||||
"@eslint/js": "^9.23.0",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^22.13.11",
|
||||
"@types/node": "^22.14.0",
|
||||
"@types/path-browserify": "^1.0.3",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@types/react": "^19.0.12",
|
||||
"@types/react-dom": "^19.0.4",
|
||||
"@types/react": "^19.1.0",
|
||||
"@types/react-dom": "^19.1.1",
|
||||
"@vitejs/plugin-basic-ssl": "^2.0.0",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"autoprefixer": "^10.4.21",
|
||||
@ -73,18 +73,18 @@
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.19",
|
||||
"globals": "^16.0.0",
|
||||
"lucide-react": "^0.483.0",
|
||||
"lucide-react": "^0.487.0",
|
||||
"path-browserify": "^1.0.1",
|
||||
"postcss-import": "^16.1.0",
|
||||
"pretty-bytes": "^6.1.1",
|
||||
"react-is": "19.0.0",
|
||||
"tailwind-merge": "^3.0.2",
|
||||
"tailwindcss": "^4.0.15",
|
||||
"react-is": "19.1.0",
|
||||
"tailwind-merge": "^3.1.0",
|
||||
"tailwindcss": "^4.1.1",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"turbo": "^2.4.4",
|
||||
"typescript": "^5.8.2",
|
||||
"typescript-eslint": "^8.27.0",
|
||||
"vite": "^6.2.2"
|
||||
"typescript-eslint": "^8.29.0",
|
||||
"vite": "^6.2.4"
|
||||
},
|
||||
"packageManager": "pnpm@10.6.5"
|
||||
"packageManager": "pnpm@10.7.1"
|
||||
}
|
@ -1 +1 @@
|
||||
Subproject commit a99d9c2322df537d7ffaac09545e9b2e121511fa
|
||||
Subproject commit 540de3df4dee9a8529c8faa53d4143d80577cc91
|
1
packages/ticket
Submodule
1
packages/ticket
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit e56eaab69acf919e74eb89272f6abf0888a749ef
|
1918
pnpm-lock.yaml
generated
1918
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -75,6 +75,8 @@
|
||||
"Detect Config Tips": "From the json configuration data of the uploaded [config/1.0.0] path.",
|
||||
"Upload Config": "Upload Config",
|
||||
"Upload Config Tips": "Upload the configuration to the default configuration of the [upload/1.0.0] path, and open the configuration item.",
|
||||
"User Config": "User Config",
|
||||
"User Config Tips": "User Config, configure the default homepage and other custom attributes of the entry page.",
|
||||
"Workspace Config": "Workspace Config",
|
||||
"Workspace Config Tips": "Set the default workspace, when there is no specific space to open, the default workspace will be opened. The collection information module, the position of the added resource, will be placed here by default. For example, the WeChat public account's essay.",
|
||||
"Add Config Tips": "Add configuration, when the key exists, the data update of the key will be queried first, if the key does not exist, the new configuration will be added.",
|
||||
@ -84,5 +86,6 @@
|
||||
"delete_directory": "Delete Directory",
|
||||
"delete_directory_success": "Delete directory success",
|
||||
"create_directory": "Create Directory",
|
||||
"create_directory_success": "Create directory success"
|
||||
"create_directory_success": "Create directory success",
|
||||
"Apply Change Name": "Apply Change Name"
|
||||
}
|
@ -75,6 +75,8 @@
|
||||
"Detect Config Tips": "从上传的 [config/1.0.0] 路径的的json的配置的数据同步过来。",
|
||||
"Upload Config": "上传配置",
|
||||
"Upload Config Tips": "上传配置到 [upload/1.0.0] 路径的默认配置,打开配置项。",
|
||||
"User Config": "用户配置",
|
||||
"User Config Tips": "用户配置,配置进入页面的默认首页等自定义属性",
|
||||
"Workspace Config": "工作空间配置",
|
||||
"Workspace Config Tips": "设置默认的工作空间,当没有打开具体的空间的时候,默认打开的工作空间。收集信息的模块,添加的资源的位置,默认放到这里。比如,微信公众号的随笔。",
|
||||
"Add Config Tips": "添加配置,当key存在的时候,会优先查询key数据的更新,如果key不存在,则新增配置。",
|
||||
@ -84,5 +86,6 @@
|
||||
"delete_directory": "删除目录",
|
||||
"delete_directory_success": "删除目录成功",
|
||||
"create_directory": "创建目录",
|
||||
"create_directory_success": "创建目录成功"
|
||||
"create_directory_success": "创建目录成功",
|
||||
"Apply Change Name": "申请修改名称"
|
||||
}
|
@ -75,6 +75,7 @@ export const App = () => {
|
||||
<Route path='/container/*' element={<ContainerApp />} />
|
||||
<Route path='/map/*' element={<MapApp />} />
|
||||
<Route path='/user-center/*' element={<UserApp />} />
|
||||
<Route path='/user/*' element={<UserApp />} />
|
||||
<Route path='/org/*' element={<OrgApp />} />
|
||||
<Route path='/config/*' element={<ConfigApp />} />
|
||||
<Route path='/app/*' element={<UserAppApp />} />
|
||||
|
@ -1,34 +0,0 @@
|
||||
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}</>;
|
||||
};
|
@ -1,14 +1,12 @@
|
||||
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';
|
||||
|
||||
initI18n(basename);
|
||||
import { I18NextProvider } from '@kevisual/components/translate/I18Next.tsx';
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<I18NextProvider>
|
||||
<I18NextProvider basename={!DEV_SERVER ? '/root/locales' : '/'}>
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<App />
|
||||
</Suspense>
|
||||
|
@ -14,21 +14,15 @@ import { useTranslation } from 'react-i18next';
|
||||
import React from 'react';
|
||||
|
||||
export const LayoutUser = () => {
|
||||
const { open, setOpen, ...store } = useLayoutStore(
|
||||
const { open, setOpen, isAdmin, ...store } = useLayoutStore(
|
||||
useShallow((state) => ({
|
||||
open: state.openUser, //
|
||||
setOpen: state.setOpenUser,
|
||||
me: state.me,
|
||||
switchOrg: state.switchOrg,
|
||||
isAdmin: state.isAdmin,
|
||||
})),
|
||||
);
|
||||
const [isAdmin, setIsAdmin] = useState<boolean>(false);
|
||||
useEffect(() => {
|
||||
queryLogin.cacheStore.getCurrentUser().then((res) => {
|
||||
const org = res?.orgs || [];
|
||||
setIsAdmin(org.includes('admin'));
|
||||
});
|
||||
}, []);
|
||||
const navigate = useNewNavigate();
|
||||
const { t } = useTranslation();
|
||||
const items = useMemo(() => {
|
||||
@ -89,7 +83,7 @@ export const LayoutUser = () => {
|
||||
};
|
||||
|
||||
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 text-primary', !open && 'hidden')}>
|
||||
<div
|
||||
className='w-full absolute h-full opacity-60 z-0'
|
||||
onClick={() => {
|
||||
|
@ -62,6 +62,8 @@ export type LayoutStore = {
|
||||
openUser: boolean;
|
||||
setOpenUser: (openUser: boolean) => void;
|
||||
switchOrg: (username?: string, type?: 'user' | 'org') => Promise<void>;
|
||||
isAdmin: boolean;
|
||||
setIsAdmin: (isAdmin: boolean) => void;
|
||||
};
|
||||
export const useLayoutStore = create<LayoutStore>((set) => ({
|
||||
open: false,
|
||||
@ -72,6 +74,7 @@ export const useLayoutStore = create<LayoutStore>((set) => ({
|
||||
const res = await queryLogin.getMe();
|
||||
if (res.code === 200) {
|
||||
set({ me: res.data });
|
||||
set({ isAdmin: res.data.orgs?.includes('admin') });
|
||||
}
|
||||
},
|
||||
openUser: false,
|
||||
@ -87,4 +90,6 @@ export const useLayoutStore = create<LayoutStore>((set) => ({
|
||||
message.error(res.message || 'Request failed');
|
||||
}
|
||||
},
|
||||
isAdmin: false,
|
||||
setIsAdmin: (isAdmin) => set({ isAdmin }),
|
||||
}));
|
||||
|
@ -72,15 +72,15 @@ const FormModal = () => {
|
||||
}}>
|
||||
<DialogTitle>{isEdit ? 'Edit' : 'Add'}</DialogTitle>
|
||||
<DialogContent>
|
||||
<form className='flex flex-col gap-6' onSubmit={handleSubmit(onFinish)}>
|
||||
<form className='flex flex-col gap-6 py-4' onSubmit={handleSubmit(onFinish)}>
|
||||
<Controller name='key' control={control} defaultValue='' render={({ field }) => <TextField label='key' {...field} disabled />} />
|
||||
<Controller name='version' control={control} defaultValue='' render={({ field }) => <TextField label='version' {...field} />} />
|
||||
<div>
|
||||
<Button type='submit' variant='contained' color='primary'>
|
||||
{t('submit')}
|
||||
{t('Submit')}
|
||||
</Button>
|
||||
<Button className='ml-2' onClick={onClose}>
|
||||
{t('cancel')}
|
||||
{t('Cancel')}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
@ -117,10 +117,10 @@ export const AppVersionList = () => {
|
||||
versionStore.setKey(appKey);
|
||||
versionStore.getList();
|
||||
}
|
||||
}, []);
|
||||
}, [appKey]);
|
||||
const appVersion = useMemo(() => {
|
||||
return versionStore.app?.version || '';
|
||||
}, [versionStore.app]);
|
||||
}, [versionStore.app?.version]);
|
||||
return (
|
||||
<div className='w-full h-full flex bg-slate-100'>
|
||||
<div className='p-2 bg-white'>
|
||||
@ -153,6 +153,7 @@ export const AppVersionList = () => {
|
||||
const isPublish = item.version === appVersion;
|
||||
const color = isPublish ? 'bg-green-500' : '';
|
||||
const isRunning = item.status === 'running';
|
||||
console.log('appVersion', item, appVersion, versionStore.app);
|
||||
return (
|
||||
<div className='card w-[300px]' key={index}>
|
||||
<div className={'flex items-center justify-between'}>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { useUserAppStore } from '../store';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useModal } from '@kevisual/components/modal/Confirm.tsx';
|
||||
|
||||
import DeleteOutlined from '@ant-design/icons/DeleteOutlined';
|
||||
@ -12,7 +12,7 @@ import CodeOutlined from '@ant-design/icons/CodeOutlined';
|
||||
import ShareAltOutlined from '@ant-design/icons/ShareAltOutlined';
|
||||
import { FormControlLabel, Switch, useTheme } from '@mui/material';
|
||||
import { isObjectNull } from '@/utils/is-null';
|
||||
import { useNewNavigate } from '@/modules';
|
||||
import { queryLogin, useNewNavigate } from '@/modules';
|
||||
import { DialogActions, Tooltip } from '@mui/material';
|
||||
import { marked } from 'marked';
|
||||
import clsx from 'clsx';
|
||||
@ -28,6 +28,7 @@ import { TextField, InputAdornment } from '@mui/material';
|
||||
import { useForm, Controller } from 'react-hook-form';
|
||||
import { pick } from 'lodash-es';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import { useLayoutStore } from '@/modules/layout/store';
|
||||
|
||||
const FormModal = () => {
|
||||
const defaultValues = {
|
||||
@ -75,6 +76,8 @@ const FormModal = () => {
|
||||
const isEdit = containerStore?.userApp?.id;
|
||||
const theme = useTheme();
|
||||
const defaultProps = theme.components?.MuiTextField?.defaultProps as any;
|
||||
const isAdmin = useLayoutStore(useShallow((state) => state.isAdmin));
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={containerStore.showEdit}
|
||||
@ -99,7 +102,7 @@ const FormModal = () => {
|
||||
control={control}
|
||||
render={({ field }) => <TextField {...defaultProps} {...field} label='description' multiline rows={4} fullWidth />}
|
||||
/>
|
||||
<Controller name='proxy' control={control} render={({ field }) => <Switch {...defaultProps} {...field} checked={field.value} />} />
|
||||
{isAdmin && <Controller name='proxy' control={control} render={({ field }) => <Switch {...defaultProps} {...field} checked={field.value} />} />}
|
||||
<Controller
|
||||
name='status'
|
||||
control={control}
|
||||
|
@ -52,7 +52,7 @@ export const useAppVersionStore = create<AppVersionStore>((set, get) => {
|
||||
key,
|
||||
},
|
||||
});
|
||||
get().getApp(key);
|
||||
get().getApp(key, true);
|
||||
set({ loading: false });
|
||||
if (res.code === 200) {
|
||||
set({ list: res.data });
|
||||
|
@ -19,6 +19,7 @@ import { isEmpty, pick } from 'lodash-es';
|
||||
import { usePermissionModal } from '@kevisual/resources/index.ts';
|
||||
import React from 'react';
|
||||
import { queryLogin } from '@/modules';
|
||||
import { useLayoutStore } from '@/modules/layout/store';
|
||||
type DataYamlEditProps = {
|
||||
onSave: (data: any) => Promise<void>;
|
||||
type?: 'yaml' | 'json';
|
||||
@ -190,14 +191,11 @@ export const DrawerEdit = () => {
|
||||
export const List = () => {
|
||||
const { list, getConfigList, setShowEdit, setFormData, deleteConfig, updateData, formData, detectConfig, onOpenKey } = useConfigStore();
|
||||
const [modal, contextHolder] = useModal();
|
||||
const [isAdmin, setIsAdmin] = useState<boolean>(false);
|
||||
const isAdmin = useLayoutStore(useShallow((state) => state.isAdmin));
|
||||
useEffect(() => {
|
||||
getConfigList();
|
||||
queryLogin.cacheStore.getCurrentUser().then((res) => {
|
||||
const org = res?.orgs || [];
|
||||
setIsAdmin(org.includes('admin'));
|
||||
});
|
||||
}, []);
|
||||
|
||||
const { setOpen, contextHolder: contextHolderPermission } = usePermissionModal({
|
||||
onSave: async (values) => {
|
||||
const permission = values;
|
||||
@ -253,17 +251,30 @@ export const List = () => {
|
||||
{t('Upload Config')}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip title={t('Workspace Config Tips')}>
|
||||
<Tooltip title={t('User Config Tips')}>
|
||||
<Button
|
||||
variant='contained'
|
||||
color='primary'
|
||||
size='small'
|
||||
onClick={() => {
|
||||
onOpenKey('workspace.json');
|
||||
onOpenKey('user.json');
|
||||
}}>
|
||||
{t('Workspace Config')}
|
||||
{t('User Config')}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
{isAdmin && (
|
||||
<Tooltip title={t('Workspace Config Tips')}>
|
||||
<Button
|
||||
variant='contained'
|
||||
color='primary'
|
||||
size='small'
|
||||
onClick={() => {
|
||||
onOpenKey('workspace.json');
|
||||
}}>
|
||||
{t('Workspace Config')}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<Tooltip title={t('VIP Config Tips')}>
|
||||
<Button
|
||||
|
@ -4,7 +4,7 @@ import { useShallow } from 'zustand/react/shallow';
|
||||
// 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 { Dialog, DialogTitle, DialogContent, Tooltip, Button, ButtonGroup, Typography } from '@mui/material';
|
||||
import { IconButton } from '@kevisual/components/button/index.tsx';
|
||||
import { getDirectoryAndName, toFile, uploadFileChunked } from '@kevisual/resources/index.ts';
|
||||
import EditOutlined from '@ant-design/icons/EditOutlined';
|
||||
@ -223,7 +223,6 @@ const PublishFormModal = () => {
|
||||
);
|
||||
};
|
||||
export const ContainerList = () => {
|
||||
const navicate = useNewNavigate();
|
||||
const [modal, contextHolder] = useModal();
|
||||
const containerStore = useContainerStore(
|
||||
useShallow((state) => {
|
||||
@ -245,7 +244,6 @@ export const ContainerList = () => {
|
||||
}),
|
||||
);
|
||||
|
||||
// const [codeEdit, setCodeEdit] = useState(false);
|
||||
useEffect(() => {
|
||||
containerStore.getList();
|
||||
}, []);
|
||||
@ -264,8 +262,15 @@ export const ContainerList = () => {
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className='flex grow overflow-hidden h-full'>
|
||||
<div className='grow overflow-auto scrollbar bg-gray-100'>
|
||||
<div className='flex grow flex-col overflow-hidden h-full'>
|
||||
<div className='p-2 flex flex-col gap-2 text-secondary px-4 mx-4'>
|
||||
<div className='text-sm'>轻代码或者配置文件替换。</div>
|
||||
</div>
|
||||
<div
|
||||
className='grow overflow-auto scrollbar bg-gray-100'
|
||||
style={{
|
||||
height: 'calc(100% - 40px)',
|
||||
}}>
|
||||
<div className='flex flex-wrap gap-x-10 gap-y-4 rounded-sm pt-10 justify-center'>
|
||||
{containerStore.list.length > 0 &&
|
||||
containerStore.list.map((item) => {
|
||||
|
125
src/pages/user/admin/store/admin-store.ts
Normal file
125
src/pages/user/admin/store/admin-store.ts
Normal file
@ -0,0 +1,125 @@
|
||||
import { create } from 'zustand';
|
||||
import { query } from '@/modules';
|
||||
import { toast } from 'react-toastify';
|
||||
import { Result } from '@kevisual/query/query';
|
||||
type AdminStore = {
|
||||
/**
|
||||
* 创建新用户
|
||||
* @returns
|
||||
*/
|
||||
createNewUser: (data: any) => Promise<void>;
|
||||
/**
|
||||
* 删除用户
|
||||
* @param id
|
||||
*/
|
||||
deleteUser: (id: string) => Promise<void>;
|
||||
/**
|
||||
* 更新用户
|
||||
* @param id
|
||||
* @param data
|
||||
*/
|
||||
updateUser: (id: string, data: any) => Promise<void>;
|
||||
|
||||
/**
|
||||
* 重置密码
|
||||
* @param id
|
||||
*/
|
||||
resetPassword: (id: string, password?: string) => Promise<void>;
|
||||
|
||||
/**
|
||||
* 修改用户名
|
||||
* @param id
|
||||
* @param name
|
||||
*/
|
||||
changeName: (id: string, name: string) => Promise<Result<any>>;
|
||||
|
||||
/**
|
||||
* 检查用户是否存在
|
||||
* @param name
|
||||
* @returns
|
||||
*/
|
||||
checkUserExist: (name: string) => Promise<boolean | null>;
|
||||
};
|
||||
|
||||
export const useAdminStore = create<AdminStore>((set) => ({
|
||||
createNewUser: async (data: any) => {
|
||||
const res = await query.post({
|
||||
path: 'user',
|
||||
key: 'createNewUser',
|
||||
data,
|
||||
});
|
||||
if (res.code === 200) {
|
||||
toast.success('创建用户成功');
|
||||
} else {
|
||||
toast.error(res.message || '创建用户失败');
|
||||
}
|
||||
},
|
||||
deleteUser: async (id: string) => {
|
||||
const res = await query.post({
|
||||
path: 'user',
|
||||
key: 'deleteUser',
|
||||
data: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
if (res.code === 200) {
|
||||
toast.success('删除用户成功');
|
||||
} else {
|
||||
toast.error(res.message || '删除用户失败');
|
||||
}
|
||||
},
|
||||
updateUser: async (id: string, data: any) => {
|
||||
console.log('updateUser', id, data);
|
||||
},
|
||||
resetPassword: async (id: string, password?: string) => {
|
||||
const res = await query.post({
|
||||
path: 'user',
|
||||
key: 'resetPassword',
|
||||
data: {
|
||||
id,
|
||||
password,
|
||||
},
|
||||
});
|
||||
if (res.code === 200) {
|
||||
if (res.data.password) {
|
||||
toast.success('new password is ' + res.data.password);
|
||||
} else {
|
||||
toast.success('重置密码成功');
|
||||
}
|
||||
} else {
|
||||
toast.error(res.message || '重置密码失败');
|
||||
}
|
||||
},
|
||||
changeName: async (id: string, name: string) => {
|
||||
const res = await query.post({
|
||||
path: 'user',
|
||||
key: 'changeName',
|
||||
data: {
|
||||
id,
|
||||
newName: name,
|
||||
},
|
||||
});
|
||||
if (res.code === 200) {
|
||||
toast.success('修改用户名成功');
|
||||
} else {
|
||||
toast.error(res.message || '修改用户名失败');
|
||||
}
|
||||
return res;
|
||||
},
|
||||
checkUserExist: async (name: string) => {
|
||||
const res = await query.post({
|
||||
path: 'user',
|
||||
key: 'checkUserExist',
|
||||
data: {
|
||||
username: name,
|
||||
},
|
||||
});
|
||||
if (res.code === 200) {
|
||||
const user = res.data || {};
|
||||
return !!user.id;
|
||||
} else {
|
||||
toast.error(res.message || '检查用户是否存在,请求失败');
|
||||
}
|
||||
return null;
|
||||
},
|
||||
}));
|
@ -1,22 +1,18 @@
|
||||
import { Fragment, useEffect, useState } from 'react';
|
||||
import { useUserStore } from '../store';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
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 clsx from 'clsx';
|
||||
import { isObjectNull } from '@/utils/is-null';
|
||||
import { CardBlank } from '@kevisual/components/card/CardBlank.tsx';
|
||||
import { Dialog, ButtonGroup, Button, DialogContent, DialogTitle } from '@mui/material';
|
||||
import { Dialog, ButtonGroup, Button, DialogContent, DialogTitle, Tooltip } from '@mui/material';
|
||||
import { IconButton } from '@kevisual/components/button/index.tsx';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useModal } from '@kevisual/components/modal/Confirm.tsx';
|
||||
import { TextField } from '@mui/material';
|
||||
import { useForm, Controller } from 'react-hook-form';
|
||||
import { pick } from 'lodash-es';
|
||||
|
||||
import { useAdminStore } from '../admin/store/admin-store';
|
||||
import { SquareAsterisk, Edit as EditOutlined, Trash as DeleteOutlined, Plus as PlusOutlined, UserPen } from 'lucide-react';
|
||||
import { toast } from 'react-toastify';
|
||||
const FormModal = () => {
|
||||
const { control, handleSubmit, reset } = useForm();
|
||||
const userStore = useUserStore(
|
||||
@ -30,7 +26,7 @@ const FormModal = () => {
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
const adminStore = useAdminStore(useShallow((state) => state));
|
||||
useEffect(() => {
|
||||
const open = userStore.showEdit;
|
||||
if (open) {
|
||||
@ -46,7 +42,12 @@ const FormModal = () => {
|
||||
|
||||
const onFinish = (values: any) => {
|
||||
const pickValues = pick(values, ['id', 'username', 'description']);
|
||||
userStore.updateData(pickValues);
|
||||
if (pickValues.id) {
|
||||
userStore.updateData(pickValues);
|
||||
} else {
|
||||
const newPickValues = pick(values, ['username', 'description', 'password']);
|
||||
adminStore.createNewUser(newPickValues);
|
||||
}
|
||||
};
|
||||
|
||||
const onClose = () => {
|
||||
@ -69,7 +70,7 @@ const FormModal = () => {
|
||||
}}>
|
||||
<DialogTitle>{isEdit ? t('Edit') : t('Add')}</DialogTitle>
|
||||
<DialogContent>
|
||||
<form onSubmit={handleSubmit(onFinish)}>
|
||||
<form className='flex flex-col gap-6 pt-4' onSubmit={handleSubmit(onFinish)}>
|
||||
<Controller name='username' control={control} defaultValue='' render={({ field }) => <TextField {...field} label='username' fullWidth />} />
|
||||
<Controller
|
||||
name='description'
|
||||
@ -77,6 +78,60 @@ const FormModal = () => {
|
||||
defaultValue=''
|
||||
render={({ field }) => <TextField {...field} label='description' multiline rows={4} fullWidth />}
|
||||
/>
|
||||
{!isEdit && (
|
||||
<Controller
|
||||
name='password'
|
||||
control={control}
|
||||
defaultValue=''
|
||||
render={({ field }) => <TextField {...field} label='password' type='password' fullWidth />}
|
||||
/>
|
||||
)}
|
||||
<div>
|
||||
<Button type='submit' variant='contained'>
|
||||
{t('Submit')}
|
||||
</Button>
|
||||
<Button className='ml-2' type='button' onClick={onClose}>
|
||||
{t('Cancel')}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
export const NameModal = () => {
|
||||
const { control, handleSubmit, reset } = useForm();
|
||||
const userStore = useUserStore(useShallow((state) => state));
|
||||
const adminStore = useAdminStore(useShallow((state) => state));
|
||||
const onFinish = async (values: any) => {
|
||||
const check = await adminStore.checkUserExist(values.username);
|
||||
if (check === false) {
|
||||
const uid = userStore.formData.id;
|
||||
if (!uid) {
|
||||
toast.error('获取用户id失败');
|
||||
return;
|
||||
}
|
||||
const res = await adminStore.changeName(uid, values.username);
|
||||
if (res.code === 200) {
|
||||
userStore.setShowNameEdit(false);
|
||||
userStore.getList();
|
||||
}
|
||||
} else {
|
||||
toast.error('用户名已存在,请重新输入');
|
||||
}
|
||||
};
|
||||
const onClose = () => {
|
||||
userStore.setShowNameEdit(false);
|
||||
reset({});
|
||||
userStore.setFormData({});
|
||||
};
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Dialog open={userStore.showNameEdit} onClose={() => userStore.setShowNameEdit(false)}>
|
||||
<DialogTitle>修改用户名</DialogTitle>
|
||||
<DialogContent>
|
||||
<form className='flex flex-col gap-6 pt-4' onSubmit={handleSubmit(onFinish)}>
|
||||
<Controller name='username' control={control} defaultValue='' render={({ field }) => <TextField {...field} label='username' fullWidth />} />
|
||||
<div>
|
||||
<Button type='submit' variant='contained'>
|
||||
{t('Submit')}
|
||||
@ -90,7 +145,6 @@ const FormModal = () => {
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export const List = () => {
|
||||
const [modal, contextHolder] = useModal();
|
||||
const userStore = useUserStore(
|
||||
@ -98,15 +152,16 @@ export const List = () => {
|
||||
return {
|
||||
setFormData: state.setFormData,
|
||||
setShowEdit: state.setShowEdit,
|
||||
setShowNameEdit: state.setShowNameEdit,
|
||||
list: state.list,
|
||||
deleteData: state.deleteData,
|
||||
getList: state.getList,
|
||||
loading: state.loading,
|
||||
updateData: state.updateData,
|
||||
formData: state.formData,
|
||||
};
|
||||
}),
|
||||
);
|
||||
const adminStore = useAdminStore(useShallow((state) => state));
|
||||
useEffect(() => {
|
||||
userStore.getList();
|
||||
}, []);
|
||||
@ -119,7 +174,7 @@ export const List = () => {
|
||||
<div className='w-full h-full flex'>
|
||||
<div className='p-2'>
|
||||
<IconButton onClick={onAdd} variant='contained'>
|
||||
<PlusOutlined />
|
||||
<PlusOutlined className='w-4 h-4' />
|
||||
</IconButton>
|
||||
</div>
|
||||
<div className='flex grow overflow-hidden h-full'>
|
||||
@ -151,27 +206,57 @@ export const List = () => {
|
||||
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);
|
||||
e.stopPropagation();
|
||||
}}>
|
||||
<EditOutlined />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
modal.confirm({
|
||||
title: 'Delete',
|
||||
content: 'Are you sure delete this data?',
|
||||
onOk: () => {
|
||||
userStore.deleteData(item.id);
|
||||
},
|
||||
});
|
||||
e.stopPropagation();
|
||||
}}>
|
||||
<DeleteOutlined />
|
||||
</Button>
|
||||
<Tooltip title='编辑'>
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
userStore.setFormData(item);
|
||||
userStore.setShowEdit(true);
|
||||
e.stopPropagation();
|
||||
}}>
|
||||
<EditOutlined />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip title='修改用户名'>
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
userStore.setFormData(item);
|
||||
userStore.setShowNameEdit(true);
|
||||
e.stopPropagation();
|
||||
}}>
|
||||
<UserPen />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip title='重置密码'>
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
modal.confirm({
|
||||
title: '重置密码',
|
||||
content: 'Are you sure reset password?',
|
||||
onOk: () => {
|
||||
adminStore.resetPassword(item.id);
|
||||
},
|
||||
});
|
||||
e.stopPropagation();
|
||||
}}>
|
||||
<SquareAsterisk />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip title='禁用'>
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
modal.confirm({
|
||||
title: 'Delete',
|
||||
content: 'Are you sure delete this data?',
|
||||
onOk: async () => {
|
||||
await adminStore.deleteUser(item.id);
|
||||
userStore.getList();
|
||||
},
|
||||
});
|
||||
e.stopPropagation();
|
||||
}}>
|
||||
<DeleteOutlined />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</div>
|
||||
@ -188,6 +273,7 @@ export const List = () => {
|
||||
</div>
|
||||
</div>
|
||||
<FormModal />
|
||||
<NameModal />
|
||||
{contextHolder}
|
||||
</div>
|
||||
);
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { TextField } from '@mui/material';
|
||||
import { InputAdornment, TextField, Tooltip } from '@mui/material';
|
||||
import { useForm, Controller } from 'react-hook-form';
|
||||
import { Button } from '@mui/material';
|
||||
import { useUserStore } from '../store';
|
||||
@ -10,6 +10,8 @@ import UploadOutlined from '@ant-design/icons/UploadOutlined';
|
||||
import PandaPNG from '@/assets/panda.png';
|
||||
import { FileUpload } from '../module/FileUpload';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Edit } from 'lucide-react';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
export const Profile = () => {
|
||||
const { t } = useTranslation();
|
||||
@ -73,7 +75,7 @@ export const Profile = () => {
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className='w-full h-full bg-amber-50 p-4 '>
|
||||
<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>
|
||||
@ -87,7 +89,38 @@ export const Profile = () => {
|
||||
<Controller
|
||||
name='username'
|
||||
control={control}
|
||||
render={({ field }) => <TextField {...field} label={t('Name')} className='w-full border rounded-lg p-2' disabled />}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
label={t('Name')}
|
||||
className='w-full border rounded-lg p-2'
|
||||
disabled
|
||||
slotProps={{
|
||||
input: {
|
||||
endAdornment: (
|
||||
<Tooltip title={t('Apply Change Name')}>
|
||||
<InputAdornment position='end'>
|
||||
<Edit
|
||||
className='w-4 h-4 cursor-pointer text-primary'
|
||||
onClick={() => {
|
||||
console.log('onClick edit');
|
||||
toast.info('联系客服修改,因为名称和上传文件绑定了。', {
|
||||
autoClose: 20000,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</InputAdornment>
|
||||
</Tooltip>
|
||||
),
|
||||
onKeyDown: (event) => {
|
||||
if (event.key === 'Enter') {
|
||||
// setSearch(field.value);
|
||||
}
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name='avatar'
|
||||
|
@ -4,6 +4,8 @@ import { message } from '@/modules/message';
|
||||
type UserStore = {
|
||||
showEdit: boolean;
|
||||
setShowEdit: (showEdit: boolean) => void;
|
||||
showNameEdit: boolean;
|
||||
setShowNameEdit: (showNameEdit: boolean) => void;
|
||||
formData: any;
|
||||
setFormData: (formData: any) => void;
|
||||
loading: boolean;
|
||||
@ -18,6 +20,8 @@ export const useUserStore = create<UserStore>((set, get) => {
|
||||
return {
|
||||
showEdit: false,
|
||||
setShowEdit: (showEdit) => set({ showEdit }),
|
||||
showNameEdit: false,
|
||||
setShowNameEdit: (showNameEdit) => set({ showNameEdit }),
|
||||
formData: {},
|
||||
setFormData: (formData) => set({ formData }),
|
||||
loading: false,
|
||||
|
@ -1 +1 @@
|
||||
Subproject commit 20688920b8be1e25de032fc9e20499ca03a470f7
|
||||
Subproject commit 862b29cfa4e5faa4a581f0bc68d885c20da889dd
|
@ -1 +1 @@
|
||||
Subproject commit 98c8a2ad86331566058ca7cc12df5ee2b406fa2f
|
||||
Subproject commit f8af24506bdfdbd895be293c8fd8ce061c94e7af
|
@ -1 +1 @@
|
||||
Subproject commit 69784e8ed4f4546c91b2653adc75034e5946123c
|
||||
Subproject commit d70118ad3db513a4ae95a925f0dd651929a8f632
|
@ -74,6 +74,10 @@ export default defineConfig({
|
||||
port: 6020,
|
||||
host: '0.0.0.0',
|
||||
proxy: {
|
||||
'/root/locales': {
|
||||
target: 'https://kevisual.cn',
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/system/lib': {
|
||||
target: 'https://kevisual.xiongxiao.me',
|
||||
changeOrigin: true,
|
||||
@ -81,6 +85,7 @@ export default defineConfig({
|
||||
'/api': {
|
||||
target: 'http://localhost:4005',
|
||||
changeOrigin: true,
|
||||
ws: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, '/api'),
|
||||
},
|
||||
...proxy,
|
||||
|
Loading…
x
Reference in New Issue
Block a user