Files
router-studio/src/pages/view/list.tsx

258 lines
9.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useState } from "react";
import { useStudioStore } from '../studio/store.ts';
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 {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
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) => ({
currentView: state.currentView,
searchRoutes: state.searchRoutes
})));
useEffect(() => {
const currentViewId = studioStore.currentView?.viewId;
if (view.views.some((v: any) => v.id === currentViewId)) {
setExpanded(true);
}
}, [studioStore.currentView?.viewId]);
const ShowViews = (props: { views: { id: string, title: string, query?: any }[] }) => {
const studioStore = useStudioStore(useShallow((state) => ({
currentView: state.currentView,
setCurrentView: state.setCurrentView,
})));
const currentViewId = studioStore.currentView?.viewId;
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
const [deleteTargetId, setDeleteTargetId] = useState<string | null>(null);
const isActiveView = (viewId: string) => {
return viewId === currentViewId;
}
return <div className="mt-2 ml-4 w-full border-l-2 border-l-gray-300 border-gray-300 pl-3 space-y-1">
{props.views.map(v => (
<div
key={v.id}
className={`text-sm px-2 py-1 rounded cursor-pointer transition-colors flex items-center justify-between group ${isActiveView(v.id) ? 'text-black bg-gray-100' : 'text-gray-600 hover:text-black hover:bg-gray-100'}`}
onClick={(e) => {
studioStore.setCurrentView({ ...view, viewId: v.id })
}}
>
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<span>{v.title || '未命名视图'}</span>
</TooltipTrigger>
<TooltipContent side="right" className="max-w-xs">
<div className="text-xs">
{v.query ? v.query : '无查询字段'}
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<Popover open={deleteConfirmOpen && deleteTargetId === v.id} onOpenChange={(open) => {
if (!open) {
setDeleteConfirmOpen(false);
setDeleteTargetId(null);
}
}}>
<PopoverTrigger>
<Trash2
className="h-4 w-4 text-gray-400 hover:text-gray-600 transition-colors cursor-pointer opacity-0 group-hover:opacity-100"
onClick={(e) => {
e.stopPropagation();
setDeleteTargetId(v.id);
setDeleteConfirmOpen(true);
}}
/>
</PopoverTrigger>
<PopoverContent side="bottom" align="end" sideOffset={8} className="w-80 border-gray-300">
<div className="space-y-4">
<div>
<h4 className="font-semibold text-sm"></h4>
<p className="text-xs text-gray-600 mt-1"></p>
</div>
<div className="flex justify-end gap-2">
<Button className="border-gray-300" variant="outline" size="sm" onClick={() => setDeleteConfirmOpen(false)}>
</Button>
<Button
variant="destructive"
size="sm"
onClick={() => {
if (deleteTargetId) {
onDeleteViewItem(view.id, deleteTargetId);
setDeleteConfirmOpen(false);
setDeleteTargetId(null);
}
}}
>
</Button>
</div>
</div>
</PopoverContent>
</Popover>
</div>
))}
</div>
}
return <div
key={view.id}
className="flex flex-col items-center py-3 px-4 border-b border-gray-200 last:border-b-0 hover:bg-gray-50 transition-colors"
>
<div className="w-full flex justify-between" onClick={() => setExpanded(!expanded)}>
<div className="flex items-center cursor-pointer" >
<Layout className="h-4 w-4 mr-2 text-gray-500" />
{view.title || '未命名视图'}
</div>
<DropdownMenu>
<DropdownMenuTrigger
className="inline-flex items-center justify-center h-8 w-8 rounded-md hover:bg-accent hover:text-accent-foreground transition-colors"
onClick={(e) => e.stopPropagation()}
>
<MoreHorizontal className="h-4 w-4" />
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="border-gray-300">
<DropdownMenuItem className="cursor-pointer" onClick={(e) => {
e.stopPropagation();
studioStore.searchRoutes('')
}}>
<MousePointer2 className="h-4 w-4 mr-2" />
</DropdownMenuItem>
<DropdownMenuItem className="cursor-pointer" onClick={(e) => {
e.stopPropagation();
onEdit(view);
}}>
<Edit2 className="h-4 w-4 mr-2" />
</DropdownMenuItem>
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onDelete(view.id);
}}
className="cursor-pointer text-red-600 focus:text-red-600"
>
<Trash2 className="h-4 w-4 mr-2" />
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
{expanded &&
<ShowViews views={view.views} />
}
</div>
}
export const ViewList = () => {
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 = store.routeViewList.filter(view =>
(view.title || '未命名视图').toLowerCase().includes(searchTerm.toLowerCase()) ||
(view.description || '').toLowerCase().includes(searchTerm.toLowerCase())
);
useEffect(() => {
store.getViewList();
}, [])
const handleRefresh = async () => {
const toastId = toast.loading('正在刷新视图列表...');
await store.getViewList();
// toast.update(toastId, { render: '视图列表已刷新', type: 'success', id: false, autoClose: 1000 });
toast.success('视图列表已刷新', { duration: 1000 });
toast.dismiss(toastId);
};
const handleAdd = () => {
handleEdit({});
};
const handleEdit = (view: any) => {
setEditingView(view);
setEditorOpen(true);
};
const handleDelete = (id: string) => {
if (confirm('确定要删除这个视图吗?')) {
store.deleteRouteView(id);
}
};
const handleSaveView = (viewData: any) => {
store.updateRouteView(viewData);
};
return (
<div className="w-full h-full max-w-4xl py-4 border border-gray-200 rounded-md shadow-sm overflow-hidden">
<div className="flex items-center px-4 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="搜索视图..."
className="pl-3 pr-8"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
<Search className="absolute right-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
</div>
<Button variant="outline" size="icon" className="h-8 w-8 cursor-pointer border-gray-300" onClick={handleRefresh}>
<RotateCw className="h-4 w-4" />
</Button>
<Button variant="outline" size="icon" className="h-8 w-8 cursor-pointer border-gray-300" onClick={handleAdd}>
<Plus className="h-4 w-4" />
</Button>
</div>
<div className="flex flex-col px-4 overscroll-auto scrollbar" style={{ height: 'calc(100% - 32px)' }}>
{filteredViews.length === 0 ? (
<div className="text-center py-4 text-gray-500">
{searchTerm ? '未找到匹配的视图' : '暂无视图'}
</div>
) : (
filteredViews.map((view) => (
<ViewItem
key={view.id}
view={view}
onEdit={handleEdit}
onDelete={handleDelete}
onDeleteViewItem={store.deleteRouteViewItem}
/>
))
)}
</div>
<ViewEditor
open={editorOpen}
onOpenChange={setEditorOpen}
data={editingView}
onSave={handleSaveView}
/>
<DocsModal />
</div>
);
}