fix: tree & guide
This commit is contained in:
@@ -305,7 +305,7 @@ class DocumentResource extends Resource
|
||||
public static function formatFileSize(?int $bytes): string
|
||||
{
|
||||
if ($bytes === null) {
|
||||
return '—';
|
||||
return '-';
|
||||
}
|
||||
|
||||
$units = ['B', 'KB', 'MB', 'GB'];
|
||||
|
||||
@@ -3,47 +3,259 @@
|
||||
namespace App\Filament\Resources\GuideResource\Pages;
|
||||
|
||||
use App\Filament\Resources\GuideResource;
|
||||
use App\Models\Guide;
|
||||
use App\Models\GuidePage;
|
||||
use App\Models\GuidePageEdge;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Forms;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use SolutionForest\FilamentTree\Actions;
|
||||
use SolutionForest\FilamentTree\Resources\Pages\TreePage;
|
||||
use Filament\Resources\Pages\Concerns\InteractsWithRecord;
|
||||
use Filament\Resources\Pages\Page;
|
||||
|
||||
class ManageGuidePages extends TreePage
|
||||
class ManageGuidePages extends Page
|
||||
{
|
||||
use InteractsWithRecord;
|
||||
|
||||
protected static string $resource = GuideResource::class;
|
||||
|
||||
protected ?string $treeTitle = '指引页面';
|
||||
protected bool $enableTreeTitle = true;
|
||||
protected static string $view = 'filament.resources.guide.manage-pages';
|
||||
|
||||
protected static string $model = GuidePage::class;
|
||||
public array $nodes = [];
|
||||
|
||||
protected function getTreeQuery(): Builder
|
||||
public array $edges = [];
|
||||
|
||||
public function mount(int|string $record): void
|
||||
{
|
||||
return GuidePage::query()
|
||||
->where('guide_id', $this->getOwnerRecord()->id);
|
||||
$this->record = $this->resolveRecord($record);
|
||||
$this->loadGraph();
|
||||
}
|
||||
|
||||
public function getTreeRecordTitle(?Model $record = null): string
|
||||
public function getTitle(): string
|
||||
{
|
||||
if (!$record) {
|
||||
return '';
|
||||
return $this->getRecord()->name . ' - 页面流程';
|
||||
}
|
||||
|
||||
public function loadGraph(): void
|
||||
{
|
||||
$pages = $this->getRecord()->pages()->get();
|
||||
$edgeModels = $this->getRecord()->edges()->orderBy('sort')->get();
|
||||
|
||||
// Build adjacency list
|
||||
$children = [];
|
||||
foreach ($pages as $p) {
|
||||
$children[$p->id] = [];
|
||||
}
|
||||
$prefix = $record->branch_option ? "[{$record->branch_option}] " : '';
|
||||
$suffix = !empty($record->options) ? ' 📋' : '';
|
||||
return $prefix . $record->title . $suffix;
|
||||
foreach ($edgeModels as $e) {
|
||||
if (isset($children[$e->from_page_id])) {
|
||||
$children[$e->from_page_id][] = $e->to_page_id;
|
||||
}
|
||||
}
|
||||
|
||||
// BFS from entry nodes (no incoming edges) to assign levels
|
||||
$hasIncoming = array_flip($edgeModels->pluck('to_page_id')->toArray());
|
||||
$levels = [];
|
||||
$visited = [];
|
||||
$queue = [];
|
||||
|
||||
foreach ($pages as $p) {
|
||||
if (!isset($hasIncoming[$p->id])) {
|
||||
$queue[] = $p->id;
|
||||
$levels[$p->id] = 0;
|
||||
$visited[$p->id] = true;
|
||||
}
|
||||
}
|
||||
|
||||
while (!empty($queue)) {
|
||||
$cur = array_shift($queue);
|
||||
foreach ($children[$cur] ?? [] as $child) {
|
||||
if (!isset($visited[$child])) {
|
||||
$visited[$child] = true;
|
||||
$levels[$child] = $levels[$cur] + 1;
|
||||
$queue[] = $child;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Orphans at bottom
|
||||
$maxLevel = empty($levels) ? 0 : max($levels);
|
||||
foreach ($pages as $p) {
|
||||
if (!isset($levels[$p->id])) {
|
||||
$levels[$p->id] = $maxLevel + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Group by level
|
||||
$levelGroups = [];
|
||||
foreach ($pages as $p) {
|
||||
$levelGroups[$levels[$p->id]][] = $p->id;
|
||||
}
|
||||
ksort($levelGroups);
|
||||
|
||||
// Compute positions: center each level horizontally, stack vertically
|
||||
$nodeWidth = 240;
|
||||
$gapX = 40;
|
||||
$gapY = 150;
|
||||
$positions = [];
|
||||
|
||||
foreach ($levelGroups as $level => $ids) {
|
||||
$count = count($ids);
|
||||
$totalWidth = $count * $nodeWidth + ($count - 1) * $gapX;
|
||||
$startX = max(20, (800 - $totalWidth) / 2);
|
||||
foreach ($ids as $i => $id) {
|
||||
$positions[$id] = [
|
||||
'x' => (int) ($startX + $i * ($nodeWidth + $gapX)),
|
||||
'y' => 40 + $level * $gapY,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$this->nodes = $pages->map(fn(GuidePage $p) => [
|
||||
'id' => $p->id,
|
||||
'title' => $p->title,
|
||||
'html_url' => $p->html_url,
|
||||
'is_entry' => !isset($hasIncoming[$p->id]),
|
||||
'options' => $p->options ?? [],
|
||||
'x' => $positions[$p->id]['x'] ?? 50,
|
||||
'y' => $positions[$p->id]['y'] ?? 50,
|
||||
])->values()->toArray();
|
||||
|
||||
$this->edges = $edgeModels->map(fn(GuidePageEdge $e) => [
|
||||
'id' => $e->id,
|
||||
'from' => $e->from_page_id,
|
||||
'to' => $e->to_page_id,
|
||||
'label' => $e->label,
|
||||
])->values()->toArray();
|
||||
}
|
||||
|
||||
protected function getFormSchema(): array
|
||||
// -- Livewire methods called by Drawflow events --
|
||||
|
||||
public function addEdge(int $fromPageId, int $toPageId, string $outputClass = 'output_1'): void
|
||||
{
|
||||
$guide = $this->getRecord();
|
||||
|
||||
if (
|
||||
!$guide->pages()->where('id', $fromPageId)->exists() ||
|
||||
!$guide->pages()->where('id', $toPageId)->exists()
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($fromPageId === $toPageId) {
|
||||
return;
|
||||
}
|
||||
|
||||
$exists = $guide->edges()
|
||||
->where('from_page_id', $fromPageId)
|
||||
->where('to_page_id', $toPageId)
|
||||
->exists();
|
||||
|
||||
if ($exists) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Derive label from output port → page options mapping
|
||||
$page = $guide->pages()->find($fromPageId);
|
||||
$options = $page->options ?? [];
|
||||
$label = null;
|
||||
|
||||
if (!empty($options)) {
|
||||
$outputIndex = (int) str_replace('output_', '', $outputClass) - 1;
|
||||
$label = $options[$outputIndex] ?? null;
|
||||
}
|
||||
|
||||
$guide->edges()->create([
|
||||
'from_page_id' => $fromPageId,
|
||||
'to_page_id' => $toPageId,
|
||||
'label' => $label,
|
||||
]);
|
||||
|
||||
$this->loadGraph();
|
||||
$this->dispatch('graphUpdated');
|
||||
}
|
||||
|
||||
public function removeEdge(int $fromPageId, int $toPageId): void
|
||||
{
|
||||
$this->getRecord()->edges()
|
||||
->where('from_page_id', $fromPageId)
|
||||
->where('to_page_id', $toPageId)
|
||||
->delete();
|
||||
|
||||
$this->loadGraph();
|
||||
$this->dispatch('graphUpdated');
|
||||
}
|
||||
|
||||
// -- Filament Actions --
|
||||
|
||||
public function createPageAction(): Action
|
||||
{
|
||||
return Action::make('createPage')
|
||||
->label('添加页面')
|
||||
->icon('heroicon-o-plus')
|
||||
->form($this->getPageFormSchema())
|
||||
->action(function (array $data): void {
|
||||
$this->getRecord()->pages()->create($data);
|
||||
|
||||
$this->loadGraph();
|
||||
$this->dispatch('graphUpdated');
|
||||
});
|
||||
}
|
||||
|
||||
public function editPageAction(): Action
|
||||
{
|
||||
return Action::make('editPage')
|
||||
->label('编辑页面')
|
||||
->icon('heroicon-o-pencil-square')
|
||||
->mountUsing(function (Forms\Form $form, array $arguments): void {
|
||||
$page = $this->getRecord()->pages()->findOrFail($arguments['id']);
|
||||
$form->fill([
|
||||
'title' => $page->title,
|
||||
'html_url' => $page->html_url,
|
||||
'options' => $page->options ?? [],
|
||||
]);
|
||||
})
|
||||
->form($this->getPageFormSchema())
|
||||
->action(function (array $data, array $arguments): void {
|
||||
$page = $this->getRecord()->pages()->findOrFail($arguments['id']);
|
||||
$page->update($data);
|
||||
|
||||
$this->loadGraph();
|
||||
$this->dispatch('graphUpdated');
|
||||
});
|
||||
}
|
||||
|
||||
public function deletePageAction(): Action
|
||||
{
|
||||
return Action::make('deletePage')
|
||||
->label('删除页面')
|
||||
->icon('heroicon-o-trash')
|
||||
->color('danger')
|
||||
->requiresConfirmation()
|
||||
->action(function (array $arguments): void {
|
||||
$page = $this->getRecord()->pages()->findOrFail($arguments['id']);
|
||||
$page->delete();
|
||||
|
||||
$this->loadGraph();
|
||||
$this->dispatch('graphUpdated');
|
||||
});
|
||||
}
|
||||
|
||||
public function deleteEdgeAction(): Action
|
||||
{
|
||||
return Action::make('deleteEdge')
|
||||
->label('删除连线')
|
||||
->icon('heroicon-o-trash')
|
||||
->color('danger')
|
||||
->requiresConfirmation()
|
||||
->action(function (array $arguments): void {
|
||||
$edge = $this->getRecord()->edges()->findOrFail($arguments['id']);
|
||||
$edge->delete();
|
||||
|
||||
$this->loadGraph();
|
||||
$this->dispatch('graphUpdated');
|
||||
});
|
||||
}
|
||||
|
||||
private function getPageFormSchema(): array
|
||||
{
|
||||
return [
|
||||
Forms\Components\TextInput::make('page_number')
|
||||
->label('页码')
|
||||
->numeric()
|
||||
->minValue(1),
|
||||
|
||||
Forms\Components\TextInput::make('title')
|
||||
->label('页面标题')
|
||||
->required()
|
||||
@@ -56,33 +268,8 @@ class ManageGuidePages extends TreePage
|
||||
->maxLength(500),
|
||||
|
||||
Forms\Components\TagsInput::make('options')
|
||||
->label('选项按钮')
|
||||
->helperText('此页面展示的选项按钮,如"前门12"、"后门"。留空=无分支。'),
|
||||
|
||||
Forms\Components\TextInput::make('branch_option')
|
||||
->label('所属分支选项')
|
||||
->maxLength(100)
|
||||
->helperText('此页面对应父页面的哪个选项值(根页面留空)'),
|
||||
->label('分支选项')
|
||||
->helperText('定义此页面的分支按钮(每个选项对应一个输出端口)。留空 = 顺序页面。'),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getTreeActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\ViewAction::make(),
|
||||
Actions\EditAction::make()->slideOver(),
|
||||
Actions\DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
|
||||
protected function mutateFormDataBeforeCreate(array $data): array
|
||||
{
|
||||
$data['guide_id'] = $this->getOwnerRecord()->id;
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function getOwnerRecord(): Guide
|
||||
{
|
||||
return Guide::findOrFail(request()->route('record'));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user