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'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace App\Http\Controllers\Api;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Guide;
|
||||
use App\Models\GuidePage;
|
||||
use App\Models\GuidePageEdge;
|
||||
use App\Services\KnowledgeContextService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -90,7 +91,7 @@ class TerminalApiController extends Controller
|
||||
|
||||
/**
|
||||
* POST /api/terminal/guides/pages
|
||||
* 组合多个指引的页面,返回递归树形结构
|
||||
* 返回指引页面(状态机格式,每页带 next 指针)
|
||||
*/
|
||||
public function guidePages(Request $request): JsonResponse
|
||||
{
|
||||
@@ -102,104 +103,54 @@ class TerminalApiController extends Controller
|
||||
$terminal = $request->attributes->get('terminal');
|
||||
$accessibleIds = $this->getTerminalGuides($terminal)->pluck('guides.id')->toArray();
|
||||
|
||||
$guideIds = $request->input('guide_ids');
|
||||
$pages = [];
|
||||
$guideIds = collect($request->input('guide_ids'))
|
||||
->intersect($accessibleIds)
|
||||
->values()
|
||||
->toArray();
|
||||
|
||||
foreach ($guideIds as $guideId) {
|
||||
if (!in_array($guideId, $accessibleIds)) {
|
||||
continue;
|
||||
$pages = GuidePage::whereIn('guide_id', $guideIds)->get();
|
||||
$edges = GuidePageEdge::whereIn('guide_id', $guideIds)
|
||||
->orderBy('from_page_id')
|
||||
->orderBy('sort')
|
||||
->get();
|
||||
|
||||
$edgesByFrom = $edges->groupBy('from_page_id');
|
||||
$hasIncoming = $edges->pluck('to_page_id')->unique()->flip();
|
||||
|
||||
$guides = [];
|
||||
foreach ($pages->groupBy('guide_id') as $guideId => $guidePages) {
|
||||
$entryPage = $guidePages->first(fn($p) => !$hasIncoming->has($p->id));
|
||||
|
||||
$pagesMap = [];
|
||||
foreach ($guidePages as $page) {
|
||||
$next = $edgesByFrom->get($page->id, collect())
|
||||
->map(function (GuidePageEdge $e) {
|
||||
$item = ['page_id' => $e->to_page_id];
|
||||
if ($e->label !== null) {
|
||||
$item['label'] = $e->label;
|
||||
}
|
||||
return $item;
|
||||
})->values()->toArray();
|
||||
|
||||
$pagesMap[$page->id] = [
|
||||
'id' => $page->id,
|
||||
'title' => $page->title,
|
||||
'html_url' => $page->html_url,
|
||||
'next' => $next,
|
||||
];
|
||||
}
|
||||
|
||||
$guide = Guide::with(
|
||||
$this->buildEagerLoadArray('trunkPages', 5)
|
||||
)->find($guideId);
|
||||
|
||||
if (!$guide) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($guide->trunkPages as $page) {
|
||||
$pages = array_merge($pages, $this->flattenSequentialPages($page, $guide->name, $guide->id));
|
||||
}
|
||||
$guides[$guideId] = [
|
||||
'entry_page_id' => $entryPage?->id,
|
||||
'pages' => $pagesMap,
|
||||
];
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'pages' => $pages,
|
||||
'total_pages' => count($pages),
|
||||
'guides' => $guides,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将树形页面结构展平:顺序节点(无 options)平铺,分支节点保留嵌套
|
||||
*/
|
||||
private function flattenSequentialPages(GuidePage $page, string $guideName, int $guideId): array
|
||||
{
|
||||
$data = [
|
||||
'id' => $page->id,
|
||||
'guide_id' => $guideId,
|
||||
'guide_name' => $guideName,
|
||||
'page_number' => $page->page_number,
|
||||
'title' => $page->title,
|
||||
'html_url' => $page->html_url,
|
||||
];
|
||||
|
||||
if ($page->options && $page->branchChildren->isNotEmpty()) {
|
||||
$data['options'] = $page->options;
|
||||
$branches = [];
|
||||
foreach ($page->branchChildren as $child) {
|
||||
$branches[$child->branch_option][] =
|
||||
$this->buildPageTree($child, $guideName, $guideId);
|
||||
}
|
||||
$data['branches'] = $branches;
|
||||
return [$data];
|
||||
}
|
||||
|
||||
if ($page->branchChildren->isNotEmpty()) {
|
||||
$result = [$data];
|
||||
foreach ($page->branchChildren as $child) {
|
||||
$result = array_merge($result, $this->flattenSequentialPages($child, $guideName, $guideId));
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
return [$data];
|
||||
}
|
||||
|
||||
private function buildPageTree(GuidePage $page, string $guideName, int $guideId): array
|
||||
{
|
||||
$data = [
|
||||
'id' => $page->id,
|
||||
'guide_id' => $guideId,
|
||||
'guide_name' => $guideName,
|
||||
'page_number' => $page->page_number,
|
||||
'title' => $page->title,
|
||||
'html_url' => $page->html_url,
|
||||
];
|
||||
|
||||
if ($page->options && $page->branchChildren->isNotEmpty()) {
|
||||
$data['options'] = $page->options;
|
||||
$branches = [];
|
||||
foreach ($page->branchChildren as $child) {
|
||||
$branches[$child->branch_option][] =
|
||||
$this->buildPageTree($child, $guideName, $guideId);
|
||||
}
|
||||
$data['branches'] = $branches;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function buildEagerLoadArray(string $base, int $depth): array
|
||||
{
|
||||
$loads = [$base => fn($q) => $q->orderBy('sort_order')];
|
||||
$current = $base;
|
||||
for ($i = 0; $i < $depth; $i++) {
|
||||
$current .= '.branchChildren';
|
||||
$loads[$current] = fn($q) => $q->orderBy('sort_order');
|
||||
}
|
||||
return $loads;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取终端可见的指引(线站关联 + 全局)
|
||||
*/
|
||||
@@ -210,7 +161,7 @@ class TerminalApiController extends Controller
|
||||
return Guide::published()->where(function ($q) use ($stationId) {
|
||||
$q->whereDoesntHave('stations'); // 全局指引
|
||||
if ($stationId) {
|
||||
$q->orWhereHas('stations', fn ($sq) => $sq->where('stations.id', $stationId));
|
||||
$q->orWhereHas('stations', fn($sq) => $sq->where('stations.id', $stationId));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -33,14 +33,22 @@ class Guide extends Model
|
||||
|
||||
public function pages()
|
||||
{
|
||||
return $this->hasMany(GuidePage::class)->orderBy('sort_order');
|
||||
return $this->hasMany(GuidePage::class);
|
||||
}
|
||||
|
||||
public function trunkPages()
|
||||
public function edges()
|
||||
{
|
||||
return $this->hasMany(GuidePage::class)
|
||||
->where('parent_id', -1)
|
||||
->orderBy('sort_order');
|
||||
return $this->hasMany(GuidePageEdge::class);
|
||||
}
|
||||
|
||||
public function entryPage()
|
||||
{
|
||||
return $this->hasOne(GuidePage::class)
|
||||
->whereNotIn('guide_pages.id', function ($q) {
|
||||
$q->select('to_page_id')
|
||||
->from('guide_page_edges')
|
||||
->whereColumn('guide_page_edges.guide_id', 'guide_pages.guide_id');
|
||||
});
|
||||
}
|
||||
|
||||
public function creator()
|
||||
@@ -76,7 +84,7 @@ class Guide extends Model
|
||||
|
||||
return $query->where(function (Builder $q) use ($stationIds) {
|
||||
$q->whereDoesntHave('stations')
|
||||
->orWhereHas('stations', fn ($sq) => $sq->whereIn('stations.id', $stationIds));
|
||||
->orWhereHas('stations', fn($sq) => $sq->whereIn('stations.id', $stationIds));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -3,42 +3,26 @@
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use SolutionForest\FilamentTree\Concern\ModelTree;
|
||||
|
||||
class GuidePage extends Model
|
||||
{
|
||||
use ModelTree;
|
||||
|
||||
protected $fillable = [
|
||||
'guide_id',
|
||||
'page_number',
|
||||
'title',
|
||||
'html_url',
|
||||
'sort_order',
|
||||
'parent_id',
|
||||
'options',
|
||||
'branch_option',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'options' => 'array',
|
||||
'parent_id' => 'int',
|
||||
];
|
||||
|
||||
// filament-tree column name mapping
|
||||
public function determineParentColumnName(): string
|
||||
protected static function booted(): void
|
||||
{
|
||||
return 'parent_id';
|
||||
}
|
||||
|
||||
public function determineOrderColumnName(): string
|
||||
{
|
||||
return 'sort_order';
|
||||
}
|
||||
|
||||
public function determineTitleColumnName(): string
|
||||
{
|
||||
return 'title';
|
||||
static::deleting(function (GuidePage $page) {
|
||||
// CASCADE on from_page_id is handled by FK, but incoming edges need cleanup
|
||||
GuidePageEdge::where('to_page_id', $page->id)->delete();
|
||||
});
|
||||
}
|
||||
|
||||
public function guide()
|
||||
@@ -46,13 +30,32 @@ class GuidePage extends Model
|
||||
return $this->belongsTo(Guide::class);
|
||||
}
|
||||
|
||||
public function branchChildren()
|
||||
public function outgoingEdges()
|
||||
{
|
||||
return $this->hasMany(self::class, 'parent_id')->orderBy('sort_order');
|
||||
return $this->hasMany(GuidePageEdge::class, 'from_page_id')->orderBy('sort');
|
||||
}
|
||||
|
||||
public function parentPage()
|
||||
public function incomingEdges()
|
||||
{
|
||||
return $this->belongsTo(self::class, 'parent_id');
|
||||
return $this->hasMany(GuidePageEdge::class, 'to_page_id');
|
||||
}
|
||||
|
||||
public function nextPages()
|
||||
{
|
||||
return $this->belongsToMany(self::class, 'guide_page_edges', 'from_page_id', 'to_page_id')
|
||||
->withPivot('label', 'sort')
|
||||
->orderByPivot('sort');
|
||||
}
|
||||
|
||||
public function previousPages()
|
||||
{
|
||||
return $this->belongsToMany(self::class, 'guide_page_edges', 'to_page_id', 'from_page_id')
|
||||
->withPivot('label', 'sort');
|
||||
}
|
||||
|
||||
public function isEntry(): bool
|
||||
{
|
||||
return !$this->incomingEdges()->exists();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
31
app/Models/GuidePageEdge.php
Normal file
31
app/Models/GuidePageEdge.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class GuidePageEdge extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'guide_id',
|
||||
'from_page_id',
|
||||
'to_page_id',
|
||||
'label',
|
||||
'sort',
|
||||
];
|
||||
|
||||
public function guide()
|
||||
{
|
||||
return $this->belongsTo(Guide::class);
|
||||
}
|
||||
|
||||
public function fromPage()
|
||||
{
|
||||
return $this->belongsTo(GuidePage::class, 'from_page_id');
|
||||
}
|
||||
|
||||
public function toPage()
|
||||
{
|
||||
return $this->belongsTo(GuidePage::class, 'to_page_id');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user