generated from kevisual/vite-react-template
feat: add CNB Board live information page with domain management
- Created a new page for managing domains with a table view and modal for editing. - Implemented Zustand store for domain state management including fetching, updating, and deleting domains. - Added components for displaying information cards with search functionality. - Integrated API calls for fetching CNB Board live data including build, repo, pull, NPC, and comment information. - Established routing for the CNB Board page.
This commit is contained in:
19
package.json
19
package.json
@@ -1,15 +1,15 @@
|
|||||||
{
|
{
|
||||||
"name": "vite-react",
|
"name": "cli-center",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"basename": "/",
|
"basename": "/root/cli-center",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"ui": "bunx shadcn@latest add ",
|
"ui": "bunx shadcn@latest add ",
|
||||||
"pub": "envision deploy ./dist -k vite-react -v 0.0.1 -y y -u"
|
"pub": "envision deploy ./dist -k cli-center -v 0.0.1 -y y -u"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
@@ -20,8 +20,8 @@
|
|||||||
"@base-ui/react": "^1.2.0",
|
"@base-ui/react": "^1.2.0",
|
||||||
"@kevisual/api": "^0.0.59",
|
"@kevisual/api": "^0.0.59",
|
||||||
"@kevisual/context": "^0.0.8",
|
"@kevisual/context": "^0.0.8",
|
||||||
"@kevisual/router": "0.0.83",
|
"@kevisual/router": "0.0.84",
|
||||||
"@tanstack/react-router": "^1.161.4",
|
"@tanstack/react-router": "^1.162.6",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
@@ -41,18 +41,19 @@
|
|||||||
"access": "public"
|
"access": "public"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@kevisual/cache": "^0.0.5",
|
||||||
"@kevisual/query": "0.0.49",
|
"@kevisual/query": "0.0.49",
|
||||||
"@kevisual/types": "^0.0.12",
|
"@kevisual/types": "^0.0.12",
|
||||||
"@tailwindcss/vite": "^4.2.0",
|
"@tailwindcss/vite": "^4.2.1",
|
||||||
"@tanstack/react-router-devtools": "^1.161.4",
|
"@tanstack/react-router-devtools": "^1.162.6",
|
||||||
"@tanstack/router-plugin": "^1.161.4",
|
"@tanstack/router-plugin": "^1.162.6",
|
||||||
"@types/node": "^25.3.0",
|
"@types/node": "^25.3.0",
|
||||||
"@types/react": "^19.2.14",
|
"@types/react": "^19.2.14",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@vitejs/plugin-react": "^5.1.4",
|
"@vitejs/plugin-react": "^5.1.4",
|
||||||
"dotenv": "^17.3.1",
|
"dotenv": "^17.3.1",
|
||||||
"tailwind-merge": "^3.5.0",
|
"tailwind-merge": "^3.5.0",
|
||||||
"tailwindcss": "^4.2.0",
|
"tailwindcss": "^4.2.1",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"vite": "v8.0.0-beta.14"
|
"vite": "v8.0.0-beta.14"
|
||||||
|
|||||||
2828
pnpm-lock.yaml
generated
Normal file
2828
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
9
skills/page/SKILL.md
Normal file
9
skills/page/SKILL.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
name: new-page
|
||||||
|
description: 创建一个新页面
|
||||||
|
---
|
||||||
|
|
||||||
|
## 参考当前的文档
|
||||||
|
|
||||||
|
|
||||||
|
`./references/*.ts`
|
||||||
202
skills/page/references/page.tsx
Normal file
202
skills/page/references/page.tsx
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
'use client';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { appDomainStatus, useDomainStore } from './store/index ';
|
||||||
|
import { useForm, Controller } from 'react-hook-form';
|
||||||
|
import { pick } from 'es-toolkit';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from '@/components/ui/table';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select"
|
||||||
|
import { Plus, Pencil, Trash2 } from 'lucide-react';
|
||||||
|
import { LayoutUser } from '@/modules/layout/LayoutUser';
|
||||||
|
|
||||||
|
const TableList = () => {
|
||||||
|
const { list, setShowEditModal, setFormData, deleteDomain } = useDomainStore();
|
||||||
|
useEffect(() => {
|
||||||
|
// Initial load is handled by the parent component
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded-md border">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>ID</TableHead>
|
||||||
|
<TableHead>域名</TableHead>
|
||||||
|
<TableHead>应用ID</TableHead>
|
||||||
|
<TableHead>UID</TableHead>
|
||||||
|
<TableHead>状态</TableHead>
|
||||||
|
<TableHead>操作</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{list.map((domain) => (
|
||||||
|
<TableRow key={domain.id}>
|
||||||
|
<TableCell>{domain.id}</TableCell>
|
||||||
|
<TableCell>{domain.domain}</TableCell>
|
||||||
|
<TableCell>{domain.appId}</TableCell>
|
||||||
|
<TableCell>{domain.uid}</TableCell>
|
||||||
|
<TableCell>{domain.status}</TableCell>
|
||||||
|
<TableCell className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
setShowEditModal(true);
|
||||||
|
setFormData(domain);
|
||||||
|
}}>
|
||||||
|
<Pencil className="w-4 h-4 mr-1" />
|
||||||
|
编辑
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => deleteDomain(domain)}>
|
||||||
|
<Trash2 className="w-4 h-4 mr-1" />
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const FomeModal = () => {
|
||||||
|
const { showEditModal, setShowEditModal, formData, updateDomain } = useDomainStore();
|
||||||
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
formState: { errors },
|
||||||
|
reset,
|
||||||
|
control,
|
||||||
|
setValue,
|
||||||
|
} = useForm();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!showEditModal) return;
|
||||||
|
if (formData?.id) {
|
||||||
|
reset(formData);
|
||||||
|
} else {
|
||||||
|
reset({
|
||||||
|
status: 'running',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [formData, showEditModal, reset]);
|
||||||
|
|
||||||
|
const onSubmit = async (data: any) => {
|
||||||
|
const _formData = pick(data, ['domain', 'appId', 'status', 'id']);
|
||||||
|
if (formData.id) {
|
||||||
|
_formData.id = formData.id;
|
||||||
|
}
|
||||||
|
const res = await updateDomain(_formData);
|
||||||
|
if (res.code === 200) {
|
||||||
|
setShowEditModal(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={showEditModal} onOpenChange={setShowEditModal}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>添加域名</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="p-4">
|
||||||
|
<form className="w-full flex flex-col gap-4" onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<label className="text-sm font-medium">域名</label>
|
||||||
|
<Input
|
||||||
|
{...control.register('domain', { required: '请输入域名' })}
|
||||||
|
placeholder="请输入域名"
|
||||||
|
className={errors.domain ? "border-red-500" : ""}
|
||||||
|
/>
|
||||||
|
{errors.domain && <span className="text-xs text-red-500">{errors.domain.message as string}</span>}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<label className="text-sm font-medium">应用ID</label>
|
||||||
|
<Input
|
||||||
|
{...control.register('appId', { required: '请输入应用ID' })}
|
||||||
|
placeholder="请输入应用ID"
|
||||||
|
className={errors.appId ? "border-red-500" : ""}
|
||||||
|
/>
|
||||||
|
{errors.appId && <span className="text-xs text-red-500">{errors.appId.message as string}</span>}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<label className="text-sm font-medium">状态</label>
|
||||||
|
<Controller
|
||||||
|
name="status"
|
||||||
|
control={control}
|
||||||
|
defaultValue=""
|
||||||
|
render={({ field }) => (
|
||||||
|
<Select value={field.value || ''} onValueChange={field.onChange}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="请选择状态" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{appDomainStatus.map((item) => (
|
||||||
|
<SelectItem key={item} value={item}>{item}</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button type="submit">提交</Button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const List = () => {
|
||||||
|
const { getDomainList, setShowEditModal, setFormData } = useDomainStore();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getDomainList();
|
||||||
|
}, [getDomainList]);
|
||||||
|
|
||||||
|
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>
|
||||||
|
</div>
|
||||||
|
<TableList />
|
||||||
|
<FomeModal />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default List
|
||||||
92
skills/page/references/store/index .ts
Normal file
92
skills/page/references/store/index .ts
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
'use strict';
|
||||||
|
import { create } from 'zustand';
|
||||||
|
import { query } from '@/modules/query';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
// 审核,通过,驳回
|
||||||
|
export const appDomainStatus = ['audit', 'auditReject', 'auditPending', 'running', 'stop'] as const;
|
||||||
|
|
||||||
|
type AppDomainStatus = (typeof appDomainStatus)[number];
|
||||||
|
type Domain = {
|
||||||
|
id: string;
|
||||||
|
domain: string;
|
||||||
|
appId?: string;
|
||||||
|
status: AppDomainStatus;
|
||||||
|
data?: any;
|
||||||
|
uid?: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
};
|
||||||
|
interface Store {
|
||||||
|
getDomainList: () => Promise<any>;
|
||||||
|
updateDomain: (data: { domain: string; id: string; [key: string]: any }, opts?: { refresh?: boolean }) => Promise<any>;
|
||||||
|
deleteDomain: (data: { id: string }) => Promise<any>;
|
||||||
|
getDomainDetail: (data: { domain?: string; id?: string }) => Promise<any>;
|
||||||
|
list: Domain[];
|
||||||
|
setList: (list: Domain[]) => void;
|
||||||
|
formData: any;
|
||||||
|
setFormData: (formData: any) => void;
|
||||||
|
showEditModal: boolean;
|
||||||
|
setShowEditModal: (showEditModal: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useDomainStore = create<Store>((set, get) => ({
|
||||||
|
getDomainList: async () => {
|
||||||
|
const res = await query.get({
|
||||||
|
path: 'app.domain.manager',
|
||||||
|
key: 'list',
|
||||||
|
});
|
||||||
|
if (res.code === 200) {
|
||||||
|
set({ list: res.data?.list || [] });
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
updateDomain: async (data: any, opts?: { refresh?: boolean }) => {
|
||||||
|
const res = await query.post({
|
||||||
|
path: 'app.domain.manager',
|
||||||
|
key: 'update',
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
if (res.code === 200) {
|
||||||
|
const list = get().list;
|
||||||
|
set({ list: list.map((item) => (item.id === data.id ? res.data : item)) });
|
||||||
|
toast.success('更新成功');
|
||||||
|
if (opts?.refresh ?? true) {
|
||||||
|
get().getDomainList();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toast.error(res.message || '更新失败');
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
deleteDomain: async (data: any) => {
|
||||||
|
const res = await query.post({
|
||||||
|
path: 'app.domain.manager',
|
||||||
|
key: 'delete',
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
if (res.code === 200) {
|
||||||
|
const list = get().list;
|
||||||
|
set({ list: list.filter((item) => item.id !== data.id) });
|
||||||
|
toast.success('删除成功');
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
getDomainDetail: async (data: any) => {
|
||||||
|
const res = await query.post({
|
||||||
|
path: 'app.domain.manager',
|
||||||
|
key: 'get',
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
if (res.code === 200) {
|
||||||
|
set({ formData: res.data });
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
list: [],
|
||||||
|
setList: (list: any[]) => set({ list }),
|
||||||
|
formData: {},
|
||||||
|
setFormData: (formData: any) => set({ formData }),
|
||||||
|
showEditModal: false,
|
||||||
|
setShowEditModal: (showEditModal: boolean) => set({ showEditModal }),
|
||||||
|
}));
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
import { Query } from '@kevisual/query';
|
import { Query, DataOpts } from '@kevisual/query';
|
||||||
import { QueryLoginBrowser } from '@kevisual/api/query-login'
|
import { QueryLoginBrowser } from '@kevisual/api/login'
|
||||||
export const query = new Query({
|
import { useContextKey } from '@kevisual/context';
|
||||||
|
export const query = useContextKey('query', new Query({
|
||||||
url: '/api/router',
|
url: '/api/router',
|
||||||
});
|
}));
|
||||||
|
|
||||||
export const queryClient = new Query({
|
export const queryClient = useContextKey('queryClient', new Query({
|
||||||
url: '/client/router',
|
url: '/client/router',
|
||||||
});
|
}));
|
||||||
|
|
||||||
export const queryLogin = new QueryLoginBrowser({
|
export const queryLogin = useContextKey('queryLogin', new QueryLoginBrowser({
|
||||||
query: query
|
query: query
|
||||||
});
|
}));
|
||||||
@@ -1,7 +1,46 @@
|
|||||||
|
import { useEffect } from "react"
|
||||||
|
import { useLayoutStore } from "./store"
|
||||||
|
import { useShallow } from "zustand/shallow"
|
||||||
|
import { LogIn, LockKeyhole } from "lucide-react"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children: React.ReactNode
|
children?: React.ReactNode,
|
||||||
|
mustLogin?: boolean,
|
||||||
}
|
}
|
||||||
export const AuthProvider = ({ children }: Props) => {
|
export const AuthProvider = ({ children, mustLogin }: Props) => {
|
||||||
|
const store = useLayoutStore(useShallow(state => ({
|
||||||
|
init: state.init,
|
||||||
|
me: state.me,
|
||||||
|
})));
|
||||||
|
useEffect(() => {
|
||||||
|
store.init()
|
||||||
|
}, [])
|
||||||
|
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">
|
||||||
|
<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, '_self')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LogIn className="w-4 h-4" />
|
||||||
|
立即登录
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
{children}
|
{children}
|
||||||
</>
|
</>
|
||||||
|
|||||||
65
src/pages/auth/store.ts
Normal file
65
src/pages/auth/store.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
|
||||||
|
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;
|
||||||
|
getMe: () => Promise<void>;
|
||||||
|
switchOrg: (username?: string) => Promise<void>;
|
||||||
|
isAdmin: boolean;
|
||||||
|
setIsAdmin: (isAdmin: boolean) => void
|
||||||
|
init: () => Promise<void>;
|
||||||
|
};
|
||||||
|
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 }),
|
||||||
|
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.checkTokenValid()
|
||||||
|
if (token) {
|
||||||
|
const user = await queryLogin.checkLocalUser() as UserInfo;
|
||||||
|
if (user) {
|
||||||
|
set({ me: user });
|
||||||
|
set({ isAdmin: user.orgs?.includes?.('admin') || false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
148
src/pages/cnb-board/components/InfoCard.tsx
Normal file
148
src/pages/cnb-board/components/InfoCard.tsx
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
import { useState, useMemo } from 'react';
|
||||||
|
import Fuse from 'fuse.js';
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from '@/components/ui/card';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Kbd } from '@/components/ui/kbd';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
|
||||||
|
interface InfoItem {
|
||||||
|
title: string;
|
||||||
|
value: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface InfoCardProps {
|
||||||
|
title: string;
|
||||||
|
list: InfoItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_VALUE_LENGTH = 200;
|
||||||
|
|
||||||
|
function isUrl(value: string): boolean {
|
||||||
|
try {
|
||||||
|
const url = new URL(value);
|
||||||
|
return url.protocol === 'http:' || url.protocol === 'https:';
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function InfoCard({ title, list }: InfoCardProps) {
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
|
||||||
|
const fuse = useMemo(
|
||||||
|
() =>
|
||||||
|
new Fuse(list, {
|
||||||
|
keys: ['title', 'value', 'description'],
|
||||||
|
threshold: 0.3,
|
||||||
|
}),
|
||||||
|
[list]
|
||||||
|
);
|
||||||
|
|
||||||
|
const filteredList = useMemo(() => {
|
||||||
|
if (!search.trim()) return list;
|
||||||
|
return fuse.search(search).map((result) => result.item);
|
||||||
|
}, [search, fuse, list]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<CardTitle>{title}</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
{search ? `匹配 ${filteredList.length} / ${list.length} 个变量` : `共 ${list.length} 个变量`}
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
placeholder="搜索..."
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
|
className="w-48 h-8"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="grid gap-2">
|
||||||
|
{filteredList.length === 0 ? (
|
||||||
|
<div className="text-center text-muted-foreground py-4">
|
||||||
|
未找到匹配项
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
filteredList.map((item) => (
|
||||||
|
<div
|
||||||
|
key={item.title}
|
||||||
|
className="grid grid-cols-[240px_1fr] gap-4 py-2 border-b last:border-0"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<Badge variant="outline" className="w-fit font-mono text-xs">
|
||||||
|
{item.title}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1 min-w-0">
|
||||||
|
{item.value ? (
|
||||||
|
isUrl(item.value) ? (
|
||||||
|
<a
|
||||||
|
href={item.value}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="w-fit font-mono text-xs text-blue-500 hover:underline break-all"
|
||||||
|
>
|
||||||
|
{item.value}
|
||||||
|
</a>
|
||||||
|
) : item.value.length > MAX_VALUE_LENGTH ? (
|
||||||
|
<Dialog>
|
||||||
|
<DialogTrigger>
|
||||||
|
<Kbd className="w-full h-full flex flex-col items-start font-mono text-xs break-all cursor-pointer hover:text-primary">
|
||||||
|
<pre className='text-left'>
|
||||||
|
{item.value.slice(0, MAX_VALUE_LENGTH)}...
|
||||||
|
</pre>
|
||||||
|
<div className="text-primary ml-1">(点击展开)</div>
|
||||||
|
</Kbd>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="w-200! max-w-[90vw]! overflow-hidden">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{item.title}</DialogTitle>
|
||||||
|
<DialogDescription>{item.description}</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="text-sm whitespace-pre-wrap break-all font-mono bg-muted p-2 rounded scrollbar">
|
||||||
|
{item.value}
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
) : (
|
||||||
|
<Kbd className="w-fit font-mono text-xs break-all">
|
||||||
|
{item.value}
|
||||||
|
</Kbd>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<Kbd className="w-fit font-mono text-xs text-muted-foreground">
|
||||||
|
(空)
|
||||||
|
</Kbd>
|
||||||
|
)}
|
||||||
|
{item.description && (
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{item.description}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
118
src/pages/cnb-board/modules/api.ts
Normal file
118
src/pages/cnb-board/modules/api.ts
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
|
||||||
|
import { queryClient as query } from '@/modules/query';
|
||||||
|
import { createQueryApi } from '@kevisual/query/api';
|
||||||
|
const api = {
|
||||||
|
"cnb_board": {
|
||||||
|
/**
|
||||||
|
* 检查是否是 cnb_board 环境
|
||||||
|
*/
|
||||||
|
"is_cnb_board": {
|
||||||
|
"path": "cnb_board",
|
||||||
|
"key": "is_cnb_board",
|
||||||
|
"description": "检查是否是 cnb_board 环境",
|
||||||
|
"metadata": {
|
||||||
|
"url": "/root/v1/cnb-dev",
|
||||||
|
"source": "query-proxy-api"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 获取cnb_board live的repo信息
|
||||||
|
*/
|
||||||
|
"live_repo_info": {
|
||||||
|
"path": "cnb_board",
|
||||||
|
"key": "live_repo_info",
|
||||||
|
"description": "获取cnb_board live的repo信息",
|
||||||
|
"metadata": {
|
||||||
|
"url": "/root/v1/cnb-dev",
|
||||||
|
"source": "query-proxy-api"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 获取cnb_board live的构建信息
|
||||||
|
*/
|
||||||
|
"live_build_info": {
|
||||||
|
"path": "cnb_board",
|
||||||
|
"key": "live_build_info",
|
||||||
|
"description": "获取cnb_board live的构建信息",
|
||||||
|
"metadata": {
|
||||||
|
"url": "/root/v1/cnb-dev",
|
||||||
|
"source": "query-proxy-api"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 获取cnb_board live的PR信息
|
||||||
|
*/
|
||||||
|
"live_pull_info": {
|
||||||
|
"path": "cnb_board",
|
||||||
|
"key": "live_pull_info",
|
||||||
|
"description": "获取cnb_board live的PR信息",
|
||||||
|
"metadata": {
|
||||||
|
"url": "/root/v1/cnb-dev",
|
||||||
|
"source": "query-proxy-api"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 获取cnb_board live的NPC信息
|
||||||
|
*/
|
||||||
|
"live_npc_info": {
|
||||||
|
"path": "cnb_board",
|
||||||
|
"key": "live_npc_info",
|
||||||
|
"description": "获取cnb_board live的NPC信息",
|
||||||
|
"metadata": {
|
||||||
|
"url": "/root/v1/cnb-dev",
|
||||||
|
"source": "query-proxy-api"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 获取cnb_board live的评论信息
|
||||||
|
*/
|
||||||
|
"live_comment_info": {
|
||||||
|
"path": "cnb_board",
|
||||||
|
"key": "live_comment_info",
|
||||||
|
"description": "获取cnb_board live的评论信息",
|
||||||
|
"metadata": {
|
||||||
|
"url": "/root/v1/cnb-dev",
|
||||||
|
"source": "query-proxy-api"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 获取cnb_board live的mdContent内容
|
||||||
|
*
|
||||||
|
* @param data - Request parameters
|
||||||
|
* @param data.more - {boolean} 是否获取更多系统信息,默认false
|
||||||
|
*/
|
||||||
|
"live": {
|
||||||
|
"path": "cnb_board",
|
||||||
|
"key": "live",
|
||||||
|
"description": "获取cnb_board live的mdContent内容",
|
||||||
|
"metadata": {
|
||||||
|
"args": {
|
||||||
|
"more": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"description": "是否获取更多系统信息,默认false",
|
||||||
|
"type": "boolean",
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": "/root/v1/cnb-dev",
|
||||||
|
"source": "query-proxy-api"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* cnb的工作环境退出程序
|
||||||
|
*/
|
||||||
|
"exit": {
|
||||||
|
"path": "cnb_board",
|
||||||
|
"key": "exit",
|
||||||
|
"description": "cnb的工作环境退出程序",
|
||||||
|
"metadata": {
|
||||||
|
"url": "/root/v1/cnb-dev",
|
||||||
|
"source": "query-proxy-api"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} as const;
|
||||||
|
const queryApi = createQueryApi({ api, query });
|
||||||
|
export { queryApi };
|
||||||
|
|
||||||
|
// const result = await queryApi['cnb_board']['live_build_info']()
|
||||||
84
src/pages/cnb-board/page.tsx
Normal file
84
src/pages/cnb-board/page.tsx
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import { useEffect, useState, useCallback, useMemo } from 'react';
|
||||||
|
import { useCnbBoardStore } from './store';
|
||||||
|
import { InfoCard } from './components/InfoCard';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { RotateCcw } from 'lucide-react';
|
||||||
|
import { useLocation } from '@tanstack/react-router';
|
||||||
|
|
||||||
|
type TabValue = 'build' | 'repo' | 'pull' | 'npc' | 'comment' | 'content';
|
||||||
|
|
||||||
|
const tabs: { value: TabValue; label: string }[] = [
|
||||||
|
{ value: 'build', label: '构建信息' },
|
||||||
|
{ value: 'repo', label: '仓库信息' },
|
||||||
|
{ value: 'pull', label: 'PR 信息' },
|
||||||
|
{ value: 'npc', label: 'NPC 信息' },
|
||||||
|
{ value: 'comment', label: '评论信息' },
|
||||||
|
{ value: 'content', label: 'MD 内容' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const App = () => {
|
||||||
|
const { loading, data, fetchAllData, refresh } = useCnbBoardStore();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
// 从 URL 读取 tab 参数
|
||||||
|
const searchParams = useMemo(() => new URLSearchParams(location.search), [location.search]);
|
||||||
|
const urlTab = searchParams.get('tab') as TabValue | null;
|
||||||
|
|
||||||
|
const [activeTab, setActiveTab] = useState<TabValue>(
|
||||||
|
urlTab && tabs.some(t => t.value === urlTab) ? urlTab : 'build'
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchAllData();
|
||||||
|
}, [fetchAllData]);
|
||||||
|
|
||||||
|
const handleTabChange = useCallback((tab: TabValue) => {
|
||||||
|
setActiveTab(tab);
|
||||||
|
const params = new URLSearchParams(location.search);
|
||||||
|
params.set('tab', tab);
|
||||||
|
// 使用 hash 路由模式
|
||||||
|
window.history.pushState(null, '', `?${params.toString()}`);
|
||||||
|
}, [location.search]);
|
||||||
|
|
||||||
|
const currentData = activeTab === 'build' ? data['live_build_info']
|
||||||
|
: activeTab === 'repo' ? data['live_repo_info']
|
||||||
|
: activeTab === 'pull' ? data['live_pull_info']
|
||||||
|
: activeTab === 'npc' ? data['live_npc_info']
|
||||||
|
: activeTab === 'comment' ? data['live_comment_info']
|
||||||
|
: data.live;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-4 max-w-7xl mx-auto">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h1 className="text-2xl font-bold">CNB Board Live 信息</h1>
|
||||||
|
<Button variant="outline" size="sm" onClick={refresh} disabled={loading}>
|
||||||
|
<RotateCcw className={`mr-2 h-4 w-4 ${loading ? 'animate-spin' : ''}`} />
|
||||||
|
刷新
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap gap-2 mb-4">
|
||||||
|
{tabs.map((tab) => (
|
||||||
|
<Button
|
||||||
|
key={tab.value}
|
||||||
|
variant={activeTab === tab.value ? 'default' : 'outline'}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleTabChange(tab.value)}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{currentData?.list ? (
|
||||||
|
<InfoCard title={currentData.title} list={currentData.list} />
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-8 text-muted-foreground">
|
||||||
|
加载中...
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default App;
|
||||||
88
src/pages/cnb-board/store.ts
Normal file
88
src/pages/cnb-board/store.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
import { queryApi } from './modules/api';
|
||||||
|
import { Result } from '@kevisual/query';
|
||||||
|
|
||||||
|
interface InfoItem {
|
||||||
|
title: string;
|
||||||
|
value: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ApiResponse {
|
||||||
|
title: string;
|
||||||
|
list: InfoItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CnbBoardData {
|
||||||
|
live: ApiResponse | null;
|
||||||
|
'live_repo_info': ApiResponse | null;
|
||||||
|
'live_build_info': ApiResponse | null;
|
||||||
|
'live_pull_info': ApiResponse | null;
|
||||||
|
'live_npc_info': ApiResponse | null;
|
||||||
|
'live_comment_info': ApiResponse | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
loading: boolean;
|
||||||
|
data: CnbBoardData;
|
||||||
|
fetchAllData: () => Promise<void>;
|
||||||
|
refresh: () => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCnbBoardStore = create<State>((set, get) => ({
|
||||||
|
loading: false,
|
||||||
|
data: {
|
||||||
|
live: null,
|
||||||
|
'live_repo_info': null,
|
||||||
|
'live_build_info': null,
|
||||||
|
'live_pull_info': null,
|
||||||
|
'live_npc_info': null,
|
||||||
|
'live_comment_info': null,
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchAllData: async () => {
|
||||||
|
// 1. 先优先加载 live 数据
|
||||||
|
set({ loading: true });
|
||||||
|
try {
|
||||||
|
const live = await queryApi['cnb_board']['live']({ more: false }) as Result<ApiResponse>;
|
||||||
|
set({
|
||||||
|
loading: false,
|
||||||
|
data: {
|
||||||
|
...get().data,
|
||||||
|
live: live?.data || null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
set({ loading: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 再并行加载其他数据
|
||||||
|
try {
|
||||||
|
const [liveRepoInfo, liveBuildInfo, livePullInfo, liveNpcInfo, liveCommentInfo] =
|
||||||
|
await Promise.all([
|
||||||
|
queryApi['cnb_board']['live_repo_info']() as Promise<Result<ApiResponse>>,
|
||||||
|
queryApi['cnb_board']['live_build_info']() as Promise<Result<ApiResponse>>,
|
||||||
|
queryApi['cnb_board']['live_pull_info']() as Promise<Result<ApiResponse>>,
|
||||||
|
queryApi['cnb_board']['live_npc_info']() as Promise<Result<ApiResponse>>,
|
||||||
|
queryApi['cnb_board']['live_comment_info']() as Promise<Result<ApiResponse>>,
|
||||||
|
]);
|
||||||
|
|
||||||
|
set({
|
||||||
|
data: {
|
||||||
|
...get().data,
|
||||||
|
'live_repo_info': liveRepoInfo?.data || null,
|
||||||
|
'live_build_info': liveBuildInfo?.data || null,
|
||||||
|
'live_pull_info': livePullInfo?.data || null,
|
||||||
|
'live_npc_info': liveNpcInfo?.data || null,
|
||||||
|
'live_comment_info': liveCommentInfo?.data || null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// 其他数据加载失败不影响主流程
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
refresh: async () => {
|
||||||
|
await get().fetchAllData();
|
||||||
|
},
|
||||||
|
}));
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
const Home = () => {
|
import { App } from './cnb-board/page'
|
||||||
return <div>Home Page</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Home;
|
export default App;
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
import { Route as rootRouteImport } from './routes/__root'
|
import { Route as rootRouteImport } from './routes/__root'
|
||||||
import { Route as DemoRouteImport } from './routes/demo'
|
import { Route as DemoRouteImport } from './routes/demo'
|
||||||
|
import { Route as CnbBoardRouteImport } from './routes/cnb-board'
|
||||||
import { Route as IndexRouteImport } from './routes/index'
|
import { Route as IndexRouteImport } from './routes/index'
|
||||||
|
|
||||||
const DemoRoute = DemoRouteImport.update({
|
const DemoRoute = DemoRouteImport.update({
|
||||||
@@ -17,6 +18,11 @@ const DemoRoute = DemoRouteImport.update({
|
|||||||
path: '/demo',
|
path: '/demo',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any)
|
||||||
|
const CnbBoardRoute = CnbBoardRouteImport.update({
|
||||||
|
id: '/cnb-board',
|
||||||
|
path: '/cnb-board',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
const IndexRoute = IndexRouteImport.update({
|
const IndexRoute = IndexRouteImport.update({
|
||||||
id: '/',
|
id: '/',
|
||||||
path: '/',
|
path: '/',
|
||||||
@@ -25,27 +31,31 @@ const IndexRoute = IndexRouteImport.update({
|
|||||||
|
|
||||||
export interface FileRoutesByFullPath {
|
export interface FileRoutesByFullPath {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
|
'/cnb-board': typeof CnbBoardRoute
|
||||||
'/demo': typeof DemoRoute
|
'/demo': typeof DemoRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
|
'/cnb-board': typeof CnbBoardRoute
|
||||||
'/demo': typeof DemoRoute
|
'/demo': typeof DemoRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesById {
|
export interface FileRoutesById {
|
||||||
__root__: typeof rootRouteImport
|
__root__: typeof rootRouteImport
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
|
'/cnb-board': typeof CnbBoardRoute
|
||||||
'/demo': typeof DemoRoute
|
'/demo': typeof DemoRoute
|
||||||
}
|
}
|
||||||
export interface FileRouteTypes {
|
export interface FileRouteTypes {
|
||||||
fileRoutesByFullPath: FileRoutesByFullPath
|
fileRoutesByFullPath: FileRoutesByFullPath
|
||||||
fullPaths: '/' | '/demo'
|
fullPaths: '/' | '/cnb-board' | '/demo'
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
to: '/' | '/demo'
|
to: '/' | '/cnb-board' | '/demo'
|
||||||
id: '__root__' | '/' | '/demo'
|
id: '__root__' | '/' | '/cnb-board' | '/demo'
|
||||||
fileRoutesById: FileRoutesById
|
fileRoutesById: FileRoutesById
|
||||||
}
|
}
|
||||||
export interface RootRouteChildren {
|
export interface RootRouteChildren {
|
||||||
IndexRoute: typeof IndexRoute
|
IndexRoute: typeof IndexRoute
|
||||||
|
CnbBoardRoute: typeof CnbBoardRoute
|
||||||
DemoRoute: typeof DemoRoute
|
DemoRoute: typeof DemoRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,6 +68,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof DemoRouteImport
|
preLoaderRoute: typeof DemoRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport
|
||||||
}
|
}
|
||||||
|
'/cnb-board': {
|
||||||
|
id: '/cnb-board'
|
||||||
|
path: '/cnb-board'
|
||||||
|
fullPath: '/cnb-board'
|
||||||
|
preLoaderRoute: typeof CnbBoardRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
'/': {
|
'/': {
|
||||||
id: '/'
|
id: '/'
|
||||||
path: '/'
|
path: '/'
|
||||||
@@ -70,6 +87,7 @@ declare module '@tanstack/react-router' {
|
|||||||
|
|
||||||
const rootRouteChildren: RootRouteChildren = {
|
const rootRouteChildren: RootRouteChildren = {
|
||||||
IndexRoute: IndexRoute,
|
IndexRoute: IndexRoute,
|
||||||
|
CnbBoardRoute: CnbBoardRoute,
|
||||||
DemoRoute: DemoRoute,
|
DemoRoute: DemoRoute,
|
||||||
}
|
}
|
||||||
export const routeTree = rootRouteImport
|
export const routeTree = rootRouteImport
|
||||||
|
|||||||
@@ -1,35 +1,52 @@
|
|||||||
import { Link, Outlet, createRootRoute, useNavigate } from '@tanstack/react-router'
|
// import { LayoutMain } from '@/modules/layout'
|
||||||
|
const LayoutMain = null;
|
||||||
|
import { Link, 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 { AuthProvider } from '@/pages/auth'
|
import { AuthProvider } from '@/pages/auth'
|
||||||
|
import { TooltipProvider } from '@/components/ui/tooltip'
|
||||||
|
import { Home } from 'lucide-react';
|
||||||
export const Route = createRootRoute({
|
export const Route = createRootRoute({
|
||||||
component: RootComponent,
|
component: RootComponent,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const BaseHeader = (props: { main?: React.ComponentType | null }) => {
|
||||||
|
if (props.main) {
|
||||||
|
const MainComponent = props.main
|
||||||
|
return <MainComponent />
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex gap-2 text-lg w-full h-12 items-center">
|
||||||
|
<div className='px-2'>
|
||||||
|
<Link
|
||||||
|
to="/"
|
||||||
|
activeProps={{
|
||||||
|
className: 'font-bold',
|
||||||
|
}}
|
||||||
|
activeOptions={{ exact: true }}
|
||||||
|
>
|
||||||
|
<Home className='w-5 h-5' />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
function RootComponent() {
|
function RootComponent() {
|
||||||
return (
|
return (
|
||||||
<div className='h-full overflow-hidden'>
|
<div className='h-full overflow-hidden'>
|
||||||
|
<BaseHeader main={LayoutMain} />
|
||||||
<div className="p-2 flex gap-2 text-lg">
|
<AuthProvider mustLogin={true}>
|
||||||
<Link
|
<TooltipProvider>
|
||||||
to="/"
|
<main className='h-[calc(100%-3rem)] overflow-auto scrollbar'>
|
||||||
activeProps={{
|
<Outlet />
|
||||||
className: 'font-bold',
|
</main>
|
||||||
}}
|
</TooltipProvider>
|
||||||
activeOptions={{ exact: true }}
|
|
||||||
>
|
|
||||||
Home
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<hr />
|
|
||||||
<AuthProvider>
|
|
||||||
<main className='h-[calc(100%-4rem)] overflow-auto scrollbar'>
|
|
||||||
<Outlet />
|
|
||||||
</main>
|
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
<TanStackRouterDevtools position="bottom-right" />
|
<TanStackRouterDevtools position="bottom-right" />
|
||||||
<Toaster />
|
<Toaster />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
9
src/routes/cnb-board.tsx
Normal file
9
src/routes/cnb-board.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
|
import { App } from '@/pages/cnb-board/page'
|
||||||
|
export const Route = createFileRoute('/cnb-board')({
|
||||||
|
component: RouteComponent,
|
||||||
|
})
|
||||||
|
|
||||||
|
function RouteComponent() {
|
||||||
|
return <App />
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user