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,164 +0,0 @@
@import "tailwindcss";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--radius-2xl: calc(var(--radius) + 8px);
--radius-3xl: calc(var(--radius) + 12px);
--radius-4xl: calc(var(--radius) + 16px);
}
:root {
--radius: 0.625rem;
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}
html,body {
height: 100%;
overflow: hidden;
}
@utility scrollbar {
overflow: auto;
/* 整个滚动条 */
&::-webkit-scrollbar {
width: 3px;
height: 3px;
}
&::-webkit-scrollbar-track {
background-color: var(--color-scrollbar-track);
}
/* 滚动条有滑块的轨道部分 */
&::-webkit-scrollbar-track-piece {
background-color: transparent;
border-radius: 1px;
}
/* 滚动条滑块(竖向:vertical 横向:horizontal) */
&::-webkit-scrollbar-thumb {
cursor: pointer;
background-color: var(--color-scrollbar-thumb);
border-radius: 5px;
}
/* 滚动条滑块hover */
&::-webkit-scrollbar-thumb:hover {
background-color: var(--color-scrollbar-thumb-hover);
}
/* 同时有垂直和水平滚动条时交汇的部分 */
&::-webkit-scrollbar-corner {
display: block; /* 修复交汇时出现的白块 */
}
}

View File

@@ -1,44 +0,0 @@
import type { Metadata, Viewport } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { Toaster } from 'sonner'
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Light Code",
description: "直觉、高效的代码编辑",
};
export const viewport: Viewport = {
themeColor: "#000000",
width: "device-width",
initialScale: 1,
maximumScale: 1,
userScalable: false,
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="zh-CN" className="">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased overflow-x-hidden bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100`}
>
{children}
<Toaster></Toaster>
</body>
</html>
);
}

View File

@@ -1,14 +0,0 @@
'use client';
import { LayoutMain } from "@/modules/layout";
export default function Home() {
return (
<div className="flex min-h-screen items-center justify-center bg-zinc-50 font-sans dark:bg-black">
<LayoutMain>
<iframe src="/root/router-studio" className="w-full border-0" style={{
height: 'calc(100vh - 48px)'
}}/>
</LayoutMain>
</div>
);
}

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',

View File

@@ -20,9 +20,8 @@ export const AIEditorLink = (props: Props) => {
return (
<Tooltip>
<TooltipTrigger>
<Button
variant='ghost'
size='icon'
<div
className='inline-flex items-center justify-center rounded-md p-2 transition-colors hover:bg-slate-100 disabled:opacity-50 disabled:pointer-events-none'
onClick={() => {
if (!layoutUser.user) {
toast.error('请先登录');
@@ -39,7 +38,7 @@ export const AIEditorLink = (props: Props) => {
openLink(openUrl, '_blank');
}}>
<Folder className='h-4 w-4' />
</Button>
</div>
</TooltipTrigger>
<TooltipContent></TooltipContent>
</Tooltip>

View File

@@ -17,8 +17,7 @@ import { Controller, useForm } from 'react-hook-form';
import { pick } from 'es-toolkit';
import { useAppDeleteModalStore, AppDeleteModal } from '../modules/AppDeleteModal';
import { AIEditorLink } from './AIEditorLink';
import { openLink } from '@/modules/basename';
import { LayoutMain } from '@/modules/layout';
import { useNavigate } from '@tanstack/react-router';
const FormModal = () => {
const { control, handleSubmit, reset } = useForm();
@@ -130,6 +129,7 @@ export const AppVersionList = () => {
}),
);
const [isUpload, setIsUpload] = useState(false);
const navigate = useNavigate();
useEffect(() => {
// fetch app version list
if (appKey) {
@@ -170,8 +170,7 @@ export const AppVersionList = () => {
variant='ghost'
size='icon'
onClick={() => {
// navigate('/app/edit/list');
history.back();
navigate({ to: '/apps' });
}}>
<ChevronLeft className='h-4 w-4' />
</Button>
@@ -236,7 +235,7 @@ export const AppVersionList = () => {
if (isRunning) {
const origin = typeof window !== 'undefined' ? window.location.origin : '';
const link = new URL(`/test/${item.id}`, origin);
openLink(link.toString(), '_blank');
window.open(link.toString(), '_blank');
} else {
message.error('The app is not running');
}
@@ -372,7 +371,5 @@ export const AppVersionFile = () => {
};
export default () => {
return <LayoutMain>
<AppVersionList />
</LayoutMain>
return <AppVersionList />
};

View File

@@ -1,7 +0,0 @@
'use client';
import { LayoutMain } from '@/modules/layout';
export const Main = () => {
return <LayoutMain title='User Apps' />;
};

View File

@@ -34,18 +34,17 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { LayoutMain } from '@/modules/layout';
import { openLink } from '@/modules/basename';
import { useNavigate } from '@tanstack/react-router';
export const IconButton = (props: any) => {
return (
<button
<div
className={clsx(
'inline-flex items-center justify-center rounded-md p-2 transition-colors hover:bg-slate-100 disabled:opacity-50 disabled:pointer-events-none',
props.className,
)}
{...props}>
{props.children}
</button>
</div>
);
};
const FormModal = () => {
@@ -276,6 +275,7 @@ export const List = () => {
useEffect(() => {
userAppStore.getList();
}, []);
const navigate = useNavigate();
return (
<div className='w-full h-full flex bg-slate-100'>
<div className='p-2 h-full bg-white flex flex-col gap-2'>
@@ -301,7 +301,7 @@ export const List = () => {
padding: '8px',
}}
onClick={() => {
openLink('/domain/', '_self');
navigate({ to: '/domain' });
}}>
<LinkIcon className='h-4 w-4' />
</IconButton>
@@ -367,68 +367,59 @@ export const List = () => {
<div className='mt-4 pt-3 border-t border-slate-100 flex gap-1 absolute bottom-0 left-0 right-0 px-4 pb-4 bg-white rounded-b-lg'>
<Tooltip>
<TooltipTrigger>
<Button
variant='ghost'
size='icon'
<div
className='inline-flex items-center justify-center rounded-md p-2 transition-colors hover:bg-slate-100 disabled:opacity-50 disabled:pointer-events-none'
onClick={() => {
userAppStore.getUserApp(item.id);
userAppStore.setFormData(item);
userAppStore.setShowEdit(true);
}}>
<Edit className='h-4 w-4' />
</Button>
</div>
</TooltipTrigger>
<TooltipContent></TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger>
<Button
variant='ghost'
size='icon'
<div className='inline-flex items-center justify-center rounded-md p-2 transition-colors hover:bg-slate-100 disabled:opacity-50 disabled:pointer-events-none'
onClick={() => {
const url = `/apps/app?appKey=${item.key}`;
openLink(url, '_self');
navigate({
to: `/apps/app?appKey=${item.key}`,
})
}}
>
<AppWindow className='h-4 w-4' />
</Button>
</div>
</TooltipTrigger>
<TooltipContent></TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger>
<Button
variant='ghost'
size='icon'
<div className='inline-flex items-center justify-center rounded-md p-2 transition-colors hover:bg-slate-100 disabled:opacity-50 disabled:pointer-events-none'
onClick={() => {
userAppStore.getUserApp(item.id);
userAppStore.setFormData(item);
userAppStore.setShowShareEdit(true);
}}>
<Share2 className='h-4 w-4' />
</Button>
</div>
</TooltipTrigger>
<TooltipContent className="whitespace-pre-wrap">{iText.share.tips}</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger>
<Button
variant='ghost'
size='icon'
<div className='inline-flex items-center justify-center rounded-md p-2 transition-colors hover:bg-slate-100 disabled:opacity-50 disabled:pointer-events-none'
onClick={() => {
appVersionStore.publishVersion({ appKey: item.key, version: item.version }, { showToast: true });
}}>
<RefreshCcw className='h-4 w-4' />
</Button>
</div>
</TooltipTrigger>
<TooltipContent></TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger>
<Button
variant='ghost'
size='icon'
<div className='inline-flex items-center justify-center rounded-md p-2 transition-colors hover:bg-slate-100 disabled:opacity-50 disabled:pointer-events-none'
onClick={() => {
if (isRunning) {
let baseUri = typeof window !== 'undefined' ? window.location.origin : '';
@@ -441,35 +432,33 @@ export const List = () => {
baseUri = new URL('https://' + item.domain).toString();
}
if (baseUri.endsWith('/')) {
openLink(baseUri, '_blank');
window.open(baseUri, '_blank');
}
console.log('baseUri', baseUri);
message.success('success');
return;
}
const link = new URL(`/${item.user}/${item.key}/`, baseUri);
openLink(link.toString(), '_blank');
window.open(link.toString(), '_blank');
} else {
message.error('应用未运行');
}
}}>
<ExternalLink className='h-4 w-4' />
</Button>
</div>
</TooltipTrigger>
<TooltipContent></TooltipContent>
</Tooltip>
<AIEditorLink pathname={item.key} />
<Tooltip>
<TooltipTrigger>
<Button
variant='ghost'
size='icon'
<div className='inline-flex items-center justify-center rounded-md p-2 transition-colors hover:bg-slate-100 disabled:opacity-50 disabled:pointer-events-none'
onClick={(e) => {
appDeleteModalStore.onClickDelete('user-app', item);
e.stopPropagation();
}}>
<Trash2 className='h-4 w-4' />
</Button>
</div>
</TooltipTrigger>
<TooltipContent></TooltipContent>
</Tooltip>

View File

@@ -43,7 +43,8 @@ export const useUserAppStore = create<UserAppStore>((set, get) => {
});
set({ loading: false });
if (res.code === 200) {
set({ list: res.data });
const list = res.data.list || [];
set({ list: list });
} else {
message.error(res.message || 'Request failed');
}

View File

@@ -15,7 +15,7 @@ export const AuthProvider = ({ children, mustLogin }: Props) => {
useEffect(() => {
store.init()
}, [])
const loginUrl = '/root/login?redirect=' + encodeURIComponent(window.location.href);
const loginUrl = '/root/login/?redirect=' + encodeURIComponent(window.location.href);
if (mustLogin && !store.me) {
return (
<div className="w-full h-full min-h-screen flex items-center justify-center bg-background">
@@ -27,13 +27,15 @@ export const AuthProvider = ({ children, mustLogin }: Props) => {
<h2 className="text-xl font-semibold text-foreground"></h2>
<p className="text-sm text-muted-foreground">访</p>
</div>
<a
href={loginUrl}
<div
className="inline-flex items-center justify-center gap-2 w-full px-6 py-2.5 rounded-lg bg-foreground text-background text-sm font-medium transition-opacity hover:opacity-80 active:opacity-70"
onClick={() => {
window.open(loginUrl, '_self')
}}
>
<LogIn className="w-4 h-4" />
</a>
</div>
</div>
</div>
)

View File

@@ -54,11 +54,12 @@ export const useLayoutStore = create<LayoutStore>((set, get) => ({
setIsAdmin: (isAdmin) => set({ isAdmin }),
init: async () => {
const token = await queryLogin.getToken()
console.log('token', token);
if (token) {
const user = await queryLogin.checkLocalUser() as UserInfo;
console.log('local user', user);
if (user) {
set({ me: user });
set({ isAdmin: user.orgs?.includes?.('admin') || false });
set({ me: user, isAdmin: user.orgs?.includes?.('admin') || false });
}
}
}

View File

@@ -208,7 +208,7 @@ export default function EnvPage() {
}
return (
<div className="h-screen flex flex-col">
<div className="h-full flex flex-col">
<div className="flex-none border-b bg-background">
<div className="container mx-auto p-6 max-w-5xl">
<div className="flex justify-between items-center">

View File

@@ -26,7 +26,6 @@ import {
PopoverTrigger,
} from '@/components/ui/popover';
import { Plus, Pencil, Trash2, Code } from 'lucide-react';
import { LayoutMain } from '@/modules/layout';
const TableList = () => {
const { list, setShowEdit, setFormData, deleteConfig, setShowDataEdit, setDataFormData } = useConfigStore();
@@ -90,12 +89,11 @@ const TableList = () => {
</Button>
<Popover>
<PopoverTrigger>
<Button
variant="destructive"
size="sm">
<div
className="flex items-center px-4 py-2 bg-destructive text-white rounded cursor-pointer">
<Trash2 className="w-4 h-4 mr-1" />
</Button>
</div>
</PopoverTrigger>
<PopoverContent className="w-48 p-2">
<div className="text-sm text-center mb-2"></div>
@@ -301,6 +299,4 @@ export const List = () => {
);
};
export default () => {
return <LayoutMain><List /></LayoutMain>;
}
export default List

View File

@@ -18,7 +18,6 @@ import {
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog';
import {
Select,
@@ -29,7 +28,6 @@ import {
} from "@/components/ui/select"
import { Plus, Pencil, Trash2 } from 'lucide-react';
import { LayoutUser } from '@/modules/layout/LayoutUser';
import { LayoutMain } from '@/modules/layout';
const TableList = () => {
const { list, setShowEditModal, setFormData, deleteDomain } = useDomainStore();
@@ -181,18 +179,14 @@ export const List = () => {
return (
<div className="p-4 w-full h-full">
<div className="flex mb-4">
<Dialog>
<DialogTrigger>
<Button
onClick={() => {
setShowEditModal(true);
setFormData({});
}}>
<Plus className="w-4 h-4 mr-1" />
</Button>
</DialogTrigger>
</Dialog>
<Button
onClick={() => {
setShowEditModal(true);
setFormData({});
}}>
<Plus className="w-4 h-4 mr-1" />
</Button>
</div>
<TableList />
<FomeModal />
@@ -200,6 +194,4 @@ export const List = () => {
);
};
export default () => {
return <LayoutMain><List /></LayoutMain>;
}
export default List

View File

@@ -33,7 +33,7 @@ interface Store {
export const useDomainStore = create<Store>((set, get) => ({
getDomainList: async () => {
const res = await query.get({
path: 'app.domain.manager',
path: 'app_domain_manager',
key: 'list',
});
if (res.code === 200) {
@@ -43,7 +43,7 @@ export const useDomainStore = create<Store>((set, get) => ({
},
updateDomain: async (data: any, opts?: { refresh?: boolean }) => {
const res = await query.post({
path: 'app.domain.manager',
path: 'app_domain_manager',
key: 'update',
data,
});
@@ -61,7 +61,7 @@ export const useDomainStore = create<Store>((set, get) => ({
},
deleteDomain: async (data: any) => {
const res = await query.post({
path: 'app.domain.manager',
path: 'app_domain_manager',
key: 'delete',
data,
});
@@ -74,7 +74,7 @@ export const useDomainStore = create<Store>((set, get) => ({
},
getDomainDetail: async (data: any) => {
const res = await query.post({
path: 'app.domain.manager',
path: 'app_domain_manager',
key: 'get',
data,
});

View File

@@ -18,10 +18,8 @@ import {
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog';
import { Plus, Pencil, Trash2 } from 'lucide-react';
import { LayoutMain } from '@/modules/layout';
const TableList = () => {
const { list, setShowEdit, setFormData, deleteData } = useFlowmeChannelStore();
@@ -163,18 +161,14 @@ export const List = () => {
return (
<div className="p-4 w-full h-full">
<div className="flex mb-4">
<Dialog>
<DialogTrigger>
<Button
onClick={() => {
setShowEdit(true);
setFormData({});
}}>
<Plus className="w-4 h-4 mr-1" />
</Button>
</DialogTrigger>
</Dialog>
<Button
onClick={() => {
setShowEdit(true);
setFormData({});
}}>
<Plus className="w-4 h-4 mr-1" />
</Button>
</div>
<TableList />
<FormModal />
@@ -182,6 +176,4 @@ export const List = () => {
);
};
export default () => {
return <LayoutMain><List /></LayoutMain>;
}
export default List

View File

@@ -18,10 +18,8 @@ import {
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog';
import { Plus, Pencil, Trash2 } from 'lucide-react';
import { LayoutMain } from '@/modules/layout';
const TableList = () => {
const { list, setShowEdit, setFormData, deleteData } = useFlowmeStore();
@@ -172,18 +170,14 @@ export const List = () => {
return (
<div className="p-4 w-full h-full">
<div className="flex mb-4">
<Dialog>
<DialogTrigger>
<Button
onClick={() => {
setShowEdit(true);
setFormData({});
}}>
<Plus className="w-4 h-4 mr-1" />
</Button>
</DialogTrigger>
</Dialog>
<Button
onClick={() => {
setShowEdit(true);
setFormData({});
}}>
<Plus className="w-4 h-4 mr-1" />
</Button>
</div>
<TableList />
<FormModal />
@@ -191,6 +185,4 @@ export const List = () => {
);
};
export default () => {
return <LayoutMain><List /></LayoutMain>;
}
export default List

View File

@@ -24,7 +24,6 @@ import {
PopoverTrigger,
} from '@/components/ui/popover';
import { Plus, Pencil, Trash2, Users } from 'lucide-react';
import { LayoutMain } from '@/modules/layout';
import { UserDrawer } from './components/UserDrawer';
const TableList = () => {
@@ -75,12 +74,12 @@ const TableList = () => {
</Button>
<Popover>
<PopoverTrigger>
<Button
variant="destructive"
size="sm">
<div
className="flex items-center px-4 py-2 bg-destructive text-white rounded cursor-pointer"
>
<Trash2 className="w-4 h-4 mr-1" />
</Button>
</div>
</PopoverTrigger>
<PopoverContent className="w-48 p-2">
<div className="text-sm text-center mb-2"></div>
@@ -204,6 +203,4 @@ export const List = () => {
);
};
export default () => {
return <LayoutMain><List /></LayoutMain>;
}
export default List

View File

@@ -1,6 +1,6 @@
export default function Home() {
return (
<div className="flex min-h-screen items-center justify-center bg-zinc-50 font-sans dark:bg-black">
<div className="flex h-full items-center justify-center bg-zinc-50 font-sans dark:bg-black">
<iframe src="/root/router-studio" className="w-full border-0" style={{
height: 'calc(100vh - 48px)'
}} />

View File

@@ -1,5 +1,4 @@
'use client';
import { LayoutMain } from "@/modules/layout";
import { RemoteApp } from "@kevisual/remote-app";
import { useEffect } from "react";
import { QueryRouterServer } from "@kevisual/router/browser";
@@ -54,10 +53,8 @@ export default function Home() {
}
}
return (
<div className="flex min-h-screen items-center justify-center bg-zinc-50 font-sans dark:bg-black">
<LayoutMain>
<div id="remote"></div>
</LayoutMain>
<div className="flex h-full items-center justify-center bg-zinc-50 font-sans dark:bg-black">
<div id="remote"></div>
</div>
);
}

View File

@@ -6,7 +6,6 @@ import { Controller, useForm } from 'react-hook-form';
import { Plus, Pencil, Trash2, Calendar as CalendarIcon, Eye, Copy } from 'lucide-react';
import { toast } from 'sonner';
import { useConfigStore, type Item } from './store';
import { LayoutMain } from '@/modules/layout';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
@@ -264,6 +263,4 @@ export const List = () => {
);
};
export default () => {
return <LayoutMain><List /></LayoutMain>;
};
export default List

View File

@@ -20,7 +20,6 @@ import {
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { LayoutMain } from '@/modules/layout';
import { Pencil, Key, User } from 'lucide-react';
import PandaPNG from '@/assets/panda.jpg';
@@ -53,7 +52,7 @@ const ProfileCard = () => {
/>
) : (
<img
src={PandaPNG.src}
src={PandaPNG}
alt="avatar"
className="w-full h-full object-cover"
/>
@@ -287,6 +286,4 @@ export const UserProfile = () => {
);
};
export default () => {
return <LayoutMain title="个人信息"><UserProfile /></LayoutMain>;
}
export default UserProfile

View File

@@ -25,7 +25,6 @@ import {
PopoverTrigger,
} from '@/components/ui/popover';
import { Plus, Pencil, Trash2 } from 'lucide-react';
import { LayoutMain } from '@/modules/layout';
const TableList = () => {
const { list, setShowEdit, setFormData, getList } = useUserStore();
@@ -71,13 +70,12 @@ const TableList = () => {
onOpenChange={(open) => setOpenPopover(open ? user.id : null)}
>
<PopoverTrigger>
<Button
variant="destructive"
size="sm"
<div
className="flex items-center px-4 py-2 bg-destructive text-white rounded cursor-pointer"
>
<Trash2 className="w-4 h-4 mr-1" />
</Button>
</div>
</PopoverTrigger>
<PopoverContent className="w-auto p-4" align="start">
<div className="space-y-3">
@@ -221,4 +219,4 @@ export const List = () => {
);
};
export default <List />
export default List

View File

@@ -9,12 +9,48 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
import { Route as AppsRouteImport } from './routes/apps'
import { Route as UsersRouteImport } from './routes/users'
import { Route as UserRouteImport } from './routes/user'
import { Route as TokenRouteImport } from './routes/token'
import { Route as RemoteRouteImport } from './routes/remote'
import { Route as OrgRouteImport } from './routes/org'
import { Route as DomainRouteImport } from './routes/domain'
import { Route as IndexRouteImport } from './routes/index'
import { Route as FlowmeIndexRouteImport } from './routes/flowme/index'
import { Route as ConfigIndexRouteImport } from './routes/config/index'
import { Route as AppsIndexRouteImport } from './routes/apps/index'
import { Route as FlowmeChannelRouteImport } from './routes/flowme/channel'
import { Route as ConfigEnvRouteImport } from './routes/config/env'
import { Route as AppsAppRouteImport } from './routes/apps/app'
const AppsRoute = AppsRouteImport.update({
id: '/apps',
path: '/apps',
const UsersRoute = UsersRouteImport.update({
id: '/users',
path: '/users',
getParentRoute: () => rootRouteImport,
} as any)
const UserRoute = UserRouteImport.update({
id: '/user',
path: '/user',
getParentRoute: () => rootRouteImport,
} as any)
const TokenRoute = TokenRouteImport.update({
id: '/token',
path: '/token',
getParentRoute: () => rootRouteImport,
} as any)
const RemoteRoute = RemoteRouteImport.update({
id: '/remote',
path: '/remote',
getParentRoute: () => rootRouteImport,
} as any)
const OrgRoute = OrgRouteImport.update({
id: '/org',
path: '/org',
getParentRoute: () => rootRouteImport,
} as any)
const DomainRoute = DomainRouteImport.update({
id: '/domain',
path: '/domain',
getParentRoute: () => rootRouteImport,
} as any)
const IndexRoute = IndexRouteImport.update({
@@ -22,40 +58,189 @@ const IndexRoute = IndexRouteImport.update({
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
const FlowmeIndexRoute = FlowmeIndexRouteImport.update({
id: '/flowme/',
path: '/flowme/',
getParentRoute: () => rootRouteImport,
} as any)
const ConfigIndexRoute = ConfigIndexRouteImport.update({
id: '/config/',
path: '/config/',
getParentRoute: () => rootRouteImport,
} as any)
const AppsIndexRoute = AppsIndexRouteImport.update({
id: '/apps/',
path: '/apps/',
getParentRoute: () => rootRouteImport,
} as any)
const FlowmeChannelRoute = FlowmeChannelRouteImport.update({
id: '/flowme/channel',
path: '/flowme/channel',
getParentRoute: () => rootRouteImport,
} as any)
const ConfigEnvRoute = ConfigEnvRouteImport.update({
id: '/config/env',
path: '/config/env',
getParentRoute: () => rootRouteImport,
} as any)
const AppsAppRoute = AppsAppRouteImport.update({
id: '/apps/app',
path: '/apps/app',
getParentRoute: () => rootRouteImport,
} as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/apps': typeof AppsRoute
'/domain': typeof DomainRoute
'/org': typeof OrgRoute
'/remote': typeof RemoteRoute
'/token': typeof TokenRoute
'/user': typeof UserRoute
'/users': typeof UsersRoute
'/apps/app': typeof AppsAppRoute
'/config/env': typeof ConfigEnvRoute
'/flowme/channel': typeof FlowmeChannelRoute
'/apps/': typeof AppsIndexRoute
'/config/': typeof ConfigIndexRoute
'/flowme/': typeof FlowmeIndexRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/apps': typeof AppsRoute
'/domain': typeof DomainRoute
'/org': typeof OrgRoute
'/remote': typeof RemoteRoute
'/token': typeof TokenRoute
'/user': typeof UserRoute
'/users': typeof UsersRoute
'/apps/app': typeof AppsAppRoute
'/config/env': typeof ConfigEnvRoute
'/flowme/channel': typeof FlowmeChannelRoute
'/apps': typeof AppsIndexRoute
'/config': typeof ConfigIndexRoute
'/flowme': typeof FlowmeIndexRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
'/apps': typeof AppsRoute
'/domain': typeof DomainRoute
'/org': typeof OrgRoute
'/remote': typeof RemoteRoute
'/token': typeof TokenRoute
'/user': typeof UserRoute
'/users': typeof UsersRoute
'/apps/app': typeof AppsAppRoute
'/config/env': typeof ConfigEnvRoute
'/flowme/channel': typeof FlowmeChannelRoute
'/apps/': typeof AppsIndexRoute
'/config/': typeof ConfigIndexRoute
'/flowme/': typeof FlowmeIndexRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' | '/apps'
fullPaths:
| '/'
| '/domain'
| '/org'
| '/remote'
| '/token'
| '/user'
| '/users'
| '/apps/app'
| '/config/env'
| '/flowme/channel'
| '/apps/'
| '/config/'
| '/flowme/'
fileRoutesByTo: FileRoutesByTo
to: '/' | '/apps'
id: '__root__' | '/' | '/apps'
to:
| '/'
| '/domain'
| '/org'
| '/remote'
| '/token'
| '/user'
| '/users'
| '/apps/app'
| '/config/env'
| '/flowme/channel'
| '/apps'
| '/config'
| '/flowme'
id:
| '__root__'
| '/'
| '/domain'
| '/org'
| '/remote'
| '/token'
| '/user'
| '/users'
| '/apps/app'
| '/config/env'
| '/flowme/channel'
| '/apps/'
| '/config/'
| '/flowme/'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
AppsRoute: typeof AppsRoute
DomainRoute: typeof DomainRoute
OrgRoute: typeof OrgRoute
RemoteRoute: typeof RemoteRoute
TokenRoute: typeof TokenRoute
UserRoute: typeof UserRoute
UsersRoute: typeof UsersRoute
AppsAppRoute: typeof AppsAppRoute
ConfigEnvRoute: typeof ConfigEnvRoute
FlowmeChannelRoute: typeof FlowmeChannelRoute
AppsIndexRoute: typeof AppsIndexRoute
ConfigIndexRoute: typeof ConfigIndexRoute
FlowmeIndexRoute: typeof FlowmeIndexRoute
}
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/apps': {
id: '/apps'
path: '/apps'
fullPath: '/apps'
preLoaderRoute: typeof AppsRouteImport
'/users': {
id: '/users'
path: '/users'
fullPath: '/users'
preLoaderRoute: typeof UsersRouteImport
parentRoute: typeof rootRouteImport
}
'/user': {
id: '/user'
path: '/user'
fullPath: '/user'
preLoaderRoute: typeof UserRouteImport
parentRoute: typeof rootRouteImport
}
'/token': {
id: '/token'
path: '/token'
fullPath: '/token'
preLoaderRoute: typeof TokenRouteImport
parentRoute: typeof rootRouteImport
}
'/remote': {
id: '/remote'
path: '/remote'
fullPath: '/remote'
preLoaderRoute: typeof RemoteRouteImport
parentRoute: typeof rootRouteImport
}
'/org': {
id: '/org'
path: '/org'
fullPath: '/org'
preLoaderRoute: typeof OrgRouteImport
parentRoute: typeof rootRouteImport
}
'/domain': {
id: '/domain'
path: '/domain'
fullPath: '/domain'
preLoaderRoute: typeof DomainRouteImport
parentRoute: typeof rootRouteImport
}
'/': {
@@ -65,12 +250,65 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
'/flowme/': {
id: '/flowme/'
path: '/flowme'
fullPath: '/flowme/'
preLoaderRoute: typeof FlowmeIndexRouteImport
parentRoute: typeof rootRouteImport
}
'/config/': {
id: '/config/'
path: '/config'
fullPath: '/config/'
preLoaderRoute: typeof ConfigIndexRouteImport
parentRoute: typeof rootRouteImport
}
'/apps/': {
id: '/apps/'
path: '/apps'
fullPath: '/apps/'
preLoaderRoute: typeof AppsIndexRouteImport
parentRoute: typeof rootRouteImport
}
'/flowme/channel': {
id: '/flowme/channel'
path: '/flowme/channel'
fullPath: '/flowme/channel'
preLoaderRoute: typeof FlowmeChannelRouteImport
parentRoute: typeof rootRouteImport
}
'/config/env': {
id: '/config/env'
path: '/config/env'
fullPath: '/config/env'
preLoaderRoute: typeof ConfigEnvRouteImport
parentRoute: typeof rootRouteImport
}
'/apps/app': {
id: '/apps/app'
path: '/apps/app'
fullPath: '/apps/app'
preLoaderRoute: typeof AppsAppRouteImport
parentRoute: typeof rootRouteImport
}
}
}
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
AppsRoute: AppsRoute,
DomainRoute: DomainRoute,
OrgRoute: OrgRoute,
RemoteRoute: RemoteRoute,
TokenRoute: TokenRoute,
UserRoute: UserRoute,
UsersRoute: UsersRoute,
AppsAppRoute: AppsAppRoute,
ConfigEnvRoute: ConfigEnvRoute,
FlowmeChannelRoute: FlowmeChannelRoute,
AppsIndexRoute: AppsIndexRoute,
ConfigIndexRoute: ConfigIndexRoute,
FlowmeIndexRoute: FlowmeIndexRoute,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)

View File

@@ -24,8 +24,8 @@ function RootComponent() {
</Link>
</div>
<hr /> */}
<LayoutMain />
<AuthProvider>
<AuthProvider mustLogin={true}>
<LayoutMain />
<TooltipProvider>
<main className='h-[calc(100%-4rem)] overflow-auto scrollbar'>
<Outlet />

10
src/routes/apps/app.tsx Normal file
View File

@@ -0,0 +1,10 @@
import { createFileRoute } from '@tanstack/react-router'
import AppDetail from '@/pages/apps/app/page'
export const Route = createFileRoute('/apps/app')({
component: RouteComponent,
})
function RouteComponent() {
return <AppDetail />
}

View File

@@ -1,6 +1,6 @@
import { createFileRoute } from '@tanstack/react-router'
import App from '@/pages/apps/page'
export const Route = createFileRoute('/apps')({
export const Route = createFileRoute('/apps/')({
component: RouteComponent,
})

10
src/routes/config/env.tsx Normal file
View File

@@ -0,0 +1,10 @@
import { createFileRoute } from '@tanstack/react-router'
import ConfigEnv from '@/pages/config/env/page'
export const Route = createFileRoute('/config/env')({
component: RouteComponent,
})
function RouteComponent() {
return <ConfigEnv />
}

View File

@@ -0,0 +1,10 @@
import { createFileRoute } from '@tanstack/react-router'
import Config from '@/pages/config/page'
export const Route = createFileRoute('/config/')({
component: RouteComponent,
})
function RouteComponent() {
return <Config />
}

10
src/routes/domain.tsx Normal file
View File

@@ -0,0 +1,10 @@
import { createFileRoute } from '@tanstack/react-router'
import Domain from '@/pages/domain/page'
export const Route = createFileRoute('/domain')({
component: RouteComponent,
})
function RouteComponent() {
return <Domain />
}

View File

@@ -0,0 +1,10 @@
import { createFileRoute } from '@tanstack/react-router'
import FlowmeChannel from '@/pages/flowme/channel/page'
export const Route = createFileRoute('/flowme/channel')({
component: RouteComponent,
})
function RouteComponent() {
return <FlowmeChannel />
}

View File

@@ -0,0 +1,10 @@
import { createFileRoute } from '@tanstack/react-router'
import Flowme from '@/pages/flowme/page'
export const Route = createFileRoute('/flowme/')({
component: RouteComponent,
})
function RouteComponent() {
return <Flowme />
}

10
src/routes/org.tsx Normal file
View File

@@ -0,0 +1,10 @@
import { createFileRoute } from '@tanstack/react-router'
import Org from '@/pages/org/page'
export const Route = createFileRoute('/org')({
component: RouteComponent,
})
function RouteComponent() {
return <Org />
}

10
src/routes/remote.tsx Normal file
View File

@@ -0,0 +1,10 @@
import { createFileRoute } from '@tanstack/react-router'
import Remote from '@/pages/remote/page'
export const Route = createFileRoute('/remote')({
component: RouteComponent,
})
function RouteComponent() {
return <Remote />
}

10
src/routes/token.tsx Normal file
View File

@@ -0,0 +1,10 @@
import { createFileRoute } from '@tanstack/react-router'
import Token from '@/pages/token/page'
export const Route = createFileRoute('/token')({
component: RouteComponent,
})
function RouteComponent() {
return <Token />
}

10
src/routes/user.tsx Normal file
View File

@@ -0,0 +1,10 @@
import { createFileRoute } from '@tanstack/react-router'
import User from '@/pages/user/page'
export const Route = createFileRoute('/user')({
component: RouteComponent,
})
function RouteComponent() {
return <User />
}

10
src/routes/users.tsx Normal file
View File

@@ -0,0 +1,10 @@
import { createFileRoute } from '@tanstack/react-router'
import Users from '@/pages/users/page'
export const Route = createFileRoute('/users')({
component: RouteComponent,
})
function RouteComponent() {
return <Users />
}