fix: tree & guide
This commit is contained in:
@@ -1,26 +1,27 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>{{ $document->title }} - Markdown 预览</title>
|
||||
|
||||
|
||||
<!-- Tailwind CSS -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
|
||||
@vite(['resources/css/app.css'])
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
background-color: #f3f4f6;
|
||||
}
|
||||
|
||||
|
||||
.preview-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
|
||||
.preview-header {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
@@ -28,7 +29,7 @@
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
|
||||
.preview-content {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
@@ -36,45 +37,46 @@
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
|
||||
.empty-state-icon {
|
||||
font-size: 64px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
|
||||
@media print {
|
||||
.preview-header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
.preview-content {
|
||||
box-shadow: none;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* 移动端优化 */
|
||||
@media (max-width: 768px) {
|
||||
.preview-container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
|
||||
.preview-header {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
|
||||
.preview-content {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="preview-container">
|
||||
<!-- 头部信息 -->
|
||||
@@ -82,7 +84,7 @@
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
<div class="flex-1">
|
||||
<h1 class="text-2xl font-bold text-gray-900 mb-2">{{ $document->title }}</h1>
|
||||
|
||||
|
||||
<div class="flex flex-wrap gap-4 text-sm text-gray-600">
|
||||
<div class="flex items-center gap-1">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -90,7 +92,7 @@
|
||||
</svg>
|
||||
<span>{{ $document->type === 'global' ? '全局知识库' : '专用知识库' }}</span>
|
||||
</div>
|
||||
|
||||
|
||||
@if($document->group)
|
||||
<div class="flex items-center gap-1">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -99,14 +101,14 @@
|
||||
<span>{{ $document->group->name }}</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
|
||||
<div class="flex items-center gap-1">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path>
|
||||
</svg>
|
||||
<span>{{ $document->uploader->name }}</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex items-center gap-1">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
|
||||
@@ -114,23 +116,23 @@
|
||||
<span>{{ $document->created_at->format('Y年m月d日 H:i') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@if($document->description)
|
||||
<p class="mt-3 text-gray-700">{{ $document->description }}</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex gap-2">
|
||||
<a href="{{ route('documents.download', $document) }}"
|
||||
class="inline-flex items-center gap-2 px-4 py-2 bg-green-600 hover:bg-green-700 text-white font-medium rounded-lg transition-colors">
|
||||
<a href="{{ route('documents.download', $document) }}"
|
||||
class="inline-flex items-center gap-2 px-4 py-2 bg-green-600 hover:bg-green-700 text-white font-medium rounded-lg transition-colors">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path>
|
||||
</svg>
|
||||
<span>下载原文档</span>
|
||||
</a>
|
||||
|
||||
<button onclick="window.print()"
|
||||
class="inline-flex items-center gap-2 px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white font-medium rounded-lg transition-colors">
|
||||
|
||||
<button onclick="window.print()"
|
||||
class="inline-flex items-center gap-2 px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white font-medium rounded-lg transition-colors">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z"></path>
|
||||
</svg>
|
||||
@@ -139,26 +141,27 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Markdown 内容 -->
|
||||
<div class="preview-content">
|
||||
@if($markdownHtml)
|
||||
{!! $markdownHtml !!}
|
||||
{!! $markdownHtml !!}
|
||||
@else
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon">📄</div>
|
||||
<h2 class="text-xl font-semibold text-gray-700 mb-2">Markdown 内容为空</h2>
|
||||
<p class="text-gray-600 mb-6">该文档的 Markdown 内容尚未生成或为空</p>
|
||||
<a href="{{ route('documents.download', $document) }}"
|
||||
class="inline-flex items-center gap-2 px-6 py-3 bg-green-600 hover:bg-green-700 text-white font-medium rounded-lg transition-colors">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path>
|
||||
</svg>
|
||||
<span>下载原始文档</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon">📄</div>
|
||||
<h2 class="text-xl font-semibold text-gray-700 mb-2">Markdown 内容为空</h2>
|
||||
<p class="text-gray-600 mb-6">该文档的 Markdown 内容尚未生成或为空</p>
|
||||
<a href="{{ route('documents.download', $document) }}"
|
||||
class="inline-flex items-center gap-2 px-6 py-3 bg-green-600 hover:bg-green-700 text-white font-medium rounded-lg transition-colors">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path>
|
||||
</svg>
|
||||
<span>下载原始文档</span>
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
352
resources/views/filament/resources/guide/manage-pages.blade.php
Normal file
352
resources/views/filament/resources/guide/manage-pages.blade.php
Normal file
@@ -0,0 +1,352 @@
|
||||
<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>
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user