fix: - 引导流程改为从左至右显示 - 添加复制指引功能 - 流程图节点去掉 URL 显示,缩小节点尺寸 - 连线添加箭头方向指示,缩短节点间距 - 重构 graphUpdated 事件监听,修复添加/编辑页面后不实时更新的问题 - 预览页面隐藏图片附件文件名
This commit is contained in:
@@ -193,6 +193,43 @@ class GuideResource extends Resource
|
|||||||
])
|
])
|
||||||
->actions([
|
->actions([
|
||||||
Tables\Actions\EditAction::make()->label('编辑'),
|
Tables\Actions\EditAction::make()->label('编辑'),
|
||||||
|
Tables\Actions\Action::make('duplicate')
|
||||||
|
->label('复制')
|
||||||
|
->icon('heroicon-o-document-duplicate')
|
||||||
|
->color('info')
|
||||||
|
->requiresConfirmation()
|
||||||
|
->action(function (Guide $record) {
|
||||||
|
$newGuide = $record->replicate(['pages_count', 'stations_count']);
|
||||||
|
$newGuide->name = $record->name . ' (副本)';
|
||||||
|
$newGuide->created_by = auth()->id();
|
||||||
|
$newGuide->published_at = null;
|
||||||
|
$newGuide->save();
|
||||||
|
|
||||||
|
// 复制关联的线站
|
||||||
|
if ($record->stations()->count() > 0) {
|
||||||
|
$newGuide->stations()->sync($record->stations()->pluck('stations.id'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 复制页面
|
||||||
|
$pageIdMap = [];
|
||||||
|
foreach ($record->pages as $page) {
|
||||||
|
$newPage = $page->replicate();
|
||||||
|
$newPage->guide_id = $newGuide->id;
|
||||||
|
$newPage->save();
|
||||||
|
$pageIdMap[$page->id] = $newPage->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 复制边(edges),并更新页面 ID 映射
|
||||||
|
foreach ($record->edges as $edge) {
|
||||||
|
$newEdge = $edge->replicate();
|
||||||
|
$newEdge->guide_id = $newGuide->id;
|
||||||
|
$newEdge->from_page_id = $pageIdMap[$edge->from_page_id] ?? $edge->from_page_id;
|
||||||
|
$newEdge->to_page_id = $pageIdMap[$edge->to_page_id] ?? $edge->to_page_id;
|
||||||
|
$newEdge->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->to(route('filament.admin.resources.guides.edit', ['record' => $newGuide]));
|
||||||
|
}),
|
||||||
Tables\Actions\DeleteAction::make()->label('删除'),
|
Tables\Actions\DeleteAction::make()->label('删除'),
|
||||||
])
|
])
|
||||||
->bulkActions([
|
->bulkActions([
|
||||||
|
|||||||
@@ -91,20 +91,21 @@ class ManageGuidePages extends Page
|
|||||||
}
|
}
|
||||||
ksort($levelGroups);
|
ksort($levelGroups);
|
||||||
|
|
||||||
// Compute positions: center each level horizontally, stack vertically
|
// Compute positions: center each level vertically, stack horizontally (left-to-right)
|
||||||
$nodeWidth = 240;
|
$nodeWidth = 180; // matches CSS max-width
|
||||||
$gapX = 40;
|
$nodeHeight = 80; // compact node height
|
||||||
$gapY = 150;
|
$gapX = 110; // horizontal gap between levels
|
||||||
|
$gapY = 30; // vertical gap within same level
|
||||||
$positions = [];
|
$positions = [];
|
||||||
|
|
||||||
foreach ($levelGroups as $level => $ids) {
|
foreach ($levelGroups as $level => $ids) {
|
||||||
$count = count($ids);
|
$count = count($ids);
|
||||||
$totalWidth = $count * $nodeWidth + ($count - 1) * $gapX;
|
$totalHeight = $count * $nodeHeight + ($count - 1) * $gapY;
|
||||||
$startX = max(20, (800 - $totalWidth) / 2);
|
$startY = max(20, (600 - $totalHeight) / 2);
|
||||||
foreach ($ids as $i => $id) {
|
foreach ($ids as $i => $id) {
|
||||||
$positions[$id] = [
|
$positions[$id] = [
|
||||||
'x' => (int) ($startX + $i * ($nodeWidth + $gapX)),
|
'x' => 40 + $level * ($nodeWidth + $gapX),
|
||||||
'y' => 40 + $level * $gapY,
|
'y' => (int) ($startY + $i * ($nodeHeight + $gapY)),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -129,6 +130,11 @@ class ManageGuidePages extends Page
|
|||||||
|
|
||||||
// -- Livewire methods called by Drawflow events --
|
// -- Livewire methods called by Drawflow events --
|
||||||
|
|
||||||
|
private function dispatchGraphUpdated(): void
|
||||||
|
{
|
||||||
|
$this->dispatch('graphUpdated', nodes: $this->nodes, edges: $this->edges);
|
||||||
|
}
|
||||||
|
|
||||||
public function addEdge(int $fromPageId, int $toPageId, string $outputClass = 'output_1'): void
|
public function addEdge(int $fromPageId, int $toPageId, string $outputClass = 'output_1'): void
|
||||||
{
|
{
|
||||||
$guide = $this->getRecord();
|
$guide = $this->getRecord();
|
||||||
@@ -170,7 +176,7 @@ class ManageGuidePages extends Page
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$this->loadGraph();
|
$this->loadGraph();
|
||||||
$this->dispatch('graphUpdated');
|
$this->dispatchGraphUpdated();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function removeEdge(int $fromPageId, int $toPageId): void
|
public function removeEdge(int $fromPageId, int $toPageId): void
|
||||||
@@ -181,7 +187,7 @@ class ManageGuidePages extends Page
|
|||||||
->delete();
|
->delete();
|
||||||
|
|
||||||
$this->loadGraph();
|
$this->loadGraph();
|
||||||
$this->dispatch('graphUpdated');
|
$this->dispatchGraphUpdated();
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- Filament Actions --
|
// -- Filament Actions --
|
||||||
@@ -196,7 +202,7 @@ class ManageGuidePages extends Page
|
|||||||
$this->getRecord()->pages()->create($data);
|
$this->getRecord()->pages()->create($data);
|
||||||
|
|
||||||
$this->loadGraph();
|
$this->loadGraph();
|
||||||
$this->dispatch('graphUpdated');
|
$this->dispatchGraphUpdated();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,7 +225,7 @@ class ManageGuidePages extends Page
|
|||||||
$page->update($data);
|
$page->update($data);
|
||||||
|
|
||||||
$this->loadGraph();
|
$this->loadGraph();
|
||||||
$this->dispatch('graphUpdated');
|
$this->dispatchGraphUpdated();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,7 +241,7 @@ class ManageGuidePages extends Page
|
|||||||
$page->delete();
|
$page->delete();
|
||||||
|
|
||||||
$this->loadGraph();
|
$this->loadGraph();
|
||||||
$this->dispatch('graphUpdated');
|
$this->dispatchGraphUpdated();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,7 +257,7 @@ class ManageGuidePages extends Page
|
|||||||
$edge->delete();
|
$edge->delete();
|
||||||
|
|
||||||
$this->loadGraph();
|
$this->loadGraph();
|
||||||
$this->dispatch('graphUpdated');
|
$this->dispatchGraphUpdated();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,13 +17,8 @@
|
|||||||
<x-filament-actions::modals />
|
<x-filament-actions::modals />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.guide-flow-page {
|
.guide-flow-page { width: 100%; }
|
||||||
width: 100%;
|
.guide-flow-canvas { width: 100%; }
|
||||||
}
|
|
||||||
|
|
||||||
.guide-flow-canvas {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#drawflow {
|
#drawflow {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -33,344 +28,311 @@
|
|||||||
background-size: 20px 20px;
|
background-size: 20px 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1024px) {
|
@media (max-width: 1024px) { #drawflow { height: max(560px, calc(100vh - 16rem)); } }
|
||||||
#drawflow {
|
@media (max-width: 640px) { #drawflow { height: max(480px, 70vh); } }
|
||||||
height: max(560px, calc(100vh - 16rem));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
/* ── Node ── */
|
||||||
#drawflow {
|
|
||||||
height: max(480px, 70vh);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Node styling */
|
|
||||||
.drawflow .drawflow-node {
|
.drawflow .drawflow-node {
|
||||||
border-radius: 12px;
|
border-radius: 8px;
|
||||||
border: 1px solid #e5e7eb;
|
border: 1px solid #e5e7eb;
|
||||||
background: white;
|
background: white;
|
||||||
min-width: 200px;
|
min-width: 160px;
|
||||||
|
max-width: 180px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
.drawflow .drawflow-node.selected {
|
.drawflow .drawflow-node.selected {
|
||||||
border-color: #f59e0b;
|
border-color: #f59e0b;
|
||||||
box-shadow: 0 0 0 2px rgba(245, 158, 11, 0.3);
|
box-shadow: 0 0 0 2px rgba(245,158,11,.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.drawflow .drawflow-node.entry-node {
|
.drawflow .drawflow-node.entry-node {
|
||||||
border: 2px solid #f59e0b;
|
border: 2px solid #f59e0b;
|
||||||
}
|
}
|
||||||
|
.drawflow .drawflow-node .drawflow_content_node { padding: 0; }
|
||||||
|
|
||||||
.drawflow .drawflow-node .drawflow_content_node {
|
/* ── Node content ── */
|
||||||
padding: 0;
|
.df-node-content { padding: 8px 10px; }
|
||||||
}
|
|
||||||
|
|
||||||
/* Node content */
|
|
||||||
.df-node-content {
|
|
||||||
padding: 10px 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.df-node-header {
|
.df-node-header {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 13px;
|
font-size: 12px;
|
||||||
color: #1f2937;
|
color: #1f2937;
|
||||||
margin-bottom: 2px;
|
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
}
|
word-break: break-all;
|
||||||
|
|
||||||
.df-node-url {
|
|
||||||
font-size: 11px;
|
|
||||||
color: #9ca3af;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
max-width: 200px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.df-node-badge {
|
.df-node-badge {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-size: 10px;
|
font-size: 9px;
|
||||||
background: #fef3c7;
|
background: #fef3c7;
|
||||||
color: #b45309;
|
color: #b45309;
|
||||||
padding: 1px 6px;
|
padding: 1px 5px;
|
||||||
border-radius: 4px;
|
border-radius: 3px;
|
||||||
margin-top: 4px;
|
margin-top: 3px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.df-node-actions {
|
.df-node-actions {
|
||||||
margin-top: 6px;
|
margin-top: 5px;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 6px;
|
||||||
border-top: 1px solid #f3f4f6;
|
border-top: 1px solid #f3f4f6;
|
||||||
padding-top: 6px;
|
padding-top: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.df-node-actions button,
|
.df-node-actions button,
|
||||||
.df-node-actions a {
|
.df-node-actions a {
|
||||||
font-size: 11px;
|
font-size: 10px;
|
||||||
color: #6b7280;
|
color: #6b7280;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
padding: 2px 0;
|
padding: 1px 0;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
transition: color 0.15s;
|
transition: color .15s;
|
||||||
}
|
}
|
||||||
|
.df-node-actions button:hover, .df-node-actions a:hover { color: #3b82f6; }
|
||||||
|
.df-node-actions button.btn-danger:hover { color: #ef4444; }
|
||||||
|
|
||||||
.df-node-actions button:hover,
|
/* ── Connections ── */
|
||||||
.df-node-actions a:hover {
|
|
||||||
color: #3b82f6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.df-node-actions button.btn-danger:hover {
|
|
||||||
color: #ef4444;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Connection styling */
|
|
||||||
.drawflow .connection .main-path {
|
.drawflow .connection .main-path {
|
||||||
stroke: #94a3b8;
|
stroke: #64748b;
|
||||||
stroke-width: 2px;
|
stroke-width: 2px;
|
||||||
|
fill: none;
|
||||||
|
marker-end: url(#arrowhead);
|
||||||
}
|
}
|
||||||
|
|
||||||
.drawflow .connection .main-path:hover {
|
.drawflow .connection .main-path:hover {
|
||||||
stroke: #ef4444;
|
stroke: #ef4444;
|
||||||
stroke-width: 3px;
|
stroke-width: 2.5px;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Port containers: top (input) / bottom (output) */
|
/* Edge label */
|
||||||
|
.drawflow .connection .connection-label {
|
||||||
|
font-size: 10px;
|
||||||
|
fill: #1e40af;
|
||||||
|
font-weight: 500;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.drawflow .connection .connection-label-bg {
|
||||||
|
fill: #dbeafe;
|
||||||
|
rx: 3;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Ports ── */
|
||||||
.drawflow .drawflow-node .inputs {
|
.drawflow .drawflow-node .inputs {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -6px;
|
left: -6px; top: 0; bottom: 0;
|
||||||
left: 0;
|
width: auto !important; height: auto !important;
|
||||||
right: 0;
|
display: flex; flex-direction: column; justify-content: center;
|
||||||
width: auto !important;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
}
|
||||||
.drawflow .drawflow-node .outputs {
|
.drawflow .drawflow-node .outputs {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: -6px;
|
right: -6px; top: 0; bottom: 0;
|
||||||
left: 0;
|
width: auto !important; height: auto !important;
|
||||||
right: 0;
|
display: flex; flex-direction: column;
|
||||||
width: auto !important;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-evenly;
|
justify-content: space-evenly;
|
||||||
gap: 24px;
|
gap: 16px; padding: 16px 0;
|
||||||
padding: 0 20px;
|
|
||||||
}
|
}
|
||||||
|
.drawflow .drawflow-node .input { left: 0 !important; top: 0 !important; margin: 0; }
|
||||||
|
.drawflow .drawflow-node .output { right: 0 !important; top: 0 !important; margin: 0; }
|
||||||
|
|
||||||
/* Reset Drawflow default offsets on individual ports */
|
|
||||||
.drawflow .drawflow-node .input {
|
|
||||||
left: 0 !important;
|
|
||||||
top: 0 !important;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
.drawflow .drawflow-node .output {
|
|
||||||
right: 0 !important;
|
|
||||||
top: 0 !important;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Port dots */
|
|
||||||
.drawflow .drawflow-node .input,
|
.drawflow .drawflow-node .input,
|
||||||
.drawflow .drawflow-node .output {
|
.drawflow .drawflow-node .output {
|
||||||
width: 12px;
|
width: 11px; height: 11px;
|
||||||
height: 12px;
|
|
||||||
border: 2px solid #94a3b8;
|
border: 2px solid #94a3b8;
|
||||||
background: white;
|
background: white;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
.drawflow .drawflow-node .input:hover,
|
.drawflow .drawflow-node .input:hover,
|
||||||
.drawflow .drawflow-node .output:hover {
|
.drawflow .drawflow-node .output:hover {
|
||||||
background: #3b82f6;
|
background: #3b82f6; border-color: #3b82f6;
|
||||||
border-color: #3b82f6;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Output port labels (below each port) */
|
/* Output port option label */
|
||||||
.drawflow .drawflow-node .output[data-label]::after {
|
.drawflow .drawflow-node .output[data-label]::after {
|
||||||
content: attr(data-label);
|
content: attr(data-label);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 50%;
|
left: 13px; top: 50%;
|
||||||
top: 14px;
|
transform: translateY(-50%);
|
||||||
transform: translateX(-50%);
|
font-size: 9px;
|
||||||
font-size: 10px;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
color: #1e40af;
|
color: #1e40af;
|
||||||
background: #dbeafe;
|
background: #dbeafe;
|
||||||
padding: 0 5px;
|
padding: 0 4px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dark mode */
|
/* ── Dark mode ── */
|
||||||
.dark #drawflow {
|
.dark #drawflow {
|
||||||
background-color: #111827;
|
background-color: #111827;
|
||||||
background-image: radial-gradient(circle, #1f2937 1px, transparent 1px);
|
background-image: radial-gradient(circle, #1f2937 1px, transparent 1px);
|
||||||
}
|
}
|
||||||
|
.dark .drawflow .drawflow-node { background: #1f2937; border-color: #374151; }
|
||||||
.dark .drawflow .drawflow-node {
|
.dark .df-node-header { color: #e5e7eb; }
|
||||||
background: #1f2937;
|
.dark .df-node-actions { border-top-color: #374151; }
|
||||||
border-color: #374151;
|
.dark .drawflow .connection .main-path { stroke: #6b7280; }
|
||||||
}
|
|
||||||
|
|
||||||
.dark .df-node-header {
|
|
||||||
color: #e5e7eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .df-node-url {
|
|
||||||
color: #6b7280;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .df-node-actions {
|
|
||||||
border-top-color: #374151;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .drawflow .connection .main-path {
|
|
||||||
stroke: #6b7280;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .drawflow .drawflow-node .input,
|
.dark .drawflow .drawflow-node .input,
|
||||||
.dark .drawflow .drawflow-node .output {
|
.dark .drawflow .drawflow-node .output { border-color: #6b7280; background: #1f2937; }
|
||||||
border-color: #6b7280;
|
.dark .drawflow .drawflow-node .output[data-label]::after { color: #93c5fd; background: #1e3a5f; }
|
||||||
background: #1f2937;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .drawflow .drawflow-node .output[data-label]::after {
|
|
||||||
color: #93c5fd;
|
|
||||||
background: #1e3a5f;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function initFlowEditor() {
|
(function () {
|
||||||
|
const componentId = @js($this->getId());
|
||||||
|
|
||||||
|
function escHtml(str) {
|
||||||
|
return String(str)
|
||||||
|
.replace(/&/g,'&').replace(/</g,'<')
|
||||||
|
.replace(/>/g,'>').replace(/"/g,'"');
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureArrowMarker() {
|
||||||
|
if (document.getElementById('df-arrow-defs')) return;
|
||||||
|
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||||
|
svg.setAttribute('id', 'df-arrow-defs');
|
||||||
|
svg.style.cssText = 'position:absolute;width:0;height:0;overflow:hidden';
|
||||||
|
svg.innerHTML = `<defs>
|
||||||
|
<marker id="arrowhead" markerWidth="8" markerHeight="6"
|
||||||
|
refX="7" refY="3" orient="auto" markerUnits="strokeWidth">
|
||||||
|
<polygon points="0 0, 8 3, 0 6" fill="#64748b"/>
|
||||||
|
</marker></defs>`;
|
||||||
|
document.body.appendChild(svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Build / rebuild the Drawflow graph ───────────────────────────────
|
||||||
|
function buildGraph(nodes, edges) {
|
||||||
const container = document.getElementById('drawflow');
|
const container = document.getElementById('drawflow');
|
||||||
if (!container || container._dfInit) return;
|
if (!container) return;
|
||||||
container._dfInit = true;
|
|
||||||
|
// 销毁旧实例
|
||||||
|
if (window._dfEditor) {
|
||||||
|
try { window._dfEditor.clear(); } catch(e) {}
|
||||||
|
window._dfEditor = null;
|
||||||
|
}
|
||||||
|
container.innerHTML = '';
|
||||||
|
|
||||||
|
ensureArrowMarker();
|
||||||
|
|
||||||
const editor = new Drawflow(container);
|
const editor = new Drawflow(container);
|
||||||
editor.reroute = false;
|
editor.reroute = false;
|
||||||
editor.curvature = 0.5;
|
editor.curvature = 0.5;
|
||||||
editor.start();
|
editor.start();
|
||||||
|
|
||||||
// Override path rendering for vertical (top-down) flow
|
// 水平贝塞尔曲线
|
||||||
editor.createCurvature = function(start_x, start_y, end_x, end_y, curvature, type) {
|
editor.createCurvature = function(sx, sy, ex, ey, curvature) {
|
||||||
const dy = Math.abs(end_y - start_y);
|
const ox = Math.max(Math.abs(ex - sx) * curvature, 30);
|
||||||
const offsetY = Math.max(dy * curvature, 50);
|
return `M ${sx} ${sy} C ${sx+ox} ${sy} ${ex-ox} ${ey} ${ex} ${ey}`;
|
||||||
return ` M ${start_x} ${start_y} C ${start_x} ${start_y + offsetY} ${end_x} ${end_y - offsetY} ${end_x} ${end_y}`;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
window._dfEditor = editor;
|
window._dfEditor = editor;
|
||||||
|
|
||||||
const nodes = @js($this-> nodes);
|
|
||||||
const edges = @js($this-> edges);
|
|
||||||
const pageIdToNodeId = {};
|
const pageIdToNodeId = {};
|
||||||
|
|
||||||
// Add nodes - output count = options.length or 1 (default)
|
// 添加节点
|
||||||
nodes.forEach(node => {
|
nodes.forEach(node => {
|
||||||
const numOutputs = (node.options && node.options.length > 0) ? node.options.length : 1;
|
const numOutputs = (node.options && node.options.length > 0) ? node.options.length : 1;
|
||||||
|
const html = `<div class="df-node-content">
|
||||||
const html = `
|
<div class="df-node-header">${escHtml(node.title)}</div>
|
||||||
<div class="df-node-content">
|
${node.is_entry ? '<div><span class="df-node-badge">入口</span></div>' : ''}
|
||||||
<div class="df-node-header">${node.title}</div>
|
<div class="df-node-actions">
|
||||||
<div class="df-node-url">${node.uri}</div>
|
<button onclick="event.stopPropagation();Livewire.find('${componentId}').mountAction('editPage',{id:${node.id}})">编辑</button>
|
||||||
${node.is_entry ? '<div><span class="df-node-badge">入口</span></div>' : ''}
|
<button class="btn-danger" onclick="event.stopPropagation();Livewire.find('${componentId}').mountAction('deletePage',{id:${node.id}})">删除</button>
|
||||||
<div class="df-node-actions">
|
<a href="${escHtml(node.uri)}" target="_blank" rel="noopener" onclick="event.stopPropagation()">预览</a>
|
||||||
<button onclick="event.stopPropagation(); Livewire.find('${@js($this->getId())}').mountAction('editPage', { id: ${node.id} })">编辑</button>
|
</div></div>`;
|
||||||
<button class="btn-danger" onclick="event.stopPropagation(); Livewire.find('${@js($this->getId())}').mountAction('deletePage', { id: ${node.id} })">删除</button>
|
|
||||||
<a href="${node.uri}" target="_blank" rel="noopener" onclick="event.stopPropagation()">预览</a>
|
|
||||||
</div>
|
|
||||||
</div>`;
|
|
||||||
|
|
||||||
const nodeId = editor.addNode(
|
const nodeId = editor.addNode(
|
||||||
'page', 1, numOutputs,
|
'page', 1, numOutputs,
|
||||||
node.x, node.y,
|
node.x, node.y,
|
||||||
node.is_entry ? 'entry-node' : '', {
|
node.is_entry ? 'entry-node' : '',
|
||||||
pageId: node.id,
|
{ pageId: node.id, options: node.options || [] },
|
||||||
options: node.options || []
|
|
||||||
},
|
|
||||||
html
|
html
|
||||||
);
|
);
|
||||||
pageIdToNodeId[node.id] = nodeId;
|
pageIdToNodeId[node.id] = nodeId;
|
||||||
|
|
||||||
// Label output ports with option names
|
// 给输出端口加选项标签
|
||||||
if (node.options && node.options.length > 0) {
|
if (node.options && node.options.length > 0) {
|
||||||
setTimeout(() => {
|
requestAnimationFrame(() => {
|
||||||
const nodeEl = container.querySelector(`#node-${nodeId}`);
|
const nodeEl = container.querySelector(`#node-${nodeId}`);
|
||||||
if (!nodeEl) return;
|
if (!nodeEl) return;
|
||||||
const outputs = nodeEl.querySelectorAll('.output');
|
nodeEl.querySelectorAll('.output').forEach((el, i) => {
|
||||||
outputs.forEach((el, i) => {
|
|
||||||
if (node.options[i]) {
|
if (node.options[i]) {
|
||||||
el.setAttribute('data-label', node.options[i]);
|
el.setAttribute('data-label', node.options[i]);
|
||||||
el.title = node.options[i];
|
el.title = node.options[i];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, 0);
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add edges - map to correct output port based on label
|
// 添加连线
|
||||||
edges.forEach(edge => {
|
edges.forEach(edge => {
|
||||||
const fromNodeId = pageIdToNodeId[edge.from];
|
const fromNodeId = pageIdToNodeId[edge.from];
|
||||||
const toNodeId = pageIdToNodeId[edge.to];
|
const toNodeId = pageIdToNodeId[edge.to];
|
||||||
if (!fromNodeId || !toNodeId) return;
|
if (!fromNodeId || !toNodeId) return;
|
||||||
|
|
||||||
// Find which output port matches this edge's label
|
|
||||||
const fromNode = nodes.find(n => n.id === edge.from);
|
const fromNode = nodes.find(n => n.id === edge.from);
|
||||||
let outputClass = 'output_1';
|
let outputClass = 'output_1';
|
||||||
if (fromNode && fromNode.options && fromNode.options.length > 0 && edge.label) {
|
if (fromNode && fromNode.options && fromNode.options.length > 0 && edge.label) {
|
||||||
const idx = fromNode.options.indexOf(edge.label);
|
const idx = fromNode.options.indexOf(edge.label);
|
||||||
if (idx >= 0) {
|
if (idx >= 0) outputClass = `output_${idx + 1}`;
|
||||||
outputClass = `output_${idx + 1}`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
try { editor.addConnection(fromNodeId, toNodeId, outputClass, 'input_1'); } catch(e) {}
|
||||||
editor.addConnection(fromNodeId, toNodeId, outputClass, 'input_1');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Events → Livewire
|
// Drawflow 事件 → Livewire(只在本次 editor 实例上绑定)
|
||||||
let ignoreEvents = false;
|
editor.on('connectionCreated', info => {
|
||||||
|
const from = editor.getNodeFromId(info.output_id);
|
||||||
editor.on('connectionCreated', (info) => {
|
const to = editor.getNodeFromId(info.input_id);
|
||||||
if (ignoreEvents) return;
|
if (from && to) {
|
||||||
const fromData = editor.getNodeFromId(info.output_id);
|
Livewire.find(componentId).call('addEdge', from.data.pageId, to.data.pageId, info.output_class);
|
||||||
const toData = editor.getNodeFromId(info.input_id);
|
|
||||||
if (fromData && toData) {
|
|
||||||
@this.call('addEdge', fromData.data.pageId, toData.data.pageId, info.output_class);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
editor.on('connectionRemoved', (info) => {
|
editor.on('connectionRemoved', info => {
|
||||||
if (ignoreEvents) return;
|
const from = editor.getNodeFromId(info.output_id);
|
||||||
const fromData = editor.getNodeFromId(info.output_id);
|
const to = editor.getNodeFromId(info.input_id);
|
||||||
const toData = editor.getNodeFromId(info.input_id);
|
if (from && to) {
|
||||||
if (fromData && toData) {
|
Livewire.find(componentId).call('removeEdge', from.data.pageId, to.data.pageId);
|
||||||
@this.call('removeEdge', fromData.data.pageId, toData.data.pageId);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Livewire graph refresh
|
|
||||||
Livewire.on('graphUpdated', () => {
|
|
||||||
ignoreEvents = true;
|
|
||||||
container._dfInit = false;
|
|
||||||
editor.clear();
|
|
||||||
setTimeout(() => {
|
|
||||||
container._dfInit = false;
|
|
||||||
container.innerHTML = '';
|
|
||||||
window._dfEditor = null;
|
|
||||||
initFlowEditor();
|
|
||||||
}, 100);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize on page load and SPA navigation
|
// ── Livewire 事件监听(只注册一次,在外层)────────────────────────────
|
||||||
document.addEventListener('DOMContentLoaded', () => setTimeout(initFlowEditor, 50));
|
function registerLivewireListener() {
|
||||||
document.addEventListener('livewire:navigated', () => setTimeout(initFlowEditor, 50));
|
Livewire.on('graphUpdated', (payload) => {
|
||||||
|
// Livewire 3 dispatch 带命名参数时,payload 是 { nodes: [...], edges: [...] }
|
||||||
|
// 也兼容数组包裹的格式
|
||||||
|
let data = payload;
|
||||||
|
if (Array.isArray(payload)) data = payload[0] ?? {};
|
||||||
|
const nodes = data.nodes ?? null;
|
||||||
|
const edges = data.edges ?? null;
|
||||||
|
if (nodes !== null && edges !== null) {
|
||||||
|
buildGraph(nodes, edges);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 初始化 ────────────────────────────────────────────────────────────
|
||||||
|
function init() {
|
||||||
|
buildGraph(
|
||||||
|
@js($this->nodes),
|
||||||
|
@js($this->edges)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
registerLivewireListener();
|
||||||
|
init();
|
||||||
|
}, 50);
|
||||||
|
});
|
||||||
|
document.addEventListener('livewire:navigated', () => {
|
||||||
|
setTimeout(init, 50);
|
||||||
|
});
|
||||||
|
})();
|
||||||
</script>
|
</script>
|
||||||
</x-filament-panels::page>
|
</x-filament-panels::page>
|
||||||
|
|||||||
@@ -26,6 +26,15 @@
|
|||||||
article ul, article ol { padding-left: 28px; margin: 12px 0; }
|
article ul, article ol { padding-left: 28px; margin: 12px 0; }
|
||||||
article li { margin: 4px 0; }
|
article li { margin: 4px 0; }
|
||||||
article img { max-width: 100%; height: auto; border-radius: 8px; margin: 12px 0; }
|
article img { max-width: 100%; height: auto; border-radius: 8px; margin: 12px 0; }
|
||||||
|
article figure { margin: 12px 0; }
|
||||||
|
article figure img { margin: 0; }
|
||||||
|
article figcaption { display: none; }
|
||||||
|
/* 隐藏 Trix 富文本编辑器生成的图片附件说明文字 */
|
||||||
|
article figure figcaption,
|
||||||
|
article .attachment__caption,
|
||||||
|
article .attachment__name,
|
||||||
|
article .attachment__size,
|
||||||
|
article action-text-attachment figcaption { display: none; }
|
||||||
article a { color: #2563eb; text-decoration: underline; }
|
article a { color: #2563eb; text-decoration: underline; }
|
||||||
article blockquote {
|
article blockquote {
|
||||||
border-left: 4px solid #cbd5e1;
|
border-left: 4px solid #cbd5e1;
|
||||||
|
|||||||
Reference in New Issue
Block a user