feat: add repository management dialogs and store functionality

- Implement CreateRepoDialog for creating new repositories with form validation.
- Implement EditRepoDialog for editing existing repository details.
- Implement SyncRepoDialog for syncing repositories with Gitea, including repository creation if necessary.
- Implement WorkspaceDetailDialog for managing workspace links and actions.
- Enhance the repo store with new state management for repository actions, including creating, editing, and syncing repositories.
- Add build configuration utilities for repository synchronization.
- Create a new page for repository management, integrating all dialogs and functionalities.
- Add login route for authentication.
This commit is contained in:
2026-02-25 01:02:55 +08:00
parent f4643464ba
commit 7ec6428643
32 changed files with 3303 additions and 71 deletions

View File

@@ -1,7 +1,7 @@
{ {
"name": "@kevisual/cnb-center", "name": "@kevisual/cnb-center",
"private": true, "private": true,
"version": "0.0.4", "version": "0.0.5",
"type": "module", "type": "module",
"basename": "/root/cnb-center", "basename": "/root/cnb-center",
"scripts": { "scripts": {
@@ -9,7 +9,7 @@
"build": "vite build", "build": "vite build",
"preview": "vite preview", "preview": "vite preview",
"ui": "pnpm dlx shadcn@latest add ", "ui": "pnpm dlx shadcn@latest add ",
"pub": "envision deploy ./dist -k cnb-center -v 0.0.4 -y y -u" "pub": "envision deploy ./dist -k cnb-center -v 0.0.5 -y y -u"
}, },
"files": [ "files": [
"dist" "dist"
@@ -21,9 +21,11 @@
"@ai-sdk/openai": "^3.0.30", "@ai-sdk/openai": "^3.0.30",
"@ai-sdk/openai-compatible": "^2.0.30", "@ai-sdk/openai-compatible": "^2.0.30",
"@base-ui/react": "^1.2.0", "@base-ui/react": "^1.2.0",
"@kevisual/api": "^0.0.60",
"@kevisual/cnb": "^0.0.26", "@kevisual/cnb": "^0.0.26",
"@kevisual/cnb-ai": "^0.0.2", "@kevisual/cnb-ai": "^0.0.2",
"@kevisual/context": "^0.0.8", "@kevisual/context": "^0.0.8",
"@kevisual/kv-login": "^0.1.15",
"@kevisual/router": "0.0.80", "@kevisual/router": "0.0.80",
"@tanstack/react-router": "^1.161.1", "@tanstack/react-router": "^1.161.1",
"ai": "^6.0.91", "ai": "^6.0.91",

2790
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,8 @@
import { QueryRouterServer } from '@kevisual/router/browser' import { QueryRouterServer } from '@kevisual/router/browser'
import { useContextKey } from '@kevisual/context' import { useContextKey } from '@kevisual/context'
import { useConfigStore } from '@/app/config/store' import { useConfigStore } from '@/pages/config/store'
import { useGiteaConfigStore } from '@/app/config/gitea/store' import { useGiteaConfigStore } from '@/pages/config/gitea/store'
import { CNB } from '@kevisual/cnb' import { CNB } from '@kevisual/cnb'
import { Gitea } from '@kevisual/gitea'; import { Gitea } from '@kevisual/gitea';
export const app = useContextKey('app', new QueryRouterServer()) export const app = useContextKey('app', new QueryRouterServer())

View File

@@ -1,3 +1,14 @@
import { QueryClient } from '@kevisual/query'; import { Query, DataOpts } from '@kevisual/query';
import { QueryLoginBrowser } from '@kevisual/api/query-login'
import { useContextKey } from '@kevisual/context';
export const query = useContextKey('query', new Query({
url: '/api/router',
}));
export const query = new QueryClient(); export const queryClient = useContextKey('queryClient', new Query({
url: '/client/router',
}));
export const queryLogin = useContextKey('queryLogin', new QueryLoginBrowser({
query: query
}));

58
src/pages/auth/index.tsx Normal file
View File

@@ -0,0 +1,58 @@
import { useEffect } from "react"
import { useLayoutStore } from "./store"
import { useShallow } from "zustand/shallow"
import { LogIn, LockKeyhole } from "lucide-react"
export { BaseHeader } from './modules/BaseHeader'
import { useMemo } from 'react';
import { useLocation, useNavigate } from '@tanstack/react-router';
type Props = {
children?: React.ReactNode,
mustLogin?: boolean,
}
export const AuthProvider = ({ children, mustLogin }: Props) => {
const store = useLayoutStore(useShallow(state => ({
init: state.init,
me: state.me,
openLinkList: state.openLinkList,
})));
useEffect(() => {
store.init()
}, [])
const location = useLocation()
const navigate = useNavigate();
const isOpen = useMemo(() => {
return store.openLinkList.some(item => location.pathname.startsWith(item))
}, [location.pathname])
const loginUrl = '/root/login/?redirect=' + encodeURIComponent(window.location.href);
if (mustLogin && !store.me && !isOpen) {
return (
<div className="w-full h-full min-h-screen flex items-center justify-center bg-background">
<div className="flex flex-col items-center gap-6 p-10 rounded-2xl border border-border bg-card shadow-lg max-w-sm w-full mx-4">
<div className="flex items-center justify-center w-16 h-16 rounded-full bg-muted">
<LockKeyhole className="w-8 h-8 text-muted-foreground" />
</div>
<div className="flex flex-col items-center gap-2 text-center">
<h2 className="text-xl font-semibold text-foreground"></h2>
<p className="text-sm text-muted-foreground">访</p>
</div>
<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, '_blank');
navigate({ to: '/login' });
}}
>
<LogIn className="w-4 h-4" />
</div>
</div>
</div>
)
}
return <>
{children}
</>
}

View File

@@ -0,0 +1,80 @@
import { Home, User, LogIn, LogOut } from 'lucide-react';
import { Link, useNavigate } from '@tanstack/react-router'
import { useLayoutStore } from '../store';
import { useShallow } from 'zustand/shallow';
import { useMemo } from 'react';
export const BaseHeader = (props: { main?: React.ComponentType | null }) => {
const store = useLayoutStore(useShallow(state => ({
me: state.me,
clearMe: state.clearMe,
links: state.links,
})));
const navigate = useNavigate();
const meInfo = useMemo(() => {
if (!store.me) {
return (
<button
onClick={() => navigate({ to: '/login' })}
className="flex items-center gap-2 px-3 py-1.5 text-sm text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer"
>
<LogIn className="w-4 h-4" />
<span></span>
</button>
)
}
return (
<div className="flex items-center gap-3">
{store.me.avatar && (
<img
src={store.me.avatar}
alt="Avatar"
className="w-8 h-8 rounded-full object-cover"
/>
)}
{!store.me.avatar && (
<div className="w-8 h-8 rounded-full bg-gray-200 flex items-center justify-center">
<User className="w-4 h-4 text-gray-500" />
</div>
)}
<span className="font-medium text-gray-700">{store.me?.username}</span>
<button
onClick={() => store.clearMe?.()}
className="flex items-center gap-1 px-2 py-1 text-sm text-gray-500 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors cursor-pointer"
title="退出登录"
>
<LogOut className="w-4 h-4" />
</button>
</div>
)
}, [store.me, store.clearMe])
return (
<>
<div className="flex gap-2 text-lg w-full h-12 items-center justify-between bg-gray-200">
<div className='px-2'>
{
store.links.map(link => (
<Link
key={link.key || link.title}
to={link.href}
className="px-3 py-1.5 text-sm text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors"
>
{link.key === 'home' && <Home className="w-4 h-4 mr-1" />}
{link.icon && <span className="mr-1">{link.icon}</span>}
{link.title}
</Link>
))
}
</div>
<div className='mr-4'>
{meInfo}
</div>
</div>
<hr />
</>
)
}
export const LayoutMain = () => {
return <BaseHeader />
}

81
src/pages/auth/page.tsx Normal file
View File

@@ -0,0 +1,81 @@
import { useContextKey } from '@kevisual/context';
import '@kevisual/kv-login';
import { checkPluginLogin } from '@kevisual/kv-login'
import { useEffect } from 'react';
import { useLayoutStore } from './store';
import { useShallow } from 'zustand/shallow';
import { useNavigate } from '@tanstack/react-router';
export const LoginComponent = ({ onLoginSuccess }: { onLoginSuccess: () => void }) => {
useEffect(() => {
// 监听登录成功事件
const handleLoginSuccess = () => {
console.log('监听到登录成功事件,关闭弹窗');
onLoginSuccess();
};
const loginEmitter = useContextKey('login-emitter')
console.log('KvLogin Types:', loginEmitter);
loginEmitter.on('login-success', handleLoginSuccess);
// 清理监听器
return () => {
loginEmitter.off('login-success', handleLoginSuccess);
};
}, [onLoginSuccess]);
// @ts-ignore
return (<kv-login></kv-login>)
}
export const App = () => {
const store = useLayoutStore(useShallow((state) => ({
init: state.init,
loginPageConfig: state.loginPageConfig,
})));
useEffect(() => {
checkPluginLogin();
}, []);
const navigate = useNavigate();
const handleLoginSuccess = async () => {
await store.init()
navigate({ to: '/' })
};
const { title, subtitle, footer } = store.loginPageConfig;
return (
<div className='w-full h-full relative overflow-hidden bg-gradient-to-br from-slate-900 via-purple-900 to-slate-900'>
{/* 背景装饰 - 圆形光晕 */}
<div className='absolute top-1/4 -left-32 w-96 h-96 bg-purple-500/30 rounded-full blur-3xl'></div>
<div className='absolute bottom-1/4 -right-32 w-96 h-96 bg-blue-500/30 rounded-full blur-3xl'></div>
<div className='absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[600px] bg-indigo-500/20 rounded-full blur-3xl'></div>
{/* 背景装饰 - 网格图案 */}
<div className='absolute inset-0 opacity-[0.03] bg-[linear-gradient(rgba(255,255,255,0.1)_1px,transparent_1px),linear-gradient(90deg,rgba(255,255,255,0.1)_1px,transparent_1px)] bg-[size:50px_50px]'></div>
{/* 顶部装饰文字 */}
<div className='absolute top-10 left-0 right-0 text-center'>
<h1 className='text-4xl font-bold text-white/90 tracking-wider'>{title}</h1>
<p className='mt-2 text-white/50 text-sm tracking-widest'>{subtitle}</p>
</div>
{/* 登录卡片容器 */}
<div className='w-full h-full flex items-center justify-center p-8'>
<div className='relative'>
{/* 卡片外圈光效 */}
<div className='absolute -inset-1 bg-gradient-to-r from-purple-500 via-blue-500 to-indigo-500 rounded-2xl blur opacity-30'></div>
{/* 登录组件容器 */}
<div className='relative bg-slate-900/80 backdrop-blur-xl rounded-2xl border border-white/10 shadow-2xl overflow-hidden'>
<LoginComponent onLoginSuccess={handleLoginSuccess} />
</div>
</div>
</div>
{/* 底部装饰 */}
<div className='absolute bottom-6 left-0 right-0 text-center'>
<p className='text-white/30 text-xs'>{footer}</p>
</div>
</div>
)
}
export default App;

103
src/pages/auth/store.ts Normal file
View File

@@ -0,0 +1,103 @@
import { queryLogin } from '@/modules/query';
import { create } from 'zustand';
import { toast } from 'sonner';
type UserInfo = {
id?: string;
username?: string;
nickname?: string | null;
needChangePassword?: boolean;
description?: string | null;
type?: 'user' | 'org';
orgs?: string[];
avatar?: string;
};
export type LayoutStore = {
open: boolean;
setOpen: (open: boolean) => void;
openUser: boolean;
setOpenUser: (openUser: boolean) => void;
me?: UserInfo;
setMe: (me: UserInfo) => void;
clearMe: () => void;
getMe: () => Promise<void>;
switchOrg: (username?: string) => Promise<void>;
isAdmin: boolean;
setIsAdmin: (isAdmin: boolean) => void
init: () => Promise<void>;
openLinkList: string[];
setOpenLinkList: (openLinkList: string[]) => void;
loginPageConfig: {
title: string;
subtitle: string;
footer: string;
};
setLoginPageConfig: (config: Partial<LayoutStore['loginPageConfig']>) => void;
links: HeaderLink[];
};
type HeaderLink = {
title: string;
href: string;
description?: string;
icon?: React.ReactNode;
key?: string;
};
export const useLayoutStore = create<LayoutStore>((set, get) => ({
open: false,
setOpen: (open) => set({ open }),
openUser: false,
setOpenUser: (openUser) => set({ openUser }),
me: undefined,
setMe: (me) => set({ me }),
clearMe: () => {
set({ me: undefined, isAdmin: false });
window.location.href = '/root/login/?redirect=' + encodeURIComponent(window.location.href);
},
getMe: async () => {
const res = await queryLogin.getMe();
if (res.code === 200) {
set({ me: res.data });
set({ isAdmin: res.data.orgs?.includes?.('admin') || false });
}
},
switchOrg: async (username?: string) => {
const res = await queryLogin.switchUser(username || '');
if (res.code === 200) {
toast.success('切换成功');
setTimeout(() => {
window.location.reload();
}, 1000);
} else {
toast.error(res.message || '请求失败');
}
},
isAdmin: false,
setIsAdmin: (isAdmin) => set({ isAdmin }),
init: async () => {
const token = await queryLogin.getToken();
if (token) {
set({ me: {} })
const me = await queryLogin.getMe();
// const user = await queryLogin.checkLocalUser() as UserInfo;
const user = me.code === 200 ? me.data : undefined;
if (user) {
set({ me: user });
set({ isAdmin: user.orgs?.includes?.('admin') || false });
} else {
set({ me: undefined, isAdmin: false });
}
}
},
openLinkList: ['/login'],
setOpenLinkList: (openLinkList) => set({ openLinkList }),
loginPageConfig: {
title: '可视化管理平台',
subtitle: '让工具和智能化触手可及',
footer: '欢迎使用可视化管理平台 · 连接您的工具',
},
setLoginPageConfig: (config) => set((state) => ({
loginPageConfig: { ...state.loginPageConfig, ...config },
})),
links: [{ title: '首页', href: '/', key: 'home' }],
}));

View File

@@ -0,0 +1,16 @@
import { useEffect } from "react";
import { useConfigStore } from "../store";
import { useNavigate } from "@tanstack/react-router";
export const useCheckConfig = () => {
const navigate = useNavigate();
useEffect(() => {
const config = useConfigStore.getState().config;
if (!config.CNB_API_KEY) {
navigate({
to: '/config'
})
}
}, [])
}

View File

@@ -23,6 +23,7 @@ import {
} from 'lucide-react' } from 'lucide-react'
import { useState } from 'react' import { useState } from 'react'
import { toast } from 'sonner' import { toast } from 'sonner'
import { useShallow } from 'zustand/shallow'
type LinkItemKey = keyof WorkspaceOpen; type LinkItemKey = keyof WorkspaceOpen;
interface LinkItem { interface LinkItem {
@@ -94,8 +95,76 @@ const LinkItem = ({ label, icon, url }: { label: string; icon: React.ReactNode;
) )
} }
// Dev tab 内容
const DevTabContent = ({ linkItems, workspaceLink, stopWorkspace }: {
linkItems: LinkItem[]
workspaceLink: Partial<WorkspaceOpen>
stopWorkspace: () => void
}) => {
return (
<>
<button
onClick={() => stopWorkspace()}
className="w-full flex items-center justify-center gap-2 px-4 py-2.5 rounded-lg bg-red-500 hover:bg-red-600 text-white font-medium transition-colors"
>
<Square className="w-4 h-4" />
</button>
<div className="grid grid-cols-2 gap-3 mt-2">
{linkItems.map((item) => (
<LinkItem
key={item.key}
label={item.label}
icon={item.icon}
url={item.getUrl(workspaceLink)}
/>
))}
</div>
</>
)
}
// Work tab 内容(暂留,需要根据 business_id 做事情)
const WorkTabContent = () => {
const store = useRepoStore(useShallow((state) => ({ selectWorkspace: state.selectWorkspace })))
const businessId = store.selectWorkspace?.business_id;
const appList = [
{
title: 'Kevisual Assistant Client', key: 'Assistant Client', port: 51515, end: '/root/cli-center/'
},
{
title: 'OpenCode', key: 'OpenCode', port: 100, end: ''
},
{
title: 'OpenClaw', key: 'OpenClaw', port: 80, end: '/openclaw'
},
{
title: 'OpenWebUI', key: 'OpenWebUI', port: 200, end: ''
},
]
const links = appList.map(app => {
const url = `https://${businessId}-${app.port}.cnb.run${app.end}`
return {
label: app.title,
icon: <Terminal className="w-5 h-5" />,
url
}
})
return (
<div className="flex flex-col items-center justify-center py-2 text-neutral-400">
<div className='mb-2'></div>
<div className="grid grid-cols-1 gap-3 w-full max-w-sm">
{links.map(link => (
<LinkItem key={link.label} label={link.label} icon={link.icon} url={link.url} />
))}
</div>
</div>
)
}
export function WorkspaceDetailDialog() { export function WorkspaceDetailDialog() {
const { showWorkspaceDialog, setShowWorkspaceDialog, workspaceLink, stopWorkspace } = useRepoStore() const { showWorkspaceDialog, setShowWorkspaceDialog, workspaceLink, stopWorkspace, workspaceTab, setWorkspaceTab } = useRepoStore()
const linkItems: LinkItem[] = [ const linkItems: LinkItem[] = [
{ {
@@ -170,22 +239,40 @@ export function WorkspaceDetailDialog() {
<DialogTitle className="text-neutral-900"></DialogTitle> <DialogTitle className="text-neutral-900"></DialogTitle>
<DialogDescription className="text-neutral-500"></DialogDescription> <DialogDescription className="text-neutral-500"></DialogDescription>
</DialogHeader> </DialogHeader>
<button {/* Tab 导航 */}
onClick={() => stopWorkspace()} <div className="flex border-b border-neutral-200">
className="w-full flex items-center justify-center gap-2 px-4 py-2.5 rounded-lg bg-red-500 hover:bg-red-600 text-white font-medium transition-colors" <button
> onClick={() => setWorkspaceTab('dev')}
<Square className="w-4 h-4" /> className={`cursor-pointer flex-1 px-4 py-3 text-sm font-medium transition-colors relative ${workspaceTab === 'dev'
? 'text-neutral-900'
</button> : 'text-neutral-500 hover:text-neutral-700'
<div className="grid grid-cols-2 gap-3"> }`}
{linkItems.map((item) => ( >
<LinkItem Dev
key={item.key} {workspaceTab === 'dev' && (
label={item.label} <div className="absolute bottom-0 left-0 right-0 h-0.5 bg-neutral-900" />
icon={item.icon} )}
url={item.getUrl(workspaceLink)} </button>
/> <button
))} onClick={() => setWorkspaceTab('work')}
className={`cursor-pointer flex-1 px-4 py-3 text-sm font-medium transition-colors relative ${workspaceTab === 'work'
? 'text-neutral-900'
: 'text-neutral-500 hover:text-neutral-700'
}`}
>
Work
{workspaceTab === 'work' && (
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-neutral-900" />
)}
</button>
</div>
{/* Tab 内容 */}
<div className="py-2">
{workspaceTab === 'dev' ? (
<DevTabContent linkItems={linkItems} workspaceLink={workspaceLink} stopWorkspace={stopWorkspace} />
) : (
<WorkTabContent />
)}
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>

View File

@@ -49,6 +49,8 @@ interface Data {
pinned_time: string; pinned_time: string;
} }
type WorkspaceTabType = 'dev' | 'work'
type State = { type State = {
formData: Record<string, any>; formData: Record<string, any>;
setFormData: (data: Record<string, any>) => void; setFormData: (data: Record<string, any>) => void;
@@ -56,6 +58,8 @@ type State = {
setShowEdit: (showEdit: boolean) => void; setShowEdit: (showEdit: boolean) => void;
loading: boolean; loading: boolean;
setLoading: (loading: boolean) => void; setLoading: (loading: boolean) => void;
workspaceTab: WorkspaceTabType;
setWorkspaceTab: (tab: WorkspaceTabType) => void;
list: Data[]; list: Data[];
editRepo: Data | null; editRepo: Data | null;
setEditRepo: (repo: Data | null) => void; setEditRepo: (repo: Data | null) => void;
@@ -102,6 +106,8 @@ export const useRepoStore = create<State>((set, get) => {
setShowCreateDialog: (show) => set({ showCreateDialog: show }), setShowCreateDialog: (show) => set({ showCreateDialog: show }),
showWorkspaceDialog: false, showWorkspaceDialog: false,
setShowWorkspaceDialog: (show) => set({ showWorkspaceDialog: show }), setShowWorkspaceDialog: (show) => set({ showWorkspaceDialog: show }),
workspaceTab: 'dev',
setWorkspaceTab: (tab) => set({ workspaceTab: tab }),
syncDialogOpen: false, syncDialogOpen: false,
setSyncDialogOpen: (open) => set({ syncDialogOpen: open }), setSyncDialogOpen: (open) => set({ syncDialogOpen: open }),
selectedSyncRepo: null, selectedSyncRepo: null,

View File

@@ -9,10 +9,16 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. // 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 rootRouteImport } from './routes/__root'
import { Route as LoginRouteImport } from './routes/login'
import { Route as IndexRouteImport } from './routes/index' import { Route as IndexRouteImport } from './routes/index'
import { Route as ConfigIndexRouteImport } from './routes/config/index' import { Route as ConfigIndexRouteImport } from './routes/config/index'
import { Route as ConfigGiteaRouteImport } from './routes/config/gitea' import { Route as ConfigGiteaRouteImport } from './routes/config/gitea'
const LoginRoute = LoginRouteImport.update({
id: '/login',
path: '/login',
getParentRoute: () => rootRouteImport,
} as any)
const IndexRoute = IndexRouteImport.update({ const IndexRoute = IndexRouteImport.update({
id: '/', id: '/',
path: '/', path: '/',
@@ -31,36 +37,47 @@ const ConfigGiteaRoute = ConfigGiteaRouteImport.update({
export interface FileRoutesByFullPath { export interface FileRoutesByFullPath {
'/': typeof IndexRoute '/': typeof IndexRoute
'/login': typeof LoginRoute
'/config/gitea': typeof ConfigGiteaRoute '/config/gitea': typeof ConfigGiteaRoute
'/config/': typeof ConfigIndexRoute '/config/': typeof ConfigIndexRoute
} }
export interface FileRoutesByTo { export interface FileRoutesByTo {
'/': typeof IndexRoute '/': typeof IndexRoute
'/login': typeof LoginRoute
'/config/gitea': typeof ConfigGiteaRoute '/config/gitea': typeof ConfigGiteaRoute
'/config': typeof ConfigIndexRoute '/config': typeof ConfigIndexRoute
} }
export interface FileRoutesById { export interface FileRoutesById {
__root__: typeof rootRouteImport __root__: typeof rootRouteImport
'/': typeof IndexRoute '/': typeof IndexRoute
'/login': typeof LoginRoute
'/config/gitea': typeof ConfigGiteaRoute '/config/gitea': typeof ConfigGiteaRoute
'/config/': typeof ConfigIndexRoute '/config/': typeof ConfigIndexRoute
} }
export interface FileRouteTypes { export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' | '/config/gitea' | '/config/' fullPaths: '/' | '/login' | '/config/gitea' | '/config/'
fileRoutesByTo: FileRoutesByTo fileRoutesByTo: FileRoutesByTo
to: '/' | '/config/gitea' | '/config' to: '/' | '/login' | '/config/gitea' | '/config'
id: '__root__' | '/' | '/config/gitea' | '/config/' id: '__root__' | '/' | '/login' | '/config/gitea' | '/config/'
fileRoutesById: FileRoutesById fileRoutesById: FileRoutesById
} }
export interface RootRouteChildren { export interface RootRouteChildren {
IndexRoute: typeof IndexRoute IndexRoute: typeof IndexRoute
LoginRoute: typeof LoginRoute
ConfigGiteaRoute: typeof ConfigGiteaRoute ConfigGiteaRoute: typeof ConfigGiteaRoute
ConfigIndexRoute: typeof ConfigIndexRoute ConfigIndexRoute: typeof ConfigIndexRoute
} }
declare module '@tanstack/react-router' { declare module '@tanstack/react-router' {
interface FileRoutesByPath { interface FileRoutesByPath {
'/login': {
id: '/login'
path: '/login'
fullPath: '/login'
preLoaderRoute: typeof LoginRouteImport
parentRoute: typeof rootRouteImport
}
'/': { '/': {
id: '/' id: '/'
path: '/' path: '/'
@@ -87,6 +104,7 @@ declare module '@tanstack/react-router' {
const rootRouteChildren: RootRouteChildren = { const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute, IndexRoute: IndexRoute,
LoginRoute: LoginRoute,
ConfigGiteaRoute: ConfigGiteaRoute, ConfigGiteaRoute: ConfigGiteaRoute,
ConfigIndexRoute: ConfigIndexRoute, ConfigIndexRoute: ConfigIndexRoute,
} }

View File

@@ -1,56 +1,27 @@
import { Link, Outlet, createRootRoute, useNavigate } from '@tanstack/react-router' import { LayoutMain } from '@/pages/auth/modules/BaseHeader';
import { Outlet, createRootRoute } from '@tanstack/react-router'
import { TanStackRouterDevtools } from '@tanstack/react-router-devtools' import { TanStackRouterDevtools } from '@tanstack/react-router-devtools'
import { Toaster } from '@/components/ui/sonner' import { Toaster } from '@/components/ui/sonner'
import { useConfigStore } from '@/app/config/store' import { AuthProvider } from '@/pages/auth'
import { useEffect } from 'react' import { TooltipProvider } from '@/components/ui/tooltip'
import { Settings } from 'lucide-react'
export const Route = createRootRoute({ export const Route = createRootRoute({
component: RootComponent, component: RootComponent,
}) })
function RootComponent() { function RootComponent() {
const navigate = useNavigate()
useEffect(() => {
const config = useConfigStore.getState().config;
if (!config.CNB_API_KEY) {
navigate({
to: '/config'
})
}
}, [])
return ( return (
<div className='h-full overflow-hidden'> <div className='h-full overflow-hidden'>
<LayoutMain />
<div className="p-2 flex gap-2 text-lg items-center"> <AuthProvider mustLogin={false}>
<Link <TooltipProvider>
to="/" <main className='h-[calc(100%-3rem)] overflow-auto scrollbar'>
activeProps={{ <Outlet />
className: 'text-gray-800', </main>
}} </TooltipProvider>
inactiveProps={{ </AuthProvider>
className: 'text-gray-500',
}}
>
</Link>
<Link to='/config' activeProps={{
className: 'text-gray-800',
}}
inactiveProps={{
className: 'text-gray-500',
}}
>
<Settings className="h-5 w-5" />
</Link>
</div>
<hr />
<main className='h-[calc(100%-4rem)] overflow-auto scrollbar'>
<Outlet />
</main>
<TanStackRouterDevtools position="bottom-right" /> <TanStackRouterDevtools position="bottom-right" />
<Toaster /> <Toaster />
</div> </div>
) )
} }

View File

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

View File

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

View File

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

9
src/routes/login.tsx Normal file
View File

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