import { useState, useEffect } from 'react'; import { useParams } from 'react-router-dom'; import { Card, Spin, Typography, Tag, message } from 'antd'; import { DndContext, DragOverlay, PointerSensor, useSensor, useSensors, closestCorners, } from '@dnd-kit/core'; import type { DragEndEvent, DragStartEvent } from '@dnd-kit/core'; import { listPluginData, patchPluginData } from '../api/pluginData'; import { getPluginSchema, type PluginPageSchema } from '../api/plugins'; // ── 内部看板渲染组件 ── interface KanbanInnerProps { pluginId: string; entity: string; laneField: string; laneOrder: string[]; cardTitleField: string; cardSubtitleField?: string; cardFields?: string[]; enableDrag?: boolean; } function KanbanInner({ pluginId, entity, laneField, laneOrder, cardTitleField, cardSubtitleField, cardFields, enableDrag, }: KanbanInnerProps) { const [lanes, setLanes] = useState>({}); const [loading, setLoading] = useState(true); const [activeId, setActiveId] = useState(null); const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 8 } }), ); const fetchData = async () => { setLoading(true); try { const allData: Record = {}; const results = await Promise.all( laneOrder.map(async (lane) => { const res = await listPluginData(pluginId, entity, 1, 100, { filter: { [laneField]: lane }, }); return { lane, data: res.data || [] }; }), ); for (const { lane, data } of results) { allData[lane] = data; } setLanes(allData); } finally { setLoading(false); } }; useEffect(() => { fetchData(); }, [pluginId, entity]); const handleDragStart = (event: DragStartEvent) => { setActiveId(event.active.id as string); }; const handleDragEnd = async (event: DragEndEvent) => { setActiveId(null); if (!enableDrag) return; const { active, over } = event; if (!over) return; const recordId = active.id as string; const newLane = String(over.data.current?.lane || over.id); if (!newLane) return; let currentLane = ''; for (const [lane, items] of Object.entries(lanes)) { if (items.some((item) => item.id === recordId)) { currentLane = lane; break; } } if (currentLane === newLane) return; // 乐观更新 setLanes((prev) => { const next: Record = {}; for (const [lane, items] of Object.entries(prev)) { if (lane === currentLane) { next[lane] = items.filter((item) => item.id !== recordId); } else if (lane === newLane) { const moved = prev[currentLane]?.find((item) => item.id === recordId); next[lane] = moved ? [...items, moved] : [...items]; } else { next[lane] = items; } } return next; }); try { await patchPluginData(pluginId, entity, recordId, { data: { [laneField]: newLane }, version: 0, }); message.success('移动成功'); } catch { message.error('移动失败'); fetchData(); } }; const handleDragCancel = () => { setActiveId(null); }; const activeCard = activeId ? Object.values(lanes) .flat() .find((item) => item.id === activeId) : null; if (loading) { return (
); } return (
{laneOrder.map((lane) => { const items = lanes[lane] || []; return (
{lane} {items.length}
{items.map((item) => ( {item.data?.[cardTitleField] ?? '-'} {cardSubtitleField && item.data?.[cardSubtitleField] && (
{item.data[cardSubtitleField]}
)} {cardFields && (
{cardFields.map( (f) => item.data?.[f] ? ( {String(item.data[f])} ) : null, )}
)}
))}
); })}
{activeCard ? ( {activeCard.data?.[cardTitleField] ?? '-'} {cardSubtitleField && activeCard.data?.[cardSubtitleField] && (
{activeCard.data[cardSubtitleField]}
)}
) : null}
); } // ── 路由入口:自加载 schema ── /** * 路由入口组件 * 路由: /plugins/:pluginId/kanban/:entityName * 自动加载 schema 并提取 kanban 页面配置 */ export default function PluginKanbanPageRoute() { const { pluginId, entityName } = useParams<{ pluginId: string; entityName: string; }>(); const [loading, setLoading] = useState(true); const [pageConfig, setPageConfig] = useState<{ entity: string; lane_field: string; lane_order?: string[]; card_title_field: string; card_subtitle_field?: string; card_fields?: string[]; enable_drag?: boolean; } | null>(null); useEffect(() => { if (!pluginId || !entityName) return; async function loadSchema() { try { const schema = await getPluginSchema(pluginId!); const pages: PluginPageSchema[] = schema.ui?.pages || []; const kanbanPage = pages.find( (p): p is PluginPageSchema & { type: 'kanban' } => p.type === 'kanban' && p.entity === entityName, ); if (kanbanPage) { setPageConfig(kanbanPage); } } catch { message.warning('Schema 加载失败'); } finally { setLoading(false); } } loadSchema(); }, [pluginId, entityName]); if (loading) { return (
); } if (!pageConfig) { return
未找到看板页面配置
; } return ( ); } // ── Tabs/Detail 内嵌使用 ── export interface PluginKanbanPageFromConfigProps { pluginId: string; page: { entity: string; lane_field: string; lane_order?: string[]; card_title_field: string; card_subtitle_field?: string; card_fields?: string[]; enable_drag?: boolean; }; } export function PluginKanbanPageFromConfig({ pluginId, page, }: PluginKanbanPageFromConfigProps) { return ( ); }