update
This commit is contained in:
247
src/pages/view/list.tsx
Normal file
247
src/pages/view/list.tsx
Normal file
@@ -0,0 +1,247 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useStudioStore } from '../studio/store.ts';
|
||||
import { Search, RotateCw, Plus, MoreHorizontal, Layout, Edit2, Trash2, MousePointer2 } 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";
|
||||
|
||||
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 { routeViewList, updateRouteView, deleteRouteView, deleteRouteViewItem, getViewList } = useStudioStore();
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [editorOpen, setEditorOpen] = useState(false);
|
||||
const [editingView, setEditingView] = useState<any>(null);
|
||||
|
||||
const filteredViews = routeViewList.filter(view =>
|
||||
(view.title || '未命名视图').toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
(view.description || '').toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
useEffect(() => {
|
||||
getViewList();
|
||||
}, [])
|
||||
|
||||
const handleRefresh = async () => {
|
||||
const toastId = toast.loading('正在刷新视图列表...');
|
||||
await 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('确定要删除这个视图吗?')) {
|
||||
deleteRouteView(id);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveView = (viewData: any) => {
|
||||
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">
|
||||
<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">
|
||||
{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={deleteRouteViewItem}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ViewEditor
|
||||
open={editorOpen}
|
||||
onOpenChange={setEditorOpen}
|
||||
data={editingView}
|
||||
onSave={handleSaveView}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user