tuonian 2 тижнів тому
батько
коміт
5502c7ff9b

+ 2 - 2
frontend/src/api/settings.ts

@@ -22,7 +22,7 @@ export namespace Settings {
         path: string
         navLink?: string
         target: 'APP' | 'TARGET' | 'TAB'
-        type: 'MENU' | 'NAV' | 'LINK'
+        type: 'MENU' | 'NAV' | 'LINK' | 'FOLDER'
         index: boolean
         pid: string
         uid: number
@@ -62,7 +62,7 @@ export const SettingsApis = {
         return request.post<BaseResp<Settings.Route>>('/settings/route/save', data)
     },
     removeRoute: (id: string) => {
-        return request.post<BaseResp<void>>(`/settings/route/remove`, {id})
+        return request.post<BaseResp<void>>(`/settings/route/delete`, {id})
     },
 }
 

Різницю між файлами не показано, бо вона завелика
+ 2 - 0
frontend/src/assets/ic_home.svg


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

@@ -0,0 +1,3 @@
+.curd-cell{
+
+}

+ 28 - 0
frontend/src/components/curd/cell/index.tsx

@@ -0,0 +1,28 @@
+import React from "react";
+import {CurdColumn} from "../index.tsx";
+import {Tooltip} from "antd";
+import './index.less'
+
+type IProps = {
+    children?: React.ReactNode[] | React.ReactNode;
+    className?: string;
+    colSpan?: number;
+    onMouseEnter?: (evt: MouseEvent) => void;
+    onMouseLeave?: (evt: MouseEvent) => void;
+    rowSpan?: number
+    style?: React.CSSProperties;
+    title?: React.ReactNode | string;
+    config: CurdColumn
+    record?: Partial<any>
+    dataIndex: string
+}
+
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+export const Cell = ({ children, config,className,record,dataIndex, ...props}: IProps) => {
+
+    return (<td {...props as any} className={className +' curd-cell'}>
+        {
+            config?.ellipsis ? <Tooltip title={children}>{children}</Tooltip> : children
+        }
+    </td>)
+}

+ 1 - 0
frontend/src/components/curd/form.less

@@ -16,6 +16,7 @@
         }
         .input-item{
           max-width: 100%;
+          width: 100%;
         }
       }
     }

+ 38 - 15
frontend/src/components/curd/index.tsx

@@ -1,5 +1,5 @@
 import {AutoColumn, AutoText, isNullOrTrue, Message} from "auto-antd";
-import {Button, Drawer, Switch, Table, TableProps} from "antd";
+import {Button, Drawer, Modal, Switch, Table, TableProps} 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";
@@ -7,6 +7,7 @@ import {LDAP} from "../../api/ldap.ts";
 import {DeleteOutlined, EditOutlined, LoadingOutlined, PlusOutlined, SyncOutlined} from "@ant-design/icons";
 import './index.less'
 import {ICurdConfig, ICurdData} from "./types.ts";
