feat: implement layout store management and add API documentation modal

This commit is contained in:
2026-02-26 04:39:17 +08:00
parent 5562296ad7
commit 3edd6b2a69
8 changed files with 97 additions and 31 deletions

View File

@@ -1,3 +1,8 @@
import { QueryRouterServer } from '@kevisual/router/browser'
import { use } from '@kevisual/context'
export const app = use('app', new QueryRouterServer())
import { useLayoutStore } from '@/pages/auth/store'
const layoutStore = useLayoutStore.getState()
layoutStore.setShowBaseHeader(false)

View File

@@ -1,3 +1,5 @@
"use client"
import * as React from "react"
import { Dialog as DialogPrimitive } from "@base-ui/react/dialog"

View File

@@ -1,14 +0,0 @@
import { AppProvider } from '../../../studio/index.tsx';
interface PageProps {
params: Promise<{
root: string
appId: string
}>
}
export default async function Page({ params }: PageProps) {
const { root, appId } = await params;
console.log('root', root, 'appId', appId);
return <AppProvider />;
}

View File

@@ -1,6 +1,6 @@
import { filterRouteInfo, useStudioStore } from './store.ts';
import { use, useEffect, useState } from 'react';
import { MonitorPlay, Play, PanelLeft, PanelLeftClose, PanelRight, PanelRightClose, Filter, FilterX, Search, X, MoreHorizontal, Info, Code, RotateCcw } from 'lucide-react';
import { MonitorPlay, Play, PanelLeft, PanelLeftClose, PanelRight, PanelRightClose, PanelTop, PanelTopClose, Filter, FilterX, Search, X, MoreHorizontal, Info, Code, RotateCcw, Book } from 'lucide-react';
import { Panel, Group } from 'react-resizable-panels'
import { ViewList } from '../view/list.tsx';
import { useShallow } from 'zustand/shallow';
@@ -12,6 +12,7 @@ import { ExportDialog } from './components/ExportDialog';
import { useQueryViewStore } from '../query-view/store/index.ts';
import { toast } from 'sonner';
import { DetailsDialog } from '../query-view/components/DetailsDialog.tsx';
import { useLayoutStore } from '../auth/store.ts';
export const AppProvider = () => {
const { showLeftPanel, showRightPanel } = useStudioStore(useShallow((state) => ({
showLeftPanel: state.showLeftPanel,
@@ -49,13 +50,27 @@ export const WrapperHeader = (props: { children: React.ReactNode }) => {
showRightPanel: state.showRightPanel,
setShowRightPanel: state.setShowRightPanel,
})));
const layoutStore = useLayoutStore(useShallow((state) => ({
showBaseHeader: state.showBaseHeader,
setShowBaseHeader: state.setShowBaseHeader,
})));
return <div className='h-full'>
<div className="w-full h-12 flex items-center justify-between px-4 border-b border-gray-200 bg-white">
<div className='flex gap-2'>
<div className="cursor-pointer text-gray-600 hover:text-gray-900 transition-colors" title="Kevisual Router Studio" onClick={() => {
store.setShowLeftPanel(!store.showLeftPanel);
}}>
{showLeftPanel ? <PanelLeftClose size={16} /> : <PanelLeft size={16} />}
</div>
<div className='cursor-pointer text-gray-600 hover:text-gray-900 transition-colors" title={layoutStore.showBaseHeader ? "隐藏BaseHeader" : "显示BaseHeader"}' onClick={
() => {
layoutStore.setShowBaseHeader(!layoutStore.showBaseHeader)
}
}>
{layoutStore.showBaseHeader ? <PanelTopClose size={16} /> : <PanelTop size={16} />}
</div>
</div>
<div className="flex items-center gap-2">
<button
className="p-1.5 rounded-md text-gray-500 hover:text-gray-900 hover:bg-gray-200 transition-all duration-200 cursor-pointer"

View File

@@ -70,6 +70,8 @@ interface StudioState {
setShowExportDialog: (show: boolean) => void;
exportRoutes?: RouteItem[];
setExportRoutes: (routes?: RouteItem[]) => void;
showApiDocs: boolean;
setShowApiDocs: (show: boolean) => void;
}
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
export const filterRouteInfo = (viewData: RouterViewItem) => {
@@ -332,7 +334,9 @@ export const useStudioStore = create<StudioState>()(
showExportDialog: false,
setShowExportDialog: (show: boolean) => set({ showExportDialog: show }),
exportRoutes: undefined,
setExportRoutes: (routes?: RouteItem[]) => set({ exportRoutes: routes })
setExportRoutes: (routes?: RouteItem[]) => set({ exportRoutes: routes }),
showApiDocs: false,
setShowApiDocs: (show: boolean) => set({ showApiDocs: show }),
}),
{
name: 'studio-storage',

View File

@@ -0,0 +1,34 @@
import { useStudioStore } from "@/pages/studio/store";
import { useShallow } from "zustand/shallow";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
export const DocsModal = () => {
const store = useStudioStore(useShallow((state) => ({
showApiDocs: state.showApiDocs,
setShowApiDocs: state.setShowApiDocs,
})));
return (
<Dialog open={store.showApiDocs} onOpenChange={store.setShowApiDocs}>
<DialogContent className="max-w-3xl! max-h-[80vh] overflow-hidden">
<DialogHeader>
<DialogTitle className="text-xl">API </DialogTitle>
</DialogHeader>
<div className="flex-1 overflow-auto">
<p> API ...</p>
</div>
<DialogFooter>
<Button onClick={() => store.setShowApiDocs(false)}></Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};

View File

@@ -1,6 +1,6 @@
import { useEffect, useState } from "react";
import { useStudioStore } from '../studio/store.ts';
import { Search, RotateCw, Plus, MoreHorizontal, Layout, Edit2, Trash2, MousePointer2 } from "lucide-react";
import { Search, RotateCw, Plus, MoreHorizontal, Layout, Edit2, Trash2, MousePointer2, Book } from "lucide-react";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import {
@@ -14,7 +14,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover
import { ViewEditor } from "@/pages/view/components/ViewEditor.tsx";
import { toast } from "sonner";
import { useShallow } from "zustand/shallow";
import { DocsModal } from './components/DocsModal.tsx'
const ViewItem = ({ view, onEdit, onDelete, onDeleteViewItem }: { view: any; onEdit: (view: any) => void; onDelete: (id: string) => void; onDeleteViewItem: (id: string, viewId: string) => void }) => {
const [expanded, setExpanded] = useState(false);
const studioStore = useStudioStore(useShallow((state) => ({
@@ -158,22 +158,29 @@ const ViewItem = ({ view, onEdit, onDelete, onDeleteViewItem }: { view: any; onE
</div>
}
export const ViewList = () => {
const { routeViewList, updateRouteView, deleteRouteView, deleteRouteViewItem, getViewList } = useStudioStore();
const store = useStudioStore(useShallow((state) => ({
routeViewList: state.routeViewList,
updateRouteView: state.updateRouteView,
deleteRouteView: state.deleteRouteView,
deleteRouteViewItem: state.deleteRouteViewItem,
getViewList: state.getViewList,
setShowApiDocs: state.setShowApiDocs,
})));
const [searchTerm, setSearchTerm] = useState("");
const [editorOpen, setEditorOpen] = useState(false);
const [editingView, setEditingView] = useState<any>(null);
const filteredViews = routeViewList.filter(view =>
const filteredViews = store.routeViewList.filter(view =>
(view.title || '未命名视图').toLowerCase().includes(searchTerm.toLowerCase()) ||
(view.description || '').toLowerCase().includes(searchTerm.toLowerCase())
);
useEffect(() => {
getViewList();
store.getViewList();
}, [])
const handleRefresh = async () => {
const toastId = toast.loading('正在刷新视图列表...');
await getViewList();
await store.getViewList();
// toast.update(toastId, { render: '视图列表已刷新', type: 'success', id: false, autoClose: 1000 });
toast.success('视图列表已刷新', { duration: 1000 });
toast.dismiss(toastId);
@@ -190,17 +197,20 @@ export const ViewList = () => {
const handleDelete = (id: string) => {
if (confirm('确定要删除这个视图吗?')) {
deleteRouteView(id);
store.deleteRouteView(id);
}
};
const handleSaveView = (viewData: any) => {
updateRouteView(viewData);
store.updateRouteView(viewData);
};
return (
<div className="w-full h-full max-w-4xl p-4 border border-gray-200 rounded-md shadow-sm">
<div className="flex items-center space-x-2 mb-4">
<Button onClick={() => store.setShowApiDocs(true)} title='文档' variant="outline" size="icon" className="h-8 w-8 cursor-pointer border-gray-300">
<Book size={16} />
</Button>
<div className="relative flex-1">
<Input
placeholder="搜索视图..."
@@ -230,7 +240,7 @@ export const ViewList = () => {
view={view}
onEdit={handleEdit}
onDelete={handleDelete}
onDeleteViewItem={deleteRouteViewItem}
onDeleteViewItem={store.deleteRouteViewItem}
/>
))
)}
@@ -242,6 +252,7 @@ export const ViewList = () => {
data={editingView}
onSave={handleSaveView}
/>
<DocsModal />
</div>
);
}

View File

@@ -4,18 +4,27 @@ import { TanStackRouterDevtools } from '@tanstack/react-router-devtools'
import { Toaster } from '@/components/ui/sonner'
import { AuthProvider } from '@/pages/auth'
import { TooltipProvider } from '@/components/ui/tooltip'
import { useLayoutStore } from '@/pages/auth/store';
import { useShallow } from 'zustand/shallow';
import clsx from 'clsx';
export const Route = createRootRoute({
component: RootComponent,
})
function RootComponent() {
const store = useLayoutStore(useShallow(state => ({
showBaseHeader: state.showBaseHeader,
})));
return (
<div className='h-full overflow-hidden'>
<LayoutMain />
<AuthProvider mustLogin={true}>
<TooltipProvider>
<main className='h-[calc(100%-3rem)] overflow-auto scrollbar'>
<main className={clsx('overflow-auto scrollbar', {
'h-[calc(100%-3rem)]': store.showBaseHeader,
'h-full': !store.showBaseHeader,
})}>
<Outlet />
</main>
</TooltipProvider>