Browse Source

feat: 用户管理-菜单权限

tuonian 1 month ago
parent
commit
c743eabdc6

+ 1 - 1
frontend/src/api/request.ts

@@ -13,7 +13,7 @@ if (!CONFIG.baseApi){
   CONFIG.baseApi = '/api'
 }
 
-let blockPromise:  Promise<any> | null = null
+export let blockPromise:  Promise<any> | null = null
 
 
 export const getLockRequest = () => {

+ 103 - 0
frontend/src/api/request2.ts

@@ -0,0 +1,103 @@
+import axios, {AxiosResponse, AxiosInstance, AxiosRequestConfig, AxiosPromise} from 'axios';
+import {BaseResp} from "../models/api.ts";
+import {Message, Notify} from "auto-antd";
+import {store} from "../store";
+import {UserActions} from "../store/slice/user.ts";
+import {blockPromise} from "./request.ts";
+// import {checkDesktopApi} from "./desktop.api.ts";
+console.log('env', import.meta.env)
+
+
+type Request2Instance = {
+  (config: AxiosRequestConfig): AxiosPromise;
+  (url: string, config?: AxiosRequestConfig): AxiosPromise;
+  request<R = any, D = any>(config: AxiosRequestConfig<D>): Promise<R>;
+  get<R = any,D = any>(url: string, config?: AxiosRequestConfig<D>): Promise<R>;
+  delete<R = any,D = any>(url: string, config?: AxiosRequestConfig<D>): Promise<R>;
+  head<R , D = any>(url: string, config?: AxiosRequestConfig<D>): Promise<R>;
+  options<R, D = any>(url: string, config?: AxiosRequestConfig<D>): Promise<R>;
+  post<R = any, D = any>(url: string, data?: D, config?: AxiosRequestConfig<D>): Promise<R>;
+  put<R = any, D = any>(url: string, data?: D, config?: AxiosRequestConfig<D>): Promise<R>;
+  patch<R = any, D = any>(url: string, data?: D, config?: AxiosRequestConfig<D>): Promise<R>;
+  postForm<R = any, D = any>(url: string, data?: D, config?: AxiosRequestConfig<D>): Promise<R>;
+  putForm<R = any, D = any>(url: string, data?: D, config?: AxiosRequestConfig<D>): Promise<R>;
+  patchForm<R = any, D = any>(url: string, data?: D, config?: AxiosRequestConfig<D>): Promise<R>;
+}
+
+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+// @ts-ignore
+const CONFIG = window.CONFIG;
+if (!CONFIG.baseApi){
+  CONFIG.baseApi = '/api'
+}
+
+/**
+ * 支持网络请求
+ * @type {AxiosInstance}
+ */
+// create an axios instance
+const request: AxiosInstance = axios.create({
+  baseURL: CONFIG.baseApi,
+  withCredentials: true, // send cookies when cross-domain requests
+  timeout: 10000, // request timeout
+});
+
+request.interceptors.request.use(
+  (config) => {
+    if (!config.headers){
+      config.headers = {}
+    }
+    config.headers["Authorization"] = "token"
+    if (config.url && blockPromise && !['/user/info'].includes(config.url)){
+      console.log('blockPromise', blockPromise, config.url);
+      return blockPromise.then(() => config)
+    }
+    return config;
+  },
+  (error) => {
+    // do something with request error
+    console.log(error); // for debug
+    return Promise.reject(error);
+  },
+);
+
+request.interceptors.response.use((resp: AxiosResponse<BaseResp>)=>{
+  const disableErrorMsg = (resp.config as any)['disableErrorMsg']
+  if (resp.data && resp.data.code == 0){
+    return resp.data.data
+  }else if (resp.data) {
+    (!disableErrorMsg) && Notify.warn(resp.data.msg)
+    return Promise.reject(new Error(resp.data.msg))
+  }else if (resp.status == 200){
+    return resp
+  }else {
+    (!disableErrorMsg) && Notify.warn("服务开小差了,请稍后重试")
+    return Promise.reject(new Error(resp.statusText))
+  }
+},error => {
+  let errData: any = {
+    code: 10
+  }
+  // const disableErrorMsg = (error.request?.config as any)?.['disableErrorMsg']
+  const disableErrorMsg = true;
+  if (error.response && error.response.data){
+    errData = error.response.data
+  }else if (error.message){
+    errData.msg = error.message;
+  }
+  if (!errData.code){
+    errData.msg = 'request fail'
+  }
+  (!disableErrorMsg)&& Message.error(errData.msg)
+  console.log('status', error.response?.status)
+  if (error.response.status == 401){
+    store.dispatch(UserActions.clearUser())
+  }else {
+    return Promise.reject(new Error(errData.msg))
+  }
+})
+
+// checkDesktopApi(request)
+
+
+export const request2: Request2Instance = request

+ 14 - 3
frontend/src/api/user.ts

@@ -1,5 +1,7 @@
 import request, {getLockRequest, lockRequest, unlockRequest} from "./request.ts";
-import {BaseResp} from "../models/api.ts";
+import {BaseResp, PageData, PageReq} from "../models/api.ts";
+import {User} from "../models/user.ts";
+import {request2} from "./request2.ts";
 
 export type LoginReq = {
     account: string
@@ -29,11 +31,12 @@ export const LoginApis = {
      * 加锁,避免多次访问,多次弹窗
      */
     userinfo: () => {
-        let promise = getLockRequest()
+        let promise: Promise<BaseResp<User>> = getLockRequest() as any
         if (promise){
             return promise
         }
-        promise = request.get('/user/info', { disableErrorMsg: true } as never)
+        promise = request.get<BaseResp<User>>('/user/info', { disableErrorMsg: true } as never)
+            .then(res => res.data)
         const block = lockRequest(promise);
         if (block){
             promise.finally(()=>unlockRequest())
@@ -41,7 +44,15 @@ export const LoginApis = {
         return promise;
     },
     modifyPassword: (data: {  oldPassword: string,newPassword: string }) => request.post<BaseResp>('/user/modifyPassword', data),
+    logout: () => request.post('/user/logout'),
     oauth2Url: ()=> request.get('/oauth2'),
     oauth2Callback: (data: SSOReq) => request.post('/oauth2/callback', data, { disableErrorMsg: true } as never)
 
 }
+
+export const userApis = {
+    getDetails: (id : number) => request2.get<User>(`/user/detail`, { params: { id}}),
+    save: (data: Partial<User>) => request2.post<User>(`/user/save`, data),
+    getList: (req:PageReq) => request2.post<PageData<User>>('/user/list', req),
+    updateMyInfo: (user: Partial<User>) => request2.post<User>(`/user/info`, user),
+}

+ 41 - 62
frontend/src/components/curd/Form.tsx

@@ -1,72 +1,51 @@
-import {Button, Drawer} from "antd";
-import {ForwardedRef, forwardRef, useImperativeHandle, useMemo, useRef, useState} from "react";
+import {Button} from "antd";
+import {ForwardedRef, forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState} from "react";
 import {AutoForm, AutoFormInstance, isNullOrTrue, Message} from "auto-antd";
 import {CurdColumn} from "./index.tsx";
 import {FormColumnType} from "auto-antd/dist/esm/Model";
-import {LoadingOutlined} from "@ant-design/icons";
 import {ICurdConfig, ICurdData} from "./types.ts";
 import './form.less'
 
 
 export type IProps<D extends ICurdData> = {
-    getDetail?: (data: Partial<D>) => Promise<D>
-    columns: CurdColumn[]
     config?: ICurdConfig<D>
+    columns: CurdColumn[]
     onSuccess?: (data: Partial<D>) => void
     onSave?: (data: Partial<D>) => Promise<Partial<D>>
+    onClose?: () => void
+    initialData?: Partial<D>
+    editable?: boolean
 }
 
 export type ICurdForm<D extends ICurdData> = {
-    show: (data: Partial<D>) => void
+    onSubmit: () => void
+    setData: (data: Partial<D>) => void
 }
 
-const Form = <D extends ICurdData,>({config,...props}: IProps<D>,ref: ForwardedRef<ICurdForm<D>>) => {
+const Form = <D extends ICurdData,>({config, initialData, onClose, editable,...props}: IProps<D>,ref: ForwardedRef<ICurdForm<D>>) => {
+
 
-    const [visible, setVisible] = useState<boolean>(false);
     const [loading,setLoading] = useState<boolean>(false);
     const [editModel,setEditModel] = useState<Partial<D>>()
 
     const formRef = useRef<AutoFormInstance>()
 
-    const onClose = () => {
-        setVisible(false)
-    }
 
     const formColumns = useMemo(()=>{
-        return props.columns.filter(item=>isNullOrTrue(item.editable)).map(item=>{
+        return props.columns.filter(item=>isNullOrTrue(item.addable))
+            .map(item=>{
             return {
                 ...item,
                 width: undefined,
+                editable: isNullOrTrue(editable) ? initialData?.id ? item.editable : item.addable : false,
             }
         }) as FormColumnType[]
-    },[props.columns])
+    },[props.columns, initialData,editable])
 
-    const show = (data: Partial<D>) => {
-        setVisible(true)
-        if (props.getDetail){
-            setLoading(true)
-            props.getDetail?.(data)
-                .then(res=>{
-                    setEditModel(res)
-                    formRef.current?.setData(res)
-                })
-                .catch(err=>{
-                    Message.warning(err.message)
-                    setVisible(false)
-                })
-                .finally(()=>{
-                    setLoading(false)
-                })
-        }else {
-            formRef.current?.setData(data)
-        }
-    }
-
-    useImperativeHandle(ref, () => {
-        return {
-            show: show
-        }
-    })
+    useEffect(() => {
+        setEditModel(initialData)
+        formRef.current?.setData(initialData)
+    }, [initialData]);
 
     const onSubmit = () => {
         formRef.current?.onSyncSubmit(true)
@@ -78,8 +57,8 @@ const Form = <D extends ICurdData,>({config,...props}: IProps<D>,ref: ForwardedR
             })
             .then((res)=>{
                 setLoading(false)
+                Message.success('保存成功!')
                 props.onSuccess?.(res)
-                setVisible(false)
             })
             .catch(e=>{
                 console.log('submit fail',e)
@@ -89,31 +68,31 @@ const Form = <D extends ICurdData,>({config,...props}: IProps<D>,ref: ForwardedR
             })
     }
 
+    useImperativeHandle(ref, () => {
+        return {
+            onSubmit: onSubmit,
+            setData: (data: Partial<D>) =>{
+                console.log('setData', data)
+            }
+        }
+    })
+
 
     return (<>
-        <Drawer open={visible} destroyOnClose width={config?.editDialogWidth ?? 650}
-                title={editModel?.id ? '编辑' : '新增'}
-                height={400} onClose={onClose}>
-            <div className="form-container">
-                <div className="form-container-loading">
-                    {
-                        loading ? (<LoadingOutlined/>) : null
-                    }
-                </div>
-                <AutoForm columns={formColumns} data={editModel}
-                          formProps={{
-                              labelCol: {
-                                  span: config?.labelSpan ?? 6
-                              }
-                          }}
-                          ref={formRef as any} />
-                <div className="form-container-footer">
-                    <Button onClick={onSubmit} loading={loading} type="primary">保存</Button>
-                    <Button onClick={onClose} danger>取消</Button>
-                </div>
+        <div className="form-container">
+            <AutoForm columns={formColumns} data={editModel}
+                      formProps={{
+                          labelCol: {
+                              span: config?.labelSpan ?? 6
+                          }
+                      }}
+                      ref={formRef as any}/>
+            <div className="form-container-footer">
+                <Button hidden={!isNullOrTrue(editable)} onClick={onSubmit} loading={loading} type="primary">保存</Button>
+                <Button onClick={onClose} danger>取消</Button>
             </div>
-        </Drawer>
-        </>)
+        </div>
+    </>)
 }
 
 export const CurdForm = forwardRef(Form)

+ 3 - 0
frontend/src/components/curd/index.less

@@ -24,4 +24,7 @@
     box-sizing: border-box;
     padding: 8px 0;
   }
+  .curd-edit-dialog{
+    min-height: 200px;
+  }
 }

+ 72 - 21
frontend/src/components/curd/index.tsx

@@ -1,10 +1,10 @@
 import {AutoColumn, isNullOrTrue, Message} from "auto-antd";
-import {Button, Table} from "antd";
+import {Button, Drawer, Table} from "antd";
 import React, {useEffect, useMemo, useRef, useState} from "react";
 import {ColumnsType} from "antd/lib/table";
 import {CurdForm, ICurdForm, IProps as IFormProps} from "./Form.tsx";
 import {LDAP} from "../../api/ldap.ts";
-import {EditOutlined, PlusOutlined, SyncOutlined} from "@ant-design/icons";
+import {EditOutlined, LoadingOutlined, PlusOutlined, SyncOutlined} from "@ant-design/icons";
 import './index.less'
 import {ICurdConfig, ICurdData} from "./types.ts";
 
@@ -13,6 +13,7 @@ export type CurdColumn = AutoColumn & {
     search?: boolean
     editable?: boolean
     hidden?: boolean
+    addable?: boolean // 添加时可编辑
 }
 
 
@@ -31,15 +32,46 @@ type IProps<T extends ICurdData> = IFormProps<T> & {
  * 一个完成的增删查改界面
  * @constructor
  */
-export function CurdPage<T extends ICurdData>({columns, getList, config, operationRender, onSuccess,...props}: IProps<T>) {
+export function CurdPage<T extends ICurdData>({
+                                                  columns,
+                                                  getList,
+                                                  config,
+                                                  operationRender,
+                                                  onSuccess,
+                                                  ...props
+                                              }: IProps<T>) {
 
     const [list, setList] = useState<T[]>([]);
     const [total, setTotal] = useState<number>(0)
     const [query, setQuery] = useState<any>({current: 1, pageSize: 10});
-    const [loading,setLoading] = useState<boolean>(false);
+    const [loading, setLoading] = useState<boolean>(false);
+    // edit or add
+    const [visible, setVisible] = useState<boolean>(false);
+    const [editData, setEditData] = useState<Partial<T>>()
 
     const formRef = useRef<ICurdForm<LDAP.Server>>()
 
+    const onAddOrEdit = (edit: Partial<T>) => {
+        setVisible(true)
+        if (props.getDetail) {
+            setLoading(true)
+            props.getDetail?.(edit)
+                .then(res => {
+                    setEditData(res)
+                })
+                .catch(err => {
+                    Message.warning(err.message)
+                    setVisible(false)
+                })
+                .finally(() => {
+                    setLoading(false)
+                })
+        } else {
+            setEditData({...edit})
+            setVisible(true)
+        }
+    }
+
 
     const tableColumns = useMemo(() => {
         const cols = columns.filter(item => !item.hidden)
@@ -59,11 +91,11 @@ export function CurdPage<T extends ICurdData>({columns, getList, config, operati
                 return (<div className="operation-list">
                     {
                         isNullOrTrue(config?.editable) ?
-                            (<Button size="small" type="primary" icon={<EditOutlined />}
-                                     onClick={() => formRef.current?.show({...record})} />) : null
+                            (<Button size="small" type="primary" icon={<EditOutlined/>}
+                                     onClick={() => onAddOrEdit(record)}/>) : null
                     }
-                    {config?.operationRender?.(record,index)}
-                    </div>)
+                    {config?.operationRender?.(record, index)}
+                </div>)
             }
         })
 
@@ -71,17 +103,22 @@ export function CurdPage<T extends ICurdData>({columns, getList, config, operati
 
     }, [columns]);
 
+    const onClose = () => {
+        setVisible(false)
+        setEditData(undefined)
+    }
+
 
     useEffect(() => {
         setLoading(true)
         getList(query).then(res => {
-            if (res){
+            if (res) {
                 setList(res.list)
                 setTotal(res.total)
-            }else {
+            } else {
                 Message.warning('无数据!')
             }
-        }).finally(()=>{
+        }).finally(() => {
             setLoading(false)
         })
     }, [getList, query]);
@@ -97,10 +134,10 @@ export function CurdPage<T extends ICurdData>({columns, getList, config, operati
 
     return <div className="curd">
         <div className="curd-header">
-            <Button onClick={() => formRef.current?.show({})} icon={<PlusOutlined />} />
+            <Button onClick={() => onAddOrEdit({})} icon={<PlusOutlined/>}/>
             {operationRender}
-            <div style={{flex: 1}} />
-            <Button loading={loading} onClick={() =>setQuery({...query})} icon={<SyncOutlined />} />
+            <div style={{flex: 1}}/>
+            <Button loading={loading} onClick={() => setQuery({...query})} icon={<SyncOutlined/>}/>
         </div>
         <div className="curd-body">
             <Table columns={tableColumns as any} dataSource={list as any} pagination={{
@@ -108,17 +145,31 @@ export function CurdPage<T extends ICurdData>({columns, getList, config, operati
                 pageSize: query.pageSize,
                 current: query.current,
                 onChange: onPageChange,
-            }}/>
+            }}
+                   rowKey={config?.rowKey ?? 'id'}/>
         </div>
         <div className="curd-footer">
 
         </div>
-        <CurdForm ref={formRef as any}
-                  columns={columns}
-                  getDetail={props.getDetail as any}
-                  onSave={props.onSave as any}
-                  onSuccess={onSaveSuccess as any}
-                  config={config as any}/>
+
+        <Drawer open={visible} destroyOnClose width={config?.editDialogWidth ?? 650}
+                title={editData?.id ? '编辑' : '新增'}
+                height={400} onClose={onClose}>
+            <div className="curd-edit-dialog">
+                {
+                    loading ? (<LoadingOutlined/>) : (
+                        <CurdForm ref={formRef as any}
+                                  initialData={editData}
+                                  columns={columns}
+                                  onSave={props.onSave as any}
+                                  onSuccess={onSaveSuccess as any}
+                                  onClose={onClose}
+                                  config={config as any}/>
+                    )
+                }
+            </div>
+        </Drawer>
+
     </div>
 
 }

+ 2 - 1
frontend/src/components/curd/types.ts

@@ -2,7 +2,7 @@ import type * as React from "react";
 
 
 export type ICurdData = {
-    id?: any
+    id?: number
     [key: string]: any
 }
 
@@ -12,6 +12,7 @@ export type ICurdConfig<T extends ICurdData> = {
     editable?: boolean // 展示编辑按钮
     details?: boolean // 展示详情按钮
     operationRender?: (record: T, index: number) => React.ReactNode;
+    rowKey?: string
 }
 
 

+ 26 - 0
frontend/src/components/loading/index.less

@@ -0,0 +1,26 @@
+.loading-container{
+  height: 64px;
+  transition: height 0.5s linear;
+  overflow: hidden;
+  &.hidden{
+    height: 0;
+    transition: height 0.5s linear;
+    overflow: hidden;
+  }
+  .loading{
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    box-sizing: border-box;
+    padding: 32px 16px;
+    color: #1e88c7;
+    overflow: hidden;
+    justify-content: center;
+    &-text{
+      padding-left: 16px;
+      font-size: 13px;
+    }
+  }
+
+}
+

+ 16 - 0
frontend/src/components/loading/index.tsx

@@ -0,0 +1,16 @@
+import {LoadingOutlined} from "@ant-design/icons";
+import './index.less'
+
+type IProps = {
+    text?: string
+    loading?: boolean
+}
+
+export const LoadingText = ({loading, text}: IProps) => {
+    return (<div className={loading ?'loading-container' :'loading-container hidden'}>
+        <div className="loading">
+            <LoadingOutlined/>
+            <span className="loading-text">{text || '加载中,请稍后...'}</span>
+        </div>
+    </div>)
+}

+ 4 - 2
frontend/src/models/api.ts

@@ -12,12 +12,14 @@ export type PageReq = {
   pageSize: number
 }
 
-export type PageResp<T=any> = BaseResp<{
+export type PageData<T> = {
   current: number
   total: number
   pageSize: number
   list: T[]
-}>
+}
+
+export type PageResp<T=any> = BaseResp<PageData<T>>
 
 /**
  * 虚拟主机,后端,跟前端不一致

+ 4 - 2
frontend/src/models/user.ts

@@ -1,7 +1,9 @@
+import {ICurdData} from "../components/curd/types.ts";
+
 /**
  * 用户
  */
-export type User = {
+export type User = ICurdData & {
     id: number
     account: string
     nickname: string
@@ -11,4 +13,4 @@ export type User = {
      * 缓存时间
      */
     timestamp: number
-}
+}

+ 3 - 2
frontend/src/pages/layout/MainLayout.tsx

@@ -8,6 +8,7 @@ import {Link} from "react-router-dom";
 import {DownOutlined} from "@ant-design/icons";
 import {ModifyPassword} from "../user/components/password";
 import {LogoutComponent} from "../user/components/logout";
+import {UserInfoDialog} from "../user/info";
 
 const BreadcrumbItem = Breadcrumb.Item
 
@@ -16,7 +17,7 @@ const {Header, Content} = Layout;
 
 export const MainLayout = () => {
 
-    const navList = useAppSelector(state => state.route.navList) || []
+    const navList = useAppSelector(state => state.user.navList) || []
     const nav = useAppSelector(state => state.route.nav)
 
     const dispatch = useAppDispatch()
@@ -41,7 +42,7 @@ export const MainLayout = () => {
     const personMenus = useMemo(()=>{
         const items: MenuProps['items'] = [
             {
-                label: (<Link to="/user/info">我的信息</Link>),
+                label: (<UserInfoDialog />),
                 key: 'userinfo',
             },
             {

+ 1 - 1
frontend/src/pages/ldap/user/list/index.tsx

@@ -108,7 +108,7 @@ export const List = () => {
             .then(res => {
                 console.log('sync users', res)
                 if (res.data?.data) {
-                    setSuccess(`操作成功:同步:${res.data.data.count}条数据!'`)
+                    setSuccess(`操作成功:同步:${res.data.data.count}条数据!`)
                 } else {
                     Message.warning(res.data.msg || '同步异常!')
                 }

+ 1 - 1
frontend/src/pages/nginx/components/EditNginxBtn.tsx

@@ -11,7 +11,7 @@ import {NginxApis} from "../../../api/nginx.ts";
 import {Notify} from "auto-antd";
 import {useAppDispatch} from "../../../store";
 import {useNavigate} from "react-router";
-import {nginxPrefix} from "../../../routes/routes.tsx";
+import {nginxPrefix} from "../routes.ts";
 
 type IProps = {
   nginx: INginx

+ 1 - 1
frontend/src/pages/nginx/index.tsx

@@ -9,7 +9,7 @@ import {useAppDispatch, useAppSelector} from "../../store";
 import './index.less'
 import type { MenuProps } from 'antd';
 import { Menu } from 'antd';
-import {createNginxMenus, serverRoute} from "../../routes/routes";
+import {createNginxMenus, serverRoute} from "./routes.ts";
 import {NginxRouteParams} from "./types.ts";
 import {NginxActions} from "../../store/slice/nginx.ts";
 import {NavLink} from "react-router-dom";

+ 1 - 1
frontend/src/pages/nginx/location/index.tsx

@@ -11,7 +11,7 @@ import {AutoForm, AutoFormInstance, Message, uniqueKey} from "auto-antd";
 import {INginxServer, PLocation} from "../../../models/nginx.ts";
 import {useFormConfig} from "../config.tsx";
 import {NginxActions} from "../../../store/slice/nginx.ts";
-import {nginxPrefix, serverIndexRoute} from "../../../routes/routes";
+import {nginxPrefix, serverIndexRoute} from "../routes";
 import {NavLink} from "react-router-dom";
 import {cloneDeep} from "lodash";
 import {NginxApis} from "../../../api/nginx.ts";

+ 76 - 0
frontend/src/pages/nginx/routes.ts

@@ -0,0 +1,76 @@
+import {INginx, INginxServer} from "../../models/nginx.ts";
+import {MenuProps} from "antd";
+
+export const nginxPrefix = (id:string | number) =>`/nginx/${id}`
+/**
+ * nginx  server的首页
+ * @param menuKey 菜单的key
+ */
+export const serverIndex = (menuKey: string) => `${menuKey}/conf`
+export const serverRoute = (id: number| string,sid: number | string) => nginxPrefix(id) +`/server/${sid}`
+export const serverIndexRoute = (id:number| string,sid: number| string) => nginxPrefix(id) +`/server/${sid}/conf`
+
+/**
+ * 放到这里来,跟上面的路径好对应起来,免得两个地方不一致,修改容易出错
+ * @param nginx
+ * @param servers 虚拟主机
+ */
+export const createNginxMenus = (nginx: INginx, servers: INginxServer[]= []) => {
+    const prefix = nginxPrefix(nginx.id)
+    const initialItems: MenuProps['items'] = [
+        {
+            label: '实例设置',
+            key:`${prefix}`
+        },
+        {
+            label: 'http配置',
+            key: `${prefix}/http`,
+        },
+        {
+            label: 'SSL证书',
+            key: `${prefix}/certs`,
+        },
+        {
+            label: '负载均衡',
+            key: `${prefix}/upstream`,
+        },
+        {
+            label: 'TCP/UDP',
+            key: `${prefix}/stream`,
+        },
+    ]
+
+    const serverMenu = {
+        label: "虚拟主机",
+        key: `no_route/${prefix}/servers`,
+        children: [] as any[]
+    }
+
+    servers.forEach(s=>{
+        if (s.isStream || s.isUpstream){
+            return
+        }
+        let label = '';
+        if (s.server_name){
+            label = `${s.server_name}-${s.listen}(${s.ssl ? 'https': 'http'})`
+        }
+        serverMenu.children.push({
+            label: label,
+            key: `${prefix}/server/${s.id}`,
+        })
+    })
+
+    serverMenu.children.push({
+        label: '新增虚拟主机',
+        key: `${prefix}/server-new`
+    })
+
+    initialItems.push(serverMenu)
+
+    initialItems.push({
+        label: "帮助",
+        key: 'help'
+    })
+
+    return initialItems
+}

+ 1 - 1
frontend/src/pages/nginx/server/index.tsx

@@ -10,7 +10,7 @@ import {NginxActions} from "../../../store/slice/nginx.ts";
 import {AutoForm, AutoFormInstance, Message} from "auto-antd";
 import {cloneDeep} from "lodash";
 import {useNavigate} from "react-router";
-import {nginxPrefix} from "../../../routes/routes";
+import {nginxPrefix} from "../routes";
 import {useFormConfig} from "../config.tsx";
 import {NavLink} from "react-router-dom";
 import {NginxApis} from "../../../api/nginx.ts";

+ 24 - 1
frontend/src/pages/user/components/logout/index.tsx

@@ -1,7 +1,30 @@
+import {Modal} from "antd";
+import {LoginApis} from "../../../../api/user.ts";
+import {useAppDispatch} from "../../../../store";
+import {UserActions} from "../../../../store/slice/user.ts";
 
 export const LogoutComponent = () => {
 
+    const dispatch = useAppDispatch()
+
+    const handleLogout = () => {
+        LoginApis.logout().finally(()=>{
+            dispatch(UserActions.clearUser())
+            window.location.reload()
+        })
+    }
+
+    const onLogout = () => {
+        Modal.confirm({
+            title: '您确认要退出登录吗?',
+            onCancel: () =>{
+                console.log("Cancel")
+            },
+            onOk: handleLogout
+        })
+    }
+
     return <>
-        <span>退出登录</span>
+        <span onClick={onLogout}>退出登录</span>
     </>
 }

+ 89 - 0
frontend/src/pages/user/config.ts

@@ -0,0 +1,89 @@
+import {CurdColumn} from "../../components/curd";
+
+export const userColumns: CurdColumn[] = [
+    {
+        key: 'id',
+        title: 'ID',
+        type: 'string',
+        disabled: true,
+        required: false,
+        addable: false,
+        width: 50
+    },
+    {
+        key: 'account',
+        title: '账号(uid)',
+        type: 'string',
+        editable: false,
+        addable: true,
+        placeholder: '账号',
+        required: true,
+        width: 120
+    },
+    {
+        key: 'enable',
+        title: '是否启用',
+        type: 'switch',
+        value: true,
+    },
+    {
+        key: 'nickname',
+        title: '姓名',
+        type: 'string',
+        placeholder: 'eg. 姓名,昵称',
+        width: 150
+    },
+    {
+        key: 'password',
+        title: '密码',
+        type: 'password',
+        hidden: true,
+    },
+    {
+        key: 'roles',
+        title: '角色',
+        type: 'select',
+        width: 150,
+        option: [
+            {
+                value: 'ADMIN',
+                label: '管理员'
+            },
+            {
+                value: 'USER',
+                label: '用户'
+            }
+        ],
+        required: false,
+    },
+    {
+        key: 'source',
+        type: 'select',
+        title: '账号来源',
+        editable: true,
+        placeholder: 'eg. LDAP',
+        option: [
+            {
+                value: 'LOCAL',
+                label: '本地'
+            },
+            {
+                value: 'LDAP',
+                label: 'LDAP'
+            },
+        ],
+    },
+    {
+        key: 'createdAt',
+        title: '注册时间',
+        type: 'string',
+        editable: false,
+        addable: false,
+    },
+    {
+        key: 'remark',
+        title: '备注',
+        type: 'textarea',
+        required: false,
+    }
+]

+ 19 - 0
frontend/src/pages/user/info/index.less

@@ -0,0 +1,19 @@
+
+.user-info{
+  box-sizing: border-box;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  position: relative;
+
+  .form-wrap{
+    width: 450px;
+    padding-bottom: 24px;
+  }
+
+  .edit-btn{
+    position: absolute;
+    right: 20px;
+    top: 10px;
+  }
+}

+ 101 - 0
frontend/src/pages/user/info/index.tsx

@@ -0,0 +1,101 @@
+import './index.less'
+import {userColumns} from "../config.ts";
+import {useEffect, useMemo, useState} from "react";
+import {CurdForm} from "../../../components/curd/Form.tsx";
+import {LoginApis, userApis} from "../../../api/user.ts";
+import {User} from "../../../models/user.ts";
+import {ICurdConfig} from "../../../components/curd/types.ts";
+import {useNavigate} from "react-router";
+import {LoadingText} from "../../../components/loading";
+import {Button, Modal} from "antd";
+import {EditOutlined} from "@ant-design/icons";
+import {useAppDispatch} from "../../../store";
+import {UserActions} from "../../../store/slice/user.ts";
+
+const config: ICurdConfig<User> = {}
+
+type IProps = {
+    onClose?: () => void
+}
+
+export const UserInfo = ({ onClose }: IProps) => {
+
+    const [editable, setEditable] = useState(false)
+    const [userinfo, setUserinfo] = useState<ICurdConfig<User>>();
+    const [loading, setLoading] = useState(true)
+
+    const dispatch = useAppDispatch();
+
+    const navigate = useNavigate()
+
+    const getInfo = () => {
+        setLoading(true)
+        LoginApis.userinfo().then(res => {
+            setUserinfo(res.data || {} as any)
+        }).catch(() => {
+            setTimeout(() => {
+                navigate(-1)
+            }, 1500)
+        })
+            .finally(() => {
+                setLoading(false)
+            })
+    }
+
+    useEffect(() => {
+        getInfo()
+    }, [])
+
+    const onSave = (info: Partial<User>) => {
+        return userApis.updateMyInfo({
+            ...userinfo,
+            ...info,
+        }).then(res=>{
+            dispatch(UserActions.setUser(res))
+            return res;
+        })
+    }
+
+    const columns = useMemo(() => {
+        return userColumns.filter(item=>['account','nickname','roles','source','createdAt']
+            .includes(item.key)).map(item => {
+            return {
+                ...item,
+                addable: true,
+                editable: ['nickname'].includes(item.key)
+            }
+        })
+
+    }, [])
+
+
+    return (<div className="user-info">
+        <LoadingText loading={loading}/>
+        <div className="form-wrap">
+            <CurdForm config={config as any} columns={columns}
+                      onClose={onClose}
+                      editable={editable}
+                      onSave={onSave}
+                      onSuccess={()=>setEditable(false)}
+                      initialData={userinfo}/>
+            <Button className="edit-btn" type="primary"
+                    icon={<EditOutlined />} onClick={()=>setEditable(!editable)}
+                    size="small"
+                    loading={loading} />
+        </div>
+    </div>)
+}
+
+
+export const UserInfoDialog = () => {
+    const [visible, setVisible] = useState<boolean>(false)
+
+    return (
+       <>
+           <span onClick={()=>setVisible(true)}>我的信息</span>
+           <Modal open={visible} onCancel={()=>setVisible(false)} title="我的信息" footer={null}>
+               <UserInfo onClose={()=>setVisible(false)} />
+           </Modal>
+       </>
+    )
+}

+ 1 - 0
frontend/src/pages/user/list/index.less

@@ -0,0 +1 @@
+.user-list{}

+ 68 - 0
frontend/src/pages/user/list/index.tsx

@@ -0,0 +1,68 @@
+import {useCallback, useMemo} from "react";
+import {ICurdConfig} from "../../../components/curd/types.ts";
+import {CurdColumn, CurdPage} from "../../../components/curd";
+import './index.less'
+import {User} from "../../../models/user.ts";
+import {userApis} from "../../../api/user.ts";
+import {userColumns} from "../config.ts";
+
+const columns: CurdColumn[] = userColumns.map(item=>{
+    if (item.key == 'enable'){
+        return {
+            ...item,
+            render: (v: any) => <span>{v ? '是' : '否'}</span>
+        }
+    }
+    return item;
+})
+
+const serverConfig: ICurdConfig<User> = {
+    editDialogWidth: 500,
+    labelSpan: 6,
+}
+
+
+export const UserList = () => {
+
+    const getList = useCallback((query: any) => {
+        return userApis.getList(query)
+    }, [])
+
+    const getDetail = (data: Partial<User>) => {
+        if (!data.id) {
+            return Promise.resolve({} as User)
+        }
+        return userApis.getDetails(data.id)
+    }
+
+    const onSave = (data: Partial<User>) => {
+        return userApis.save(data)
+    }
+
+    // eslint-disable-next-line @typescript-eslint/no-unused-vars
+    const operationRender = (_record: User, _: number) => {
+        return (<></>)
+    }
+
+    const config = useMemo(() => {
+
+        return {
+            ...serverConfig,
+            operationRender
+        } as ICurdConfig<User>
+
+    }, [])
+
+
+    const onSaveSuccess = (user: Partial<User>) => {
+        console.log('onSaveSuccess', user)
+    }
+
+
+    return (<div className="user-list">
+        <CurdPage columns={columns} getList={getList} getDetail={getDetail}
+                  onSave={onSave}
+                  onSuccess={onSaveSuccess}
+                  config={config}/>
+    </div>)
+}

+ 9 - 15
frontend/src/routes/index.tsx

@@ -7,7 +7,6 @@ import {Spin} from "antd";
 import './index.less'
 import {useLocation, useNavigate} from "react-router";
 import {UserActions} from "../store/slice/user.ts";
-import dayjs from "dayjs";
 import {SSOWrapper} from "../pages/login/sso.tsx";
 import {MainLayout} from "../pages/layout/MainLayout.tsx";
 import {ErrorPage} from "../pages/error";
@@ -16,6 +15,7 @@ import {SignupPage} from "../pages/signup";
 import {NginxLayout} from "../pages/nginx/layout.tsx";
 import {ldapRoutes} from '../pages/ldap/layout.tsx'
 import {ErrorBoundary} from "../components/error/ErrorBoundary.tsx";
+import {UserList} from "../pages/user/list";
 /**
  * @author tuonian
  * @date 2023/6/26
@@ -38,7 +38,7 @@ export const RouteWrapper = ({Component, ...props}: RouteWrapperProps) => {
     const fetchUser = () => {
         setLoading(true);
         LoginApis.userinfo().then(({data}) => {
-            dispatch(UserActions.setUser(data.data))
+            dispatch(UserActions.setUser(data as any))
             console.log('fetchUser', data)
         }).catch(e => {
             console.warn('userinfo fail', e);
@@ -50,18 +50,8 @@ export const RouteWrapper = ({Component, ...props}: RouteWrapperProps) => {
     }
 
     useEffect(() => {
-        if (!user?.account) {
-            fetchUser()
-            return
-        }
-        const unix = dayjs().unix()
-        const cacheUnix = user.timestamp || 0;
-        if (unix - cacheUnix < 3600) {
-            setLoading(false)
-        } else {
-            fetchUser()
-        }
-    }, [user])
+        fetchUser()
+    }, [])
 
     if (!user || loading) {
         return (<div className="empty-loading">
@@ -100,7 +90,11 @@ const router = createHashRouter([
                 //     label: 'Nginx列表'
                 // }
             },
-            ...ldapRoutes
+            ...ldapRoutes,
+            {
+                path: 'user/list',
+                Component: UserList,
+            },
         ],
         errorElement: <ErrorBoundary />
     },

+ 17 - 73
frontend/src/routes/routes.tsx

@@ -1,76 +1,20 @@
-import {INginx, INginxServer} from "../models/nginx.ts";
-import {MenuProps} from "antd";
-
-export const nginxPrefix = (id:string | number) =>`/nginx/${id}`
 /**
- * nginx  server的首页
- * @param menuKey 菜单的key
+ * 顶部的菜单
  */
-export const serverIndex = (menuKey: string) => `${menuKey}/conf`
-export const serverRoute = (id: number| string,sid: number | string) => nginxPrefix(id) +`/server/${sid}`
-export const serverIndexRoute = (id:number| string,sid: number| string) => nginxPrefix(id) +`/server/${sid}/conf`
-
-/**
- * 放到这里来,跟上面的路径好对应起来,免得两个地方不一致,修改容易出错
- * @param nginx
- * @param servers 虚拟主机
- */
-export const createNginxMenus = (nginx: INginx, servers: INginxServer[]= []) => {
-  const prefix = nginxPrefix(nginx.id)
-  const initialItems: MenuProps['items'] = [
-    {
-      label: '实例设置',
-      key:`${prefix}`
-    },
-    {
-      label: 'http配置',
-      key: `${prefix}/http`,
-    },
-    {
-      label: 'SSL证书',
-      key: `${prefix}/certs`,
-    },
-    {
-      label: '负载均衡',
-      key: `${prefix}/upstream`,
-    },
-    {
-      label: 'TCP/UDP',
-      key: `${prefix}/stream`,
-    },
-  ]
-
-  const serverMenu = {
-    label: "虚拟主机",
-    key: `no_route/${prefix}/servers`,
-    children: [] as any[]
+export const NavList = [
+  {
+    key: '/nginx',
+    label: 'Nginx管理',
+    roles: []
+  },
+  {
+    key: '/ldap',
+    label: 'LDAP管理',
+    roles: ['ADMIN']
+  },
+  {
+    key: '/user/list',
+    label: '用户管理',
+    roles: ['ADMIN']
   }
-
-  servers.forEach(s=>{
-    if (s.isStream || s.isUpstream){
-      return
-    }
-    let label = '';
-    if (s.server_name){
-      label = `${s.server_name}-${s.listen}(${s.ssl ? 'https': 'http'})`
-    }
-    serverMenu.children.push({
-      label: label,
-      key: `${prefix}/server/${s.id}`,
-    })
-  })
-
-  serverMenu.children.push({
-    label: '新增虚拟主机',
-    key: `${prefix}/server-new`
-  })
-
-  initialItems.push(serverMenu)
-
-  initialItems.push({
-    label: "帮助",
-    key: 'help'
-  })
-
-  return initialItems
-}
+]

+ 4 - 0
frontend/src/store/slice/route.ts

@@ -17,6 +17,10 @@ const initialState: IRouteState = {
     {
       key: '/ldap',
       label: 'LDAP管理'
+    },
+    {
+      key: '/user/list',
+      label: '用户管理'
     }
   ],
   routes: {

+ 22 - 0
frontend/src/store/slice/user.ts

@@ -1,16 +1,27 @@
 import { createSlice, PayloadAction } from '@reduxjs/toolkit';
 import {User} from "../../models/user.ts";
 import dayjs from "dayjs";
+import {MenuProps} from "antd";
+import {NavList} from "../../routes/routes.tsx";
 
 export type IUserState = {
   user?: User & { timestamp: number };
   isLogin: boolean;
   isAdmin?: boolean;
+  navList: MenuProps['items'],
+  nav: string
 };
 
 const initialState: IUserState = {
   isLogin: false,
   isAdmin: false,
+  nav: '/nginx',
+  navList: [
+    {
+      key: '/nginx',
+      label: 'Nginx管理'
+    },
+  ],
 };
 
 const userSlice = createSlice({
@@ -23,6 +34,17 @@ const userSlice = createSlice({
       state.isLogin = true;
       const roles = state.user?.roles || '';
       state.isAdmin = roles.indexOf('ADMIN') > -1;
+      if (state.isAdmin){
+        state.navList = [...NavList]
+      }else {
+        const roleList = roles.split(',') || []
+        state.navList = NavList.filter(nav=>{
+          if (!nav.roles || nav.roles.length == 0){
+            return true
+          }
+          return nav.roles.find(r => roleList.includes(r))
+        })
+      }
     },
     clearUser(state) {
       state.isLogin = false;

+ 55 - 0
server/modules/user/controller.go

@@ -2,6 +2,7 @@ package user
 
 import (
 	"encoding/json"
+	"errors"
 	"github.com/astaxie/beego/logs"
 	"nginx-ui/server/base"
 	"nginx-ui/server/models"
@@ -36,6 +37,15 @@ func (c *Controller) Login() {
 	c.PostJson(resp)
 }
 
+// Logout 退出登录
+func (c *Controller) Logout() {
+	user := c.RequiredUser()
+	if user != nil {
+		c.DelSession("user")
+	}
+	c.Json()
+}
+
 func (c *Controller) User() {
 	user := c.RequiredUser()
 	if user == nil {
@@ -88,6 +98,29 @@ func (c *Controller) Update() {
 	c.SetData(resp).Json()
 }
 
+// Update 获取全部用户信息
+func (c *Controller) UpdateUserInfo() {
+	user := c.RequiredUser()
+	if user == nil {
+		return
+	}
+	req := models.User{}
+	if !c.ReadBody(&req) {
+		return
+	}
+	if req.Id != user.Id {
+		c.ErrorJson(errors.New("更新失败,无操作权限!"))
+		return
+	}
+	resp, err := c.service.Update(&req)
+	if err != nil {
+		logs.Warn("Users get fail: %v", err)
+		c.ErrorJson(err)
+		return
+	}
+	c.SetData(resp).Json()
+}
+
 // UpdatePassword Update 获取全部用户信息
 func (c *Controller) UpdatePassword() {
 	user := c.RequiredUser()
@@ -107,3 +140,25 @@ func (c *Controller) UpdatePassword() {
 	}
 	c.SetMsg("密码更新成功").Json()
 }
+
+// GetDetail 获取全部用户信息
+func (c *Controller) GetDetail() {
+	user := c.RequiredUser()
+	if user == nil {
+		return
+	}
+
+	id, err := c.GetIntQuery("id")
+	if err != nil {
+		logs.Warn("Users get fail: %v", err)
+		c.ErrorJson(err)
+		return
+	}
+	query, err := c.service.GetDetail(id)
+	if err != nil {
+		logs.Warn("Users get fail: %v", err)
+		c.ErrorJson(err)
+		return
+	}
+	c.SetData(query).Json()
+}

+ 27 - 4
server/modules/user/service.go

@@ -63,7 +63,7 @@ func (u *UserService) SignUp(req []byte) *models.RespData {
 func (u *UserService) Users(req *vo.PageReq) (*vo.PageResp, error) {
 	req.Ensure()
 	qs := orm.NewOrm().QueryTable(new(models.User))
-	qs = qs.Offset(req.Offset).Limit(req.PageSize)
+	qs = qs.Offset(req.Offset).Limit(req.PageSize).OrderBy("-Id")
 
 	var list []models.User
 	_, err := qs.All(&list)
@@ -75,14 +75,16 @@ func (u *UserService) Users(req *vo.PageReq) (*vo.PageResp, error) {
 		return nil, err
 	}
 
+	var resList []models.User
 	for _, user := range list {
 		user.Password = config.ReplacePassword
+		resList = append(resList, user)
 	}
 	resp := vo.PageResp{
 		PageSize: req.PageSize,
 		Current:  req.Current,
 		Total:    count,
-		List:     list,
+		List:     resList,
 	}
 	return &resp, err
 }
@@ -92,11 +94,20 @@ func (u *UserService) Update(req *models.User) (*models.User, error) {
 
 	exist := models.User{Id: req.Id}
 	err := o.Read(&exist)
-	if err != nil {
-		return nil, errors.New("该用户不存在或者已被删除!")
+	if err != nil && !errors.Is(err, orm.ErrNoRows) {
+		return nil, err
+	} else if err != nil {
+		req.Password = utils.GetSHA256HashCode(req.Password)
+		_, err = o.Insert(req)
+		if err != nil {
+			return nil, err
+		}
+		return req, nil
 	}
 	if req.Password == "" || req.Password == config.ReplacePassword {
 		req.Password = exist.Password
+	} else {
+		req.Password = utils.GetSHA256HashCode(req.Password)
 	}
 	_, err = o.Update(req)
 	if err != nil {
@@ -105,6 +116,18 @@ func (u *UserService) Update(req *models.User) (*models.User, error) {
 	return req, nil
 }
 
+func (u *UserService) GetDetail(id int) (*models.User, error) {
+	o := orm.NewOrm()
+
+	exist := models.User{Id: id}
+	err := o.Read(&exist)
+	if err != nil {
+		return nil, errors.New("该用户不存在或者已被删除!")
+	}
+	exist.Password = config.ReplacePassword
+	return &exist, nil
+}
+
 // UpdatePassword 更新用户密码,如果存在LDAP账号,则更新下
 func (u *UserService) UpdatePassword(req *vo.UserUpdatePassword) error {
 	o := orm.NewOrm()

+ 4 - 1
server/routers/router.go

@@ -52,10 +52,13 @@ func init() {
 		beego.NSRouter("/logger", &nginx_controller.LoggerController{}),
 
 		beego.NSRouter("/user/login", userController, "post:Login"),
+		beego.NSRouter("/user/logout", userController, "post:Logout"),
 		beego.NSRouter("/user/info", userController, "get:User"),
+		beego.NSRouter("/user/info", userController, "post:UpdateUserInfo"),
 		beego.NSRouter("/user/register", userController, "post:Register"),
 		beego.NSRouter("/user/list", userController, "post:Users"),
-		beego.NSRouter("/user/update", userController, "post:Update"),
+		beego.NSRouter("/user/detail", userController, "get:GetDetail"),
+		beego.NSRouter("/user/save", userController, "post:Update"),
 		beego.NSRouter("/user/modifyPassword", userController, "post:UpdatePassword"),
 		beego.NSRouter("/oauth2", &oauth2.Controller{}),
 		beego.NSRouter("/oauth2/callback", &oauth2.Controller{}, "post:Callback"),