+import {Cell} from "./cell";
 
 
 export type CurdColumn = AutoColumn & {
@@ -16,6 +17,7 @@ export type CurdColumn = AutoColumn & {
     addable?: boolean // 添加时可编辑
     onlyAdd?: boolean // 仅新增时展示
     editHide?: boolean // 编辑时隐藏
+    ellipsis?: boolean // 完整显示
 }
 
 
@@ -83,21 +85,30 @@ export function CurdPage<T extends ICurdData>(
         }
     }
     const handleDelete = async (edit: T) => {
-        setLoading(true)
-        try {
-            await props.onDelete?.(edit)
-            setQuery((q: any)=>({...q}))
-        }catch(err) {
-            if(err instanceof Error){
-                Message.warning(err.message)
-            }else if (typeof err === "string") {
-                Message.warning(err)
-            }else {
-                Message.warning('删除失败,错误信息为:'+ err)
+        Modal.confirm({
+            type: 'warning',
+            title: '您确定要删除该数据吗?',
+            content: '删除操作不可撤销,请谨慎操作',
+            onOk: async () => {
+                setLoading(true)
+                try {
+                    await props.onDelete?.(edit)
+                    setQuery((q: any)=>({...q}))
+                }catch(err: any) {
+                    if(err instanceof Error){
+                        Message.warning(err.message)
+                    }else if (typeof err === "string") {
+                        Message.warning(err)
+                    }else if (err?.msg) {
+                        Message.warning('删除失败,错误信息为:'+ err.msg)
+                    }else {
+                        Message.warning('删除失败,错误信息为:'+ JSON.stringify(err,null,2))
+                    }
+                }finally {
+                    setLoading(false)
+                }
             }
-        }finally {
-            setLoading(false)
-        }
+        })
     }
 
 
@@ -109,6 +120,13 @@ export function CurdPage<T extends ICurdData>(
                     dataIndex: column.key,
                     title: column.title,
                     width: column.width,
+                    onCell: (record: Partial<T>) => {
+                        return {
+                            record,
+                            config: column,
+                            dataIndex: column.key,
+                        }
+                    }
                 }
                 if (col.type == 'switch' && !col.render){
                     col.render = (value: boolean) => <Switch checked={value} disabled/>
@@ -193,6 +211,11 @@ export function CurdPage<T extends ICurdData>(
                        current: query.current,
                        onChange: onPageChange,
                    }}
+                   components={{
+                       body: {
+                           cell: Cell,
+                       }
+                   }}
                    expandable={expandable}
                    bordered={config?.bordered}
                    rowKey={config?.rowKey ?? 'id'}

+ 0 - 2
frontend/src/components/fullscreen/index.less

@@ -1,6 +1,4 @@
 .full-screen-exit{
   position: fixed;
-  transform: translateY(48px);
   transition: all .3s;
-  opacity: 0.8;
 }

+ 7 - 3
frontend/src/components/fullscreen/index.tsx

@@ -5,8 +5,11 @@ import './index.less'
 import {FullscreenExitOutlined, FullscreenOutlined} from "@ant-design/icons";
 import {useState} from "react";
 
+type IProps = {
+    hideInNonFullscreen?: boolean
+}
 
-export const Fullscreen = () => {
+export const Fullscreen = ({hideInNonFullscreen}: IProps) => {
 
     const fullScreen = useAppSelector(state => state.settings.fullScreen)
     const dispatch = useAppDispatch()
@@ -24,7 +27,7 @@ export const Fullscreen = () => {
     }
 
     if (!fullScreen) {
-        return (
+        return hideInNonFullscreen ? null : (
             <Button icon={<FullscreenOutlined />} onClick={()=>setFullScreen(true)}
                     onDragEnd={onDragEnd}
                     style={{ marginLeft: 10,marginRight: 10, color: "#fff" }}
@@ -34,7 +37,8 @@ export const Fullscreen = () => {
     return (<div className="full-screen-exit" style={{ top: pageY, left: pageX }}>
         <Button icon={<FullscreenExitOutlined />} onClick={()=>setFullScreen(false)}
                 onDragEnd={onDragEnd}
-                type="primary" draggable={true} />
+                style={{ color: 'white'}}
+                type="text" draggable={true} />
     </div>)
 }
 

+ 34 - 0
frontend/src/pages/home/index.less

@@ -0,0 +1,34 @@
+.home{
+  position: relative;
+  background-image: url("@/assets/bg.jpg");
+  background-size: cover;
+  display: flex;
+  flex-direction: column;
+  justify-content: flex-start;
+  padding: 48px;
+  align-items: center;
+  height: 100%;
+  width: 100%;
+  &-container{
+    background: rgba(255, 255, 255, 0.3);
+    width: 100%;
+    border-radius: 10px;
+    box-sizing: border-box;
+    padding: 24px;
+    .home-folder{
+      padding-bottom: 20px;
+      box-sizing: border-box;
+      .home-folder-title{
+        font-size: 13px;
+        color: white;
+        margin-bottom: 5px;
+      }
+    }
+    .home-folder-links{
+      display: flex;
+      flex-direction: row;
+      gap: 16px;
+      align-items: center;
+    }
+  }
+}

+ 65 - 0
frontend/src/pages/home/index.tsx

@@ -0,0 +1,65 @@
+import './index.less'
+import {useAppSelector} from "../../store";
+import {QuickLink} from "./link";
+import {useEffect, useMemo, useRef} from "react";
+import {Button} from "antd";
+import {PlusOutlined} from "@ant-design/icons";
+import {AddLinkModal} from "../user/links";
+
+export const HomePage = () => {
+
+    const folders = useAppSelector(state => state.user.links)
+    const addLinkRef = useRef<any>()
+
+    useEffect(() => {
+        console.log('folders', folders)
+    }, [folders]);
+
+    const otherLinks = useMemo(()=>{
+        return folders.filter(item=>item.type == 'LINK' && item.uid == 0)
+    },[folders])
+
+    const myLinks = useMemo(()=>{
+        return folders.filter(item=>item.type == 'LINK' && item.uid > 0)
+    },[folders])
+
+    return (<div className="home">
+        <div className="home-container">
+            <div className="home-folder">
+                <div className="home-folder-title">我的收藏</div>
+                <div className="home-folder-links">
+                    {
+                        myLinks?.map(link => (<QuickLink route={link}/>))
+                    }
+                    <Button onClick={()=>addLinkRef?.current?.onShow()} style={{color: 'white'}} type="text" icon={<PlusOutlined />}>添加</Button>
+                </div>
+            </div>
+            {
+                folders.map(links => {
+                    if (links.type == 'FOLDER') {
+                        return (
+                            <div className="home-folder">
+                                <div className="home-folder-title">{links.title}</div>
+                                <div className="home-folder-links">
+                                    {
+                                        links.children?.map(link => (<QuickLink route={link}/>))
+                                    }
+                                </div>
+                            </div>
+                        )
+                    }
+                    return null
+                })
+            }
+            <div className="home-folder">
+                <div className="home-folder-title">其它链接</div>
+                <div className="home-folder-links">
+                    {
+                        otherLinks?.map(link => (<QuickLink route={link}/>))
+                    }
+                </div>
+            </div>
+        </div>
+        <AddLinkModal ref={addLinkRef} />
+    </div>)
+}

+ 25 - 0
frontend/src/pages/home/link/index.less

@@ -0,0 +1,25 @@
+.quick-link{
+  background: rgba(255, 255, 255, 0.6);
+  box-sizing: border-box;
+  padding: 10px 16px;
+  width: 200px;
+  font-size: 14px;
+  border-radius: 5px;
+  height: 60px;
+  cursor: pointer;
+  &-title{
+    font-weight: 500;
+  }
+  &-desc{
+    font-size: 12px;
+    color: #666666;
+    display: -webkit-box;
+    -webkit-box-orient: vertical;
+    -webkit-line-clamp: 2;
+    overflow: hidden;
+    text-overflow: ellipsis;
+  }
+  &:hover{
+    background: rgba(255, 255, 255, 0.9);
+  }
+}

+ 28 - 0
frontend/src/pages/home/link/index.tsx

@@ -0,0 +1,28 @@
+import {Settings} from "../../../api/settings.ts";
+import './index.less'
+import {useNavigate} from "react-router";
+
+type IProps = {
+    route: Settings.Route
+}
+
+export const QuickLink = ({route}: IProps) => {
+
+    const navigate = useNavigate()
+
+    const handleNavigate = () => {
+        if (route.target == 'TARGET'){
+            window.open(route.path)
+        }else if (route.target == 'APP'){
+            navigate(`/app/${route.id}`)
+        }else {
+            navigate(route.path)
+        }
+    }
+
+    return (<div onClick={handleNavigate} className="quick-link">
+        <div className="quick-link-title">{route.title}</div>
+        <div className="quick-link-desc">{route.brief}</div>
+    </div>)
+
+}

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

@@ -8,7 +8,7 @@ import {DownOutlined} from "@ant-design/icons";
 import {ModifyPassword} from "../user/components/password";
 import {LogoutComponent} from "../user/components/logout";
 import {UserInfoDialog} from "../user/info";
-import HomeIcon from '../../assets/ic_home.png'
+import HomeIcon from '../../assets/ic_home.svg'
 import {Fullscreen} from "../../components/fullscreen";
 import {ItemType} from "antd/es/menu/hooks/useItems";
 import {Settings} from "../../api/settings.ts";
@@ -42,7 +42,7 @@ export const MainLayout = () => {
 
 
     const navList = useMemo(() => {
-        return _navList.map(item => {
+        const list = _navList.map(item => {
             return {
                 key: item.id,
                 label: item.title,
@@ -50,8 +50,17 @@ export const MainLayout = () => {
                 path: item.navLink || item.path
             } as INavList
         })
+        return [
+            {
+                key: 'home',
+                label: '首页',
+                path: 'home',
+                data: {}
+            } as INavList
+        ].concat(list)
     }, [_navList])
 
+
     const handleSetNav = ( menu: INavList) => {
         if (!menu.data){
             return
@@ -74,6 +83,9 @@ export const MainLayout = () => {
             handleSetNav(nav)
         }
     }
+    const navigateToHome = () => {
+        handleSetNav(navList[0])
+    }
 
     useEffect(() => {
         if (nav || !matches.length) {
@@ -115,7 +127,7 @@ export const MainLayout = () => {
     return (
         <Layout className={`customLayout ${fullScreen ? 'fullScreen' : 'nonFullScreen'}`}>
             <Header style={{display: 'flex', alignItems: 'center'}}>
-                <div className="demo-logo">
+                <div className="demo-logo" onClick={navigateToHome}>
                     <img className="home-icon" src={HomeIcon} alt="logo"/>
                 </div>
                 <Menu
@@ -151,6 +163,7 @@ export const MainLayout = () => {
                     <Outlet/>
                 </Content>
             </Layout>
+            <Fullscreen hideInNonFullscreen={true} />
         </Layout>
     );
 };

+ 7 - 6
frontend/src/pages/layout/layout.less

@@ -10,13 +10,14 @@
   .ant-layout-header{
     height: 48px;
     line-height: 48px;
+    padding: 0 25px;
   }
   .ant-layout-content{
 
   }
   .demo-logo{
-    margin-left: -40px;
-    margin-right: 15px;
+    margin-left: -25px;
+    margin-right: 2px;
     padding: 0 16px;
     cursor: pointer;
   }
@@ -27,14 +28,14 @@
 
   &.fullScreen{
     .ant-layout-header{
-      transform: translateY(-100%);
-      transition: transform 0.5s;
+      margin-top: -48px;
+      transition: margin-top 0.5s;
     }
   }
   &.nonFullScreen{
     .ant-layout-header{
-      transform: translateY(0);
-      transition: transform 0.5s;
+      margin-top: 0;
+      transition: margin-top 0.5s;
     }
   }
 }

+ 15 - 14
frontend/src/pages/layout/menu/index.tsx

@@ -1,9 +1,9 @@
-import {useRoleData} from "../../../routes/hooks.ts";
+import {useRouteData} from "../../../routes/hooks.ts";
 import {useEffect, useMemo, useState} from "react";
-import {RouteObjectData} from "../../../routes/types.ts";
 import {Menu, MenuProps} from "antd";
 import './index.less'
 import {Outlet, useNavigate} from "react-router";
+import {Settings} from "../../../api/settings.ts";
 
 
 type IProps = {
@@ -13,18 +13,18 @@ type IProps = {
 export const MenuLayout = ({routeId}: IProps) => {
 
 
-    const routeData = useRoleData(routeId)
+    const routeData = useRouteData(routeId)
     const navigate = useNavigate()
-    const [active,setActive] = useState<RouteObjectData>()
+    const [active,setActive] = useState<string>()
 
-    useEffect(() => {
-        console.log('routeData', routeData)
-    }, [routeData])
+    // useEffect(() => {
+    //     console.log('routeData', routeData)
+    // }, [routeData])
 
     const menus = useMemo(() => {
-        return (routeData.children || []).map((item: RouteObjectData) => {
+        return (routeData.children || []).map((item: Settings.Route) => {
             return {
-                label: item.label || item.path,
+                label: item.title || item.path,
                 key: item.path,
                 data: item
             }
@@ -33,22 +33,23 @@ export const MenuLayout = ({routeId}: IProps) => {
     }, [routeData])
 
     const onMenuClick = (item: any) => {
-        console.log('menuClick', item)
+        // console.log('menuClick', item)
         navigate(item.key)
-        setActive(item.data)
+        setActive(item.key)
     }
 
     useEffect(() => {
-        if (menus?.length){
+        if (menus?.length && !active){
             onMenuClick(menus[0])
         }
-    }, [menus]);
+    }, [menus,active]);
 
 
     return (<div className="menu-layout">
         <div className="menu-layout-menu">
             <Menu items={menus} onClick={onMenuClick}
-                  activeKey={active?.path}/>
+                  selectedKeys={active ? [active]: []}
+                  activeKey={active}/>
         </div>
         <div className="menu-layout-content">
             <Outlet />

+ 7 - 1
frontend/src/pages/routes/list/index.tsx

@@ -31,6 +31,7 @@ const columns: CurdColumn[] = [
         placeholder: '菜单名称',
         required: true,
         addable: true,
+        ellipsis: true,
     },
     {
         key: 'path',
@@ -72,6 +73,10 @@ const columns: CurdColumn[] = [
                 label: '导航栏',
                 value: 'NAV'
             },
+            {
+              label: '文件夹',
+              value: 'FOLDER'
+            },
             {
                 label: '快捷方式',
                 value: 'LINK'
@@ -126,7 +131,8 @@ const columns: CurdColumn[] = [
         title: '简介',
         value: '',
         required: false,
-        rows: 4
+        rows: 4,
+        ellipsis: true,
     }
 ]
 

+ 74 - 20
frontend/src/pages/user/links/index.tsx

@@ -1,10 +1,14 @@
-import {useCallback, useMemo, useState} from "react";
-import {Alert} from "antd";
+import {forwardRef, useCallback, useImperativeHandle, useMemo, useState} from "react";
+import {Alert, Modal} from "antd";
 import './index.less'
 import {CurdColumn, CurdPage} from "../../../components/curd";
 import {ICurdConfig} from "../../../components/curd/types.ts";
 import {Settings, SettingsApis} from "../../../api/settings.ts";
 import {PageData} from "../../../models/api.ts";
+import {CurdForm} from "../../../components/curd/Form.tsx";
+import {useAppDispatch} from "../../../store";
+import {UserActions} from "../../../store/slice/user.ts";
+import {LoginApis} from "../../../api/user.ts";
 
 const columns: CurdColumn[] = [
     {
@@ -13,7 +17,9 @@ const columns: CurdColumn[] = [
         type: 'string',
         required: true,
         width: 220,
-        addable: true,
+        addable: false,
+        editable: false,
+        placeholder: '网站的唯一ID,不可重复'
     },
     {
         key: 'title',
@@ -34,17 +40,21 @@ const columns: CurdColumn[] = [
         key: 'type',
         type: 'select',
         title: '菜单类型',
-        value: 'MENU',
+        value: 'LINK',
         width: 100,
         option: [
             {
-                label: '导航栏',
-                value: 'NAV'
+                label: '文件夹',
+                value: 'FOLDER'
             },
             {
                 label: '快捷方式',
                 value: 'LINK'
-            }
+            },
+            {
+                label: '导航栏',
+                value: 'NAV'
+            },
         ]
     },
     {
@@ -63,7 +73,7 @@ const columns: CurdColumn[] = [
                 value: 'APP',
             },
             {
-                label: '新标签',
+                label: '路由',
                 value: 'TAB',
             },
         ]
@@ -73,7 +83,8 @@ const columns: CurdColumn[] = [
         type: 'number',
         title: '排序',
         placeholder: '越大越靠前',
-        width: 100
+        width: 100,
+        required: false,
     },
     {
         key: 'brief',
@@ -93,6 +104,18 @@ const serverConfig: ICurdConfig<Settings.Route> = {
     operationWidth: 120,
 }
 
+const onSave = (data: Partial<Settings.Route>) => {
+    return SettingsApis.saveRoute({
+        pid: "",
+        uid: -1,
+        deleted: true,
+        ...data,
+    })
+        .then(res => {
+            return res.data.data as Settings.Route;
+        })
+}
+
 /**
  * 用户列表的操作
  * @constructor
@@ -137,17 +160,7 @@ export const UserLinks = () => {
         return Promise.resolve({...data} as Settings.Route)
     }
 
-    const onSave = (data: Partial<Settings.Route>) => {
-        return SettingsApis.saveRoute({
-            pid: "",
-            uid: -1,
-            deleted: true,
-            ...data,
-        })
-            .then(res => {
-                return res.data.data as Settings.Route;
-            })
-    }
+
 
     const handleDelete = (record: Settings.Route) => {
         return SettingsApis.removeRoute(record.id)
@@ -176,3 +189,44 @@ export const UserLinks = () => {
                   config={config}/>
     </>)
 }
+
+
+type IAddProps = {
+    onSuccess?: () => void
+}
+
+export const AddLinkModal = forwardRef<any,IAddProps>((_props, ref) => {
+    const [visible,setVisible] = useState<boolean>(false)
+    const dispatch = useAppDispatch()
+
+    const onShow = () => {
+        setVisible(true)
+    }
+
+    const onClose = () => {
+        setVisible(false)
+    }
+
+    const onSuccess = () => {
+        LoginApis.userinfo().then(({data}) => {
+            dispatch(UserActions.setUser(data as any))
+            _props.onSuccess?.()
+        }).catch(e => {
+            console.warn('userinfo fail', e);
+        }).finally(()=>{
+            setVisible(false)
+        })
+    }
+
+    useImperativeHandle(ref,()=>{
+        return {
+            onShow
+        }
+    })
+
+    return (<Modal open={visible} onCancel={onClose}
+                   title="添加快捷方式"
+                   footer={null} destroyOnClose={true}>
+        <CurdForm columns={columns} onSave={onSave as any} onClose={onClose} onSuccess={onSuccess}/>
+    </Modal>)
+})

+ 2 - 14
frontend/src/routes/hooks.ts

@@ -100,20 +100,8 @@ export const useDynamicRoutes = () => {
 }
 
 
-export const useRoleData = (routeId = "") => {
-    const routeMap = useMemo(() => {
-        const map: { [key: string]: RouteObjectData } = {}
-        const addMap = (r: RouteObjectData) => {
-            if (r.id) {
-                map[r.id] = r
-            }
-            if (r.children?.length) {
-                r.children.forEach(c => addMap(c))
-            }
-        }
-        routes.forEach(route => addMap(route))
-        return map;
-    }, [])
+export const useRouteData = (routeId = "") => {
+    const routeMap = useAppSelector(state => state.user.routeMap)
     return useMemo(() => {
         return routeMap[routeId] || {}
     }, [routeId, routeMap])

+ 3 - 1
frontend/src/routes/index.tsx

@@ -14,6 +14,7 @@ import {CONFIG} from "../config/consts.ts";
 import {ErrorBoundary} from "./components/ErrorBoundary.tsx";
 import {ModifyPasswordPage} from "../pages/user/password/modify";
 import {MicroAppPage} from "../pages/microApp";
+import {HomePage} from "../pages/home";
 
 /**
  * @author tuonian
@@ -49,7 +50,8 @@ export const MyRouter = () => {
                     <Route id="INDEX" path="/*" element={<RouteWrapper Component={MainLayout}/>} handle={{ skipAuth: true }}>
                         <Route path="app/:id" Component={MicroAppPage} index={true}  handle={{ skipAuth: true }}/>
                         {routes.map((item, idx) => (buildRoutes(item, idx)))}
-                        <Route path="modifyPassword" Component={ModifyPasswordPage}  handle={{ skipAuth: true }}/>
+                        <Route id="MODIFY_PASSWORD" path="modifyPassword" Component={ModifyPasswordPage}  handle={{ skipAuth: true }}/>
+                        <Route path="home" Component={HomePage} handle={{ skipAuth: true }} />
                     </Route>
                     {
                         openRoutes.map((item,idx) => (<Route key={`${idx}_${item.path}`}

+ 1 - 1
frontend/src/routes/routes.tsx

@@ -51,7 +51,7 @@ export const routes: RouteObject[] = [
     {
         id: 'USER_MANAGER',
         path: 'user',
-        element: <MenuLayout routeId="User"/>,
+        element: <MenuLayout routeId="USER_MANAGER"/>,
         children: [
             {
                 id: 'USER_LIST',

+ 13 - 4
frontend/src/store/slice/user.ts

@@ -33,16 +33,25 @@ const userSlice = createSlice({
             state.isAdmin = roles.indexOf('ADMIN') > -1;
             const routeMap: { [key: string]: Settings.Route } = {};
             const navList: Settings.Route[] = []
-            const linkList: Settings.Route[] = []
             action.payload.routes?.forEach((route: Settings.Route) => {
-                routeMap[route.id] = route
+                routeMap[route.id] = {...route}
                 if (route.type == 'NAV') {
                     navList.push({...route})
-                } else if (route.type == 'LINK') {
-                    linkList.push({...route})
                 }
             })
             state.routeMap = routeMap
+          const linkList: Settings.Route[] = []
+            action.payload.routes?.forEach((route: Settings.Route) => {
+                const parent = routeMap[route.pid]
+                if (parent) {
+                    !parent.children && (parent.children = [])
+                    parent.children?.push({...route})
+                }else if (route.type == 'LINK' || route.type == 'FOLDER'){
+                  const exist = routeMap[route.id]
+                  linkList.push(exist)
+                }
+            })
+
             state.navList = navList;
             state.links = linkList;
             state.isLogin = true;

+ 31 - 0
server/init/sql.go

@@ -5,6 +5,7 @@ import (
 	"nginx-ui/server/modules/settings"
 )
 
+// 初始化菜单数据
 func ensureRoutes() {
 	routes := []models.SettingRoute{
 		{
@@ -172,6 +173,36 @@ func ensureRoutes() {
 			Deleted: false,
 			SortNum: 0,
 		},
+		{
+			Id:      "QUICK_LINKS",
+			Path:    "#",
+			Index:   false,
+			Pid:     "",
+			Uid:     0,
+			Roles:   "",
+			Type:    "FOLDER",
+			Target:  "TAB",
+			Title:   "快捷链接",
+			Brief:   "快捷链接",
+			NavLink: "",
+			Deleted: false,
+			SortNum: 0,
+		},
+		{
+			Id:      "MODIFY_PASSWORD",
+			Path:    "/modifyPassword",
+			Index:   false,
+			Pid:     "QUICK_LINKS",
+			Uid:     0,
+			Roles:   "",
+			Type:    "LINK",
+			Target:  "TAB",
+			Title:   "修改密码",
+			Brief:   "修改您的密码",
+			NavLink: "",
+			Deleted: false,
+			SortNum: 0,
+		},
 	}
 
 	for _, route := range routes {

+ 14 - 2
server/modules/settings/route_controller.go

@@ -1,8 +1,10 @@
 package settings
 
 import (
+	"errors"
 	"nginx-ui/server/base"
 	"nginx-ui/server/models"
+	"nginx-ui/server/utils"
 )
 
 type RouteController struct {
@@ -43,6 +45,13 @@ func (c *RouteController) Save() {
 	}
 	if route.Uid == -1 {
 		route.Uid = current.Id
+		if route.Id == "" {
+			route.Id = utils.NextIdStr()
+		}
+	}
+	if route.Id == "" {
+		c.ErrorJson(errors.New("ID不能为空"))
+		return
 	}
 	resp, err := Route.Save(&route)
 	if err != nil {
@@ -57,8 +66,11 @@ func (c *RouteController) Delete() {
 	if current == nil {
 		return
 	}
-	id := c.GetQuery("id")
-	err := Route.Delete(id)
+	route := models.SettingRoute{}
+	if !c.ReadBody(&route) {
+		return
+	}
+	err := Route.Delete(route.Id)
 	if err != nil {
 		c.ErrorJson(err)
 		return

+ 1 - 1
server/modules/settings/route_service.go

@@ -29,7 +29,7 @@ func (c *RouteService) GetList(vo RouteListVo) ([]models.SettingRoute, error) {
 	if vo.NonType != "" {
 		qs = qs.FilterRaw("Type", fmt.Sprintf("<>'%s'", vo.NonType))
 	}
-	qs = qs.OrderBy("-SortNum")
+	qs = qs.OrderBy("Uid", "-SortNum")
 
 	var list []models.SettingRoute
 	_, err := qs.All(&list)

+ 79 - 0
server/utils/snow.go

@@ -0,0 +1,79 @@
+package utils
+
+import (
+	"fmt"
+	"strconv"
+	"sync"
+	"time"
+)
+
+type Snowflake struct {
+	epoch       int64
+	timestamp   int64
+	dataCenter  int64
+	workerId    int64
+	sequence    int64
+	mutex       sync.Mutex
+	maxSequence int64
+}
+
+const (
+	epochShift      = 22
+	dataCenterShift = 12
+	workerIdShift   = 10
+	sequenceShift   = 0
+	sequenceMask    = -1 ^ (-1 << sequenceShift)
+	maxWorkerId     = -1 ^ (-1 << workerIdShift)
+	maxDataCenter   = -1 ^ (-1 << dataCenterShift)
+)
+
+func NewSnowflake(epoch int64, dataCenter, workerId int64) (*Snowflake, error) {
+	if dataCenter > maxDataCenter || workerId > maxWorkerId {
+		return nil, fmt.Errorf("dataCenter or workerId exceeds limit")
+	}
+	return &Snowflake{
+		epoch:       epoch,
+		dataCenter:  dataCenter,
+		workerId:    workerId,
+		maxSequence: -1 ^ (-1 << sequenceShift),
+	}, nil
+}
+
+func (s *Snowflake) NextID() int64 {
+	s.mutex.Lock()
+	defer s.mutex.Unlock()
+
+	currentTimestamp := time.Now().UnixNano() / 1e6
+	if s.timestamp == currentTimestamp {
+		s.sequence = (s.sequence + 1) & sequenceMask
+		if s.sequence == 0 {
+			currentTimestamp = s.waitNextTime(currentTimestamp)
+		}
+	} else {
+		s.sequence = 0
+	}
+	s.timestamp = currentTimestamp
+
+	id := ((currentTimestamp - s.epoch) << epochShift) |
+		(s.dataCenter << dataCenterShift) |
+		(s.workerId << workerIdShift) |
+		s.sequence
+	return id
+}
+
+func (s *Snowflake) waitNextTime(currentTimestamp int64) int64 {
+	for currentTimestamp <= s.timestamp {
+		currentTimestamp = time.Now().UnixNano() / 1e6
+	}
+	return currentTimestamp
+}
+
+var snowflake, _ = NewSnowflake(time.Now().UnixNano()/1e6, 1, 1)
+
+func NextId() int64 {
+	return snowflake.NextID()
+}
+
+func NextIdStr() string {
+	return strconv.FormatInt(NextId(), 10)
+}

Деякі файли не було показано, через те що забагато файлів було змінено