feat: implement layout store management and add API documentation modal
This commit is contained in:
@@ -1,3 +1,8 @@
|
|||||||
import { QueryRouterServer } from '@kevisual/router/browser'
|
import { QueryRouterServer } from '@kevisual/router/browser'
|
||||||
import { use } from '@kevisual/context'
|
import { use } from '@kevisual/context'
|
||||||
export const app = use('app', new QueryRouterServer())
|
export const app = use('app', new QueryRouterServer())
|
||||||
|
|
||||||
|
import { useLayoutStore } from '@/pages/auth/store'
|
||||||
|
|
||||||
|
const layoutStore = useLayoutStore.getState()
|
||||||
|
layoutStore.setShowBaseHeader(false)
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { Dialog as DialogPrimitive } from "@base-ui/react/dialog"
|
import { Dialog as DialogPrimitive } from "@base-ui/react/dialog"
|
||||||
|
|
||||||
|
|||||||
@@ -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 />;
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { filterRouteInfo, useStudioStore } from './store.ts';
|
import { filterRouteInfo, useStudioStore } from './store.ts';
|
||||||
import { use, useEffect, useState } from 'react';
|
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 { Panel, Group } from 'react-resizable-panels'
|
||||||
import { ViewList } from '../view/list.tsx';
|
import { ViewList } from '../view/list.tsx';
|
||||||
import { useShallow } from 'zustand/shallow';
|
import { useShallow } from 'zustand/shallow';
|
||||||
@@ -12,6 +12,7 @@ import { ExportDialog } from './components/ExportDialog';
|
|||||||
import { useQueryViewStore } from '../query-view/store/index.ts';
|
import { useQueryViewStore } from '../query-view/store/index.ts';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { DetailsDialog } from '../query-view/components/DetailsDialog.tsx';
|
import { DetailsDialog } from '../query-view/components/DetailsDialog.tsx';
|
||||||
|
import { useLayoutStore } from '../auth/store.ts';
|
||||||
export const AppProvider = () => {
|
export const AppProvider = () => {
|
||||||
const { showLeftPanel, showRightPanel } = useStudioStore(useShallow((state) => ({
|
const { showLeftPanel, showRightPanel } = useStudioStore(useShallow((state) => ({
|
||||||
showLeftPanel: state.showLeftPanel,
|
showLeftPanel: state.showLeftPanel,
|
||||||
@@ -49,12 +50,26 @@ export const WrapperHeader = (props: { children: React.ReactNode }) => {
|
|||||||
showRightPanel: state.showRightPanel,
|
showRightPanel: state.showRightPanel,
|
||||||
setShowRightPanel: state.setShowRightPanel,
|
setShowRightPanel: state.setShowRightPanel,
|
||||||
})));
|
})));
|
||||||
|
const layoutStore = useLayoutStore(useShallow((state) => ({
|
||||||
|
showBaseHeader: state.showBaseHeader,
|
||||||
|
setShowBaseHeader: state.setShowBaseHeader,
|
||||||
|
})));
|
||||||
return <div className='h-full'>
|
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="w-full h-12 flex items-center justify-between px-4 border-b border-gray-200 bg-white">
|
||||||
<div className="cursor-pointer text-gray-600 hover:text-gray-900 transition-colors" title="Kevisual Router Studio" onClick={() => {
|
<div className='flex gap-2'>
|
||||||
store.setShowLeftPanel(!store.showLeftPanel);
|
|
||||||
}}>
|
<div className="cursor-pointer text-gray-600 hover:text-gray-900 transition-colors" title="Kevisual Router Studio" onClick={() => {
|
||||||
{showLeftPanel ? <PanelLeftClose size={16} /> : <PanelLeft size={16} />}
|
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>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ interface StudioState {
|
|||||||
setShowExportDialog: (show: boolean) => void;
|
setShowExportDialog: (show: boolean) => void;
|
||||||
exportRoutes?: RouteItem[];
|
exportRoutes?: RouteItem[];
|
||||||
setExportRoutes: (routes?: RouteItem[]) => void;
|
setExportRoutes: (routes?: RouteItem[]) => void;
|
||||||
|
showApiDocs: boolean;
|
||||||
|
setShowApiDocs: (show: boolean) => void;
|
||||||
}
|
}
|
||||||
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
||||||
export const filterRouteInfo = (viewData: RouterViewItem) => {
|
export const filterRouteInfo = (viewData: RouterViewItem) => {
|
||||||
@@ -332,7 +334,9 @@ export const useStudioStore = create<StudioState>()(
|
|||||||
showExportDialog: false,
|
showExportDialog: false,
|
||||||
setShowExportDialog: (show: boolean) => set({ showExportDialog: show }),
|
setShowExportDialog: (show: boolean) => set({ showExportDialog: show }),
|
||||||
exportRoutes: undefined,
|
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',
|
name: 'studio-storage',
|
||||||
|
|||||||
34
src/pages/view/components/DocsModal.tsx
Normal file
34
src/pages/view/components/DocsModal.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useStudioStore } from '../studio/store.ts';
|
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 { Input } from "@/components/ui/input";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -14,7 +14,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover
|
|||||||
import { ViewEditor } from "@/pages/view/components/ViewEditor.tsx";
|
import { ViewEditor } from "@/pages/view/components/ViewEditor.tsx";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { useShallow } from "zustand/shallow";
|
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 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 [expanded, setExpanded] = useState(false);
|
||||||
const studioStore = useStudioStore(useShallow((state) => ({
|
const studioStore = useStudioStore(useShallow((state) => ({
|
||||||
@@ -158,22 +158,29 @@ const ViewItem = ({ view, onEdit, onDelete, onDeleteViewItem }: { view: any; onE
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
export const ViewList = () => {
|
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 [searchTerm, setSearchTerm] = useState("");
|
||||||
const [editorOpen, setEditorOpen] = useState(false);
|
const [editorOpen, setEditorOpen] = useState(false);
|
||||||
const [editingView, setEditingView] = useState<any>(null);
|
const [editingView, setEditingView] = useState<any>(null);
|
||||||
|
|
||||||
const filteredViews = routeViewList.filter(view =>
|
const filteredViews = store.routeViewList.filter(view =>
|
||||||
(view.title || '未命名视图').toLowerCase().includes(searchTerm.toLowerCase()) ||
|
(view.title || '未命名视图').toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
(view.description || '').toLowerCase().includes(searchTerm.toLowerCase())
|
(view.description || '').toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
);
|
);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getViewList();
|
store.getViewList();
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleRefresh = async () => {
|
const handleRefresh = async () => {
|
||||||
const toastId = toast.loading('正在刷新视图列表...');
|
const toastId = toast.loading('正在刷新视图列表...');
|
||||||
await getViewList();
|
await store.getViewList();
|
||||||
// toast.update(toastId, { render: '视图列表已刷新', type: 'success', id: false, autoClose: 1000 });
|
// toast.update(toastId, { render: '视图列表已刷新', type: 'success', id: false, autoClose: 1000 });
|
||||||
toast.success('视图列表已刷新', { duration: 1000 });
|
toast.success('视图列表已刷新', { duration: 1000 });
|
||||||
toast.dismiss(toastId);
|
toast.dismiss(toastId);
|
||||||
@@ -190,17 +197,20 @@ export const ViewList = () => {
|
|||||||
|
|
||||||
const handleDelete = (id: string) => {
|
const handleDelete = (id: string) => {
|
||||||
if (confirm('确定要删除这个视图吗?')) {
|
if (confirm('确定要删除这个视图吗?')) {
|
||||||
deleteRouteView(id);
|
store.deleteRouteView(id);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSaveView = (viewData: any) => {
|
const handleSaveView = (viewData: any) => {
|
||||||
updateRouteView(viewData);
|
store.updateRouteView(viewData);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full max-w-4xl p-4 border border-gray-200 rounded-md shadow-sm">
|
<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">
|
<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">
|
<div className="relative flex-1">
|
||||||
<Input
|
<Input
|
||||||
placeholder="搜索视图..."
|
placeholder="搜索视图..."
|
||||||
@@ -230,7 +240,7 @@ export const ViewList = () => {
|
|||||||
view={view}
|
view={view}
|
||||||
onEdit={handleEdit}
|
onEdit={handleEdit}
|
||||||
onDelete={handleDelete}
|
onDelete={handleDelete}
|
||||||
onDeleteViewItem={deleteRouteViewItem}
|
onDeleteViewItem={store.deleteRouteViewItem}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
@@ -242,6 +252,7 @@ export const ViewList = () => {
|
|||||||
data={editingView}
|
data={editingView}
|
||||||
onSave={handleSaveView}
|
onSave={handleSaveView}
|
||||||
/>
|
/>
|
||||||
|
<DocsModal />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -4,18 +4,27 @@ 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 { TooltipProvider } from '@/components/ui/tooltip'
|
||||||
|
import { useLayoutStore } from '@/pages/auth/store';
|
||||||
|
import { useShallow } from 'zustand/shallow';
|
||||||
|
import clsx from 'clsx';
|
||||||
export const Route = createRootRoute({
|
export const Route = createRootRoute({
|
||||||
component: RootComponent,
|
component: RootComponent,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
function RootComponent() {
|
function RootComponent() {
|
||||||
|
const store = useLayoutStore(useShallow(state => ({
|
||||||
|
showBaseHeader: state.showBaseHeader,
|
||||||
|
})));
|
||||||
return (
|
return (
|
||||||
<div className='h-full overflow-hidden'>
|
<div className='h-full overflow-hidden'>
|
||||||
<LayoutMain />
|
<LayoutMain />
|
||||||
<AuthProvider mustLogin={true}>
|
<AuthProvider mustLogin={true}>
|
||||||
<TooltipProvider>
|
<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 />
|
<Outlet />
|
||||||
</main>
|
</main>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
|
|||||||
Reference in New Issue
Block a user