Files
KnowledgeBase/resources/views/filament/resources/guide/manage-pages.blade.php
2026-03-24 15:33:10 +08:00

353 lines
11 KiB
PHP

<x-filament-panels::page>
<link rel="stylesheet" href="{{ asset('vendor/drawflow/drawflow.min.css') }}">
<script src="{{ asset('vendor/drawflow/drawflow.min.js') }}"></script>
<div class="space-y-4">
{{-- Header actions --}}
<div class="flex gap-2">
{{ $this->createPageAction }}
</div>
{{-- Drawflow canvas --}}
<div class="fi-section rounded-xl bg-white shadow-sm ring-1 ring-gray-950/5 dark:bg-gray-900 dark:ring-white/10 overflow-hidden">
<div id="drawflow" wire:ignore></div>
</div>
</div>
<x-filament-actions::modals />
<style>
#drawflow {
width: 100%;
height: 600px;
background-color: #f8fafc;
background-image: radial-gradient(circle, #e2e8f0 1px, transparent 1px);
background-size: 20px 20px;
}
/* Node styling */
.drawflow .drawflow-node {
border-radius: 12px;
border: 1px solid #e5e7eb;
background: white;
min-width: 200px;
padding: 0;
}
.drawflow .drawflow-node.selected {
border-color: #f59e0b;
box-shadow: 0 0 0 2px rgba(245, 158, 11, 0.3);
}
.drawflow .drawflow-node.entry-node {
border: 2px solid #f59e0b;
}
.drawflow .drawflow-node .drawflow_content_node {
padding: 0;
}
/* Node content */
.df-node-content {
padding: 10px 12px;
}
.df-node-header {
font-weight: 600;
font-size: 13px;
color: #1f2937;
margin-bottom: 2px;
line-height: 1.3;
}
.df-node-url {
font-size: 11px;
color: #9ca3af;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 200px;
}
.df-node-badge {
display: inline-block;
font-size: 10px;
background: #fef3c7;
color: #b45309;
padding: 1px 6px;
border-radius: 4px;
margin-top: 4px;
font-weight: 500;
}
.df-node-actions {
margin-top: 6px;
display: flex;
gap: 8px;
border-top: 1px solid #f3f4f6;
padding-top: 6px;
}
.df-node-actions button {
font-size: 11px;
color: #6b7280;
cursor: pointer;
background: none;
border: none;
padding: 2px 0;
transition: color 0.15s;
}
.df-node-actions button:hover {
color: #3b82f6;
}
.df-node-actions button.btn-danger:hover {
color: #ef4444;
}
/* Connection styling */
.drawflow .connection .main-path {
stroke: #94a3b8;
stroke-width: 2px;
}
.drawflow .connection .main-path:hover {
stroke: #ef4444;
stroke-width: 3px;
}
/* Port containers: top (input) / bottom (output) */
.drawflow .drawflow-node .inputs {
position: absolute;
top: -6px;
left: 0;
right: 0;
width: auto !important;
display: flex;
flex-direction: row;
justify-content: center;
}
.drawflow .drawflow-node .outputs {
position: absolute;
bottom: -6px;
left: 0;
right: 0;
width: auto !important;
display: flex;
flex-direction: row;
justify-content: space-evenly;
gap: 24px;
padding: 0 20px;
}
/* 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 .output {
width: 12px;
height: 12px;
border: 2px solid #94a3b8;
background: white;
position: relative;
}
.drawflow .drawflow-node .input:hover,
.drawflow .drawflow-node .output:hover {
background: #3b82f6;
border-color: #3b82f6;
}
/* Output port labels (below each port) */
.drawflow .drawflow-node .output[data-label]::after {
content: attr(data-label);
position: absolute;
left: 50%;
top: 14px;
transform: translateX(-50%);
font-size: 10px;
white-space: nowrap;
color: #1e40af;
background: #dbeafe;
padding: 0 5px;
border-radius: 3px;
font-weight: 500;
pointer-events: none;
line-height: 1.6;
}
/* Dark mode */
.dark #drawflow {
background-color: #111827;
background-image: radial-gradient(circle, #1f2937 1px, transparent 1px);
}
.dark .drawflow .drawflow-node {
background: #1f2937;
border-color: #374151;
}
.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 .output {
border-color: #6b7280;
background: #1f2937;
}
.dark .drawflow .drawflow-node .output[data-label]::after {
color: #93c5fd;
background: #1e3a5f;
}
</style>
<script>
function initFlowEditor() {
const container = document.getElementById('drawflow');
if (!container || container._dfInit) return;
container._dfInit = true;
const editor = new Drawflow(container);
editor.reroute = false;
editor.curvature = 0.5;
editor.start();
// Override path rendering for vertical (top-down) flow
editor.createCurvature = function(start_x, start_y, end_x, end_y, curvature, type) {
const dy = Math.abs(end_y - start_y);
const offsetY = Math.max(dy * curvature, 50);
return ` M ${start_x} ${start_y} C ${start_x} ${start_y + offsetY} ${end_x} ${end_y - offsetY} ${end_x} ${end_y}`;
};
window._dfEditor = editor;
const nodes = @js($this-> nodes);
const edges = @js($this-> edges);
const pageIdToNodeId = {};
// Add nodes - output count = options.length or 1 (default)
nodes.forEach(node => {
const numOutputs = (node.options && node.options.length > 0) ? node.options.length : 1;
const html = `
<div class="df-node-content">
<div class="df-node-header">${node.title}</div>
<div class="df-node-url">${node.html_url}</div>
${node.is_entry ? '<div><span class="df-node-badge">入口</span></div>' : ''}
<div class="df-node-actions">
<button onclick="event.stopPropagation(); Livewire.find('${@js($this->getId())}').mountAction('editPage', { id: ${node.id} })">编辑</button>
<button class="btn-danger" onclick="event.stopPropagation(); Livewire.find('${@js($this->getId())}').mountAction('deletePage', { id: ${node.id} })">删除</button>
</div>
</div>`;
const nodeId = editor.addNode(
'page', 1, numOutputs,
node.x, node.y,
node.is_entry ? 'entry-node' : '', {
pageId: node.id,
options: node.options || []
},
html
);
pageIdToNodeId[node.id] = nodeId;
// Label output ports with option names
if (node.options && node.options.length > 0) {
setTimeout(() => {
const nodeEl = container.querySelector(`#node-${nodeId}`);
if (!nodeEl) return;
const outputs = nodeEl.querySelectorAll('.output');
outputs.forEach((el, i) => {
if (node.options[i]) {
el.setAttribute('data-label', node.options[i]);
el.title = node.options[i];
}
});
}, 0);
}
});
// Add edges - map to correct output port based on label
edges.forEach(edge => {
const fromNodeId = pageIdToNodeId[edge.from];
const toNodeId = pageIdToNodeId[edge.to];
if (!fromNodeId || !toNodeId) return;
// Find which output port matches this edge's label
const fromNode = nodes.find(n => n.id === edge.from);
let outputClass = 'output_1';
if (fromNode && fromNode.options && fromNode.options.length > 0 && edge.label) {
const idx = fromNode.options.indexOf(edge.label);
if (idx >= 0) {
outputClass = `output_${idx + 1}`;
}
}
editor.addConnection(fromNodeId, toNodeId, outputClass, 'input_1');
});
// Events → Livewire
let ignoreEvents = false;
editor.on('connectionCreated', (info) => {
if (ignoreEvents) return;
const fromData = editor.getNodeFromId(info.output_id);
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) => {
if (ignoreEvents) return;
const fromData = editor.getNodeFromId(info.output_id);
const toData = editor.getNodeFromId(info.input_id);
if (fromData && toData) {
@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
document.addEventListener('DOMContentLoaded', () => setTimeout(initFlowEditor, 50));
document.addEventListener('livewire:navigated', () => setTimeout(initFlowEditor, 50));
</script>
</x-filament-panels::page>