feat: implement logout on 401 response and update query handling

refactor: replace Button with div for consistent styling in AIEditorLink

refactor: update navigation handling in AppVersionList and remove LayoutMain wrapper

refactor: remove unused LayoutMain imports and components across various pages

fix: ensure user app list is set correctly in useUserAppStore

fix: update login URL format in AuthProvider

fix: adjust layout styles in EnvPage and other pages for better responsiveness

chore: update route definitions and create new routes for apps, config, domain, flowme, org, remote, token, user, and users

style: replace Button with div for delete confirmation in various components

fix: ensure correct handling of user profile image source
This commit is contained in:
2026-02-22 03:24:14 +08:00
parent f3c269dd83
commit 66ee0d7f60
44 changed files with 740 additions and 761 deletions

View File

@@ -1,11 +1,10 @@
'use strict';
import { useShallow } from 'zustand/react/shallow';
import { useLayoutStore } from './store';
import clsx from 'clsx';
import { toast as message } from 'sonner';
import { useMemo } from 'react';
import { queryLogin } from '../query';
import { LogOut, Map, SquareUser, Users, X, ArrowDownLeftFromSquareIcon } from 'lucide-react';
import { LogOut, Users, X, ArrowDownLeftFromSquareIcon } from 'lucide-react';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
@@ -14,11 +13,12 @@ import {
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip';
Drawer,
DrawerContent,
DrawerHeader,
DrawerTitle,
DrawerDescription,
} from '@/components/ui/drawer';
import { openLink } from '../basename';
export const LayoutUser = () => {
@@ -87,92 +87,82 @@ export const LayoutUser = () => {
}, [store.me]);
return (
<TooltipProvider>
<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={() => {
setOpen(false);
}}></div>
<div className='w-[400px] bg-white transition-all duration-300 h-full absolute top-0 right-0 rounded-l-lg'>
<div className='flex justify-between p-6 mt-4 font-bold items-center border-b'>
<div className='flex items-center gap-2'>
: <span className='text-primary'>{store.me?.username}</span>
</div>
<div className='flex gap-4'>
<Drawer open={open} onOpenChange={setOpen} direction="right">
<DrawerContent className="w-100">
<DrawerHeader className="border-b">
<div className="flex items-center justify-between">
<DrawerTitle className="flex items-center gap-2">
: <span className="text-primary">{store.me?.username}</span>
</DrawerTitle>
<div className="flex gap-2">
<Button
variant="ghost"
size="icon"
title="退出登录"
onClick={async () => {
const res = await queryLogin.logout();
if (res.code === 200) {
const url = new URL(location.origin);
url.pathname = '/root/login/';
openLink(url.toString(), '_self');
} else {
message.error(res.message || '退出失败');
}
}}>
<LogOut size={18} />
</Button>
{items.length > 0 && (
<Tooltip>
<TooltipTrigger>
<DropdownMenu>
<DropdownMenuTrigger>
<Button variant='ghost' size='icon'>
<Users />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
{items.map((item, index) => (
<DropdownMenuItem
key={index}
onClick={() => {
store.switchOrg(item.key, 'org');
}}>
<div className='mr-2'>{item.icon}</div>
<div>{item.label}</div>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</TooltipTrigger>
<TooltipContent>
<p></p>
</TooltipContent>
</Tooltip>
<DropdownMenu>
<DropdownMenuTrigger
title="切换组织"
className="inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground h-9 w-9">
<Users size={18} />
</DropdownMenuTrigger>
<DropdownMenuContent>
{items.map((item, index) => (
<DropdownMenuItem
key={index}
onClick={() => {
store.switchOrg(item.key);
}}>
<div className="mr-2">{item.icon}</div>
<div>{item.label}</div>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
)}
<Button variant='ghost' size='icon' onClick={() => setOpen(false)}>
<X />
<Button variant="ghost" size="icon" onClick={() => setOpen(false)}>
<X size={18} />
</Button>
</div>
</div>
<div className='mt-3 font-medium'>
{menu.map((item, index) => {
return (
<div
key={index}
className='flex items-center p-4 hover:bg-secondary hover:text-white cursor-pointer'
onClick={() => {
if (item.link) {
openLink(item.link, '_self');
setOpen(false);
} else {
message.info('即将上线');
}
}}>
<div className='mr-4'>{item.icon}</div>
<div>{item.title}</div>
</div>
);
})}
</div>
<div
className='flex items-center p-4 hover:bg-secondary hover:text-white cursor-pointer'
onClick={async () => {
const res = await queryLogin.logout();
if (res.success) {
const url = new URL(location.origin);
url.pathname = '/root/login';
openLink(url.toString(), '_self');
} else {
message.error(res.message || '退出失败');
}
}}>
<div className='mr-4'>
<LogOut size={16} />
</div>
<div>退</div>
<DrawerDescription className="sr-only">
</DrawerDescription>
</DrawerHeader>
<div className="flex flex-col h-full">
<div className="flex-1">
{menu.map((item, index) => (
<div
key={index}
className="flex items-center px-4 py-3 hover:bg-secondary hover:text-secondary-foreground cursor-pointer transition-colors"
onClick={() => {
if (item.link) {
openLink(item.link, '_self');
setOpen(false);
} else {
message.info('即将上线');
}
}}>
<div className="mr-3">{item.icon}</div>
<div className="font-medium">{item.title}</div>
</div>
))}
</div>
</div>
</div>
</TooltipProvider>
</DrawerContent>
</Drawer>
);
};

View File

@@ -57,10 +57,7 @@ export const LayoutMain = (props: LayoutMainProps) => {
useLayoutEffect(() => {
platformStore.init();
}, []);
useEffect(() => {
menuStore.getMe();
console.log('menuStore', menuStore.me);
}, []);
return (
@@ -76,7 +73,6 @@ export const LayoutMain = (props: LayoutMainProps) => {
<div className='flex items-center gap-2 text-sm '>
{quickMenu.map((item, index) => {
const isActive = location.pathname === item.link;
console.log('isActive', location, item.link, isActive);
return (
<div
key={index}
@@ -114,7 +110,7 @@ export const LayoutMain = (props: LayoutMainProps) => {
<TooltipTrigger>
<IconButton
onClick={() => {
menuStore.switchOrg('', 'user');
menuStore.switchOrg('');
}}>
<SwapOutlined />
</IconButton>

View File

@@ -2,6 +2,7 @@
import { query, queryLogin } from '@/modules/query';
import { create } from 'zustand';
import { toast as message } from 'sonner';
import { useLayoutStore } from '@/pages/auth/store';
export const getIsMac = async () => {
// @ts-ignore
const userAgentData = navigator.userAgentData;
@@ -68,38 +69,4 @@ export type LayoutStore = {
setIsAdmin: (isAdmin: boolean) => void;
checkHasOrg: () => boolean;
};
export const useLayoutStore = create<LayoutStore>((set, get) => ({
open: false,
setOpen: (open) => set({ open }),
me: {},
setMe: (me) => set({ me }),
getMe: async () => {
const res = await queryLogin.getMe();
if (res.code === 200) {
set({ me: res.data });
set({ isAdmin: res.data.orgs?.includes('admin') });
}
},
openUser: false,
setOpenUser: (openUser) => set({ openUser }),
switchOrg: async (username?: string, type?: string) => {
const res = await queryLogin.switchUser(username || '');
if (res.code === 200) {
message.success('Switch success');
setTimeout(() => {
window.location.reload();
}, 1000);
} else {
message.error(res.message || 'Request failed');
}
},
isAdmin: false,
setIsAdmin: (isAdmin) => set({ isAdmin }),
checkHasOrg: () => {
const user = get().me || {};
if (!user.orgs) {
return false;
}
return user?.orgs?.length > 0;
},
}));
export { useLayoutStore }

View File

@@ -3,7 +3,15 @@ import { QueryLoginBrowser } from '@kevisual/api/query-login'
import { useContextKey } from '@kevisual/context';
export const query = useContextKey('query', new Query({
url: '/api/router',
}));
query.afterResponse = async (response, ctx) => {
if (response.code === 401) {
queryLogin.logout();
setTimeout(() => { location.reload() }, 2000);
}
return response;
}
export const queryClient = useContextKey('queryClient', new Query({
url: '/client/router',