Skip to content

文章管理示例

完整的文章管理 CRUD,包含富文本编辑器、搜索、状态切换等功能。

迁移

php
Schema::create('posts', function (Blueprint $table) {
    $table->id();
    $table->string('title');
    $table->string('slug')->unique();
    $table->text('content');
    $table->string('cover')->nullable();
    $table->foreignId('category_id')->constrained();
    $table->foreignId('user_id')->constrained();
    $table->enum('status', ['draft', 'published'])->default('draft');
    $table->timestamp('published_at')->nullable();
    $table->timestamps();
});

模型

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    protected $fillable = [
        'title',
        'slug',
        'content',
        'cover',
        'category_id',
        'user_id',
        'status',
        'published_at',
    ];

    protected $casts = [
        'published_at' => 'datetime',
    ];

    public function category()
    {
        return $this->belongsTo(Category::class);
    }

    public function user()
    {
        return $this->belongsTo(User::class);
    }
}

控制器

php
<?php

namespace App\Http\Controllers;

use Lartrix\Controllers\CrudController;
use Lartrix\Schema\Components\NaiveUI\{Input, Select, SwitchC, Button, Space, Tag, Image};
use Lartrix\Schema\Components\Business\{CrudPage, OptForm, RichEditor};
use Lartrix\Schema\Actions\{SetAction, CallAction, FetchAction};
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;

class PostController extends CrudController
{
    protected function getModelClass(): string
    {
        return Post::class;
    }

    protected function getResourceName(): string
    {
        return '文章';
    }

    protected function getListWith(): array
    {
        return ['category', 'user'];
    }

    protected function listUi(): array
    {
        $schema = CrudPage::make('文章管理')
            ->apiPrefix('/blog/posts')
            ->columns([
                ['key' => 'id', 'title' => 'ID', 'width' => 80],
                [
                    'key' => 'cover',
                    'title' => '封面',
                    'width' => 100,
                    'slot' => [
                        Image::make()
                            ->props([
                                'src' => '{{ slotData.row.cover }}',
                                'width' => 60,
                                'height' => 60,
                                'objectFit' => 'cover',
                            ]),
                    ],
                ],
                ['key' => 'title', 'title' => '标题', 'ellipsis' => true],
                ['key' => 'category.name', 'title' => '分类', 'width' => 120],
                ['key' => 'user.name', 'title' => '作者', 'width' => 120],
                [
                    'key' => 'status',
                    'title' => '状态',
                    'width' => 100,
                    'slot' => [
                        SwitchC::make()
                            ->props([
                                'value' => '{{ slotData.row.status === "published" }}',
                                'checkedValue' => 'published',
                                'uncheckedValue' => 'draft',
                            ])
                            ->on('update:value', [
                                FetchAction::make('/blog/posts/{{ slotData.row.id }}')
                                    ->put()
                                    ->body([
                                        'action_type' => 'status',
                                        'status' => '{{ $event }}',
                                    ])
                                    ->then([
                                        CallAction::make('$message.success', ['状态更新成功']),
                                        CallAction::make('loadData'),
                                    ])
                                    ->catch([
                                        CallAction::make('$message.error', ['状态更新失败']),
                                    ]),
                            ]),
                    ],
                ],
                ['key' => 'published_at', 'title' => '发布时间', 'width' => 180],
                [
                    'key' => 'actions',
                    'title' => '操作',
                    'width' => 180,
                    'fixed' => 'right',
                    'slot' => [
                        Space::make()->children([
                            Button::make()
                                ->text('编辑')
                                ->type('primary')
                                ->size('small')
                                ->on('click', [
                                    SetAction::make('editId', '{{ slotData.row.id }}'),
                                    FetchAction::make('/blog/posts/{{ slotData.row.id }}')
                                        ->then([
                                            SetAction::make('formData', '{{ $response.data }}'),
                                            SetAction::make('formVisible', true),
                                        ]),
                                ]),
                            Button::make()
                                ->text('删除')
                                ->type('error')
                                ->size('small')
                                ->on('click', [
                                    CallAction::make('$dialog.warning', [
                                        '确认删除',
                                        '确定要删除这篇文章吗?',
                                        [
                                            'positiveText' => '确认删除',
                                            'negativeText' => '取消',
                                            'onPositiveClick' => [
                                                FetchAction::make('/blog/posts/{{ slotData.row.id }}')
                                                    ->delete()
                                                    ->then([
                                                        CallAction::make('$message.success', ['删除成功']),
                                                        CallAction::make('loadData'),
                                                    ]),
                                            ],
                                        ],
                                    ]),
                                ]),
                        ]),
                    ],
                ],
            ])
            ->scrollX(1200)
            ->search([
                ['关键词', 'keyword', Input::make()->props(['placeholder' => '搜索标题或内容', 'clearable' => true])],
                ['分类', 'category_id', Select::make()->props(['options' => '{{ categories }}', 'clearable' => true])],
                ['状态', 'status', Select::make()->props([
                    'options' => [
                        ['label' => '草稿', 'value' => 'draft'],
                        ['label' => '已发布', 'value' => 'published'],
                    ],
                    'clearable' => true,
                ])],
            ])
            ->toolbarLeft([
                Button::make()
                    ->type('primary')
                    ->on('click', [
                        SetAction::make('editId', null),
                        SetAction::make('formData', [
                            'title' => '',
                            'slug' => '',
                            'content' => '',
                            'cover' => '',
                            'category_id' => null,
                            'status' => 'draft',
                        ]),
                        SetAction::make('formVisible', true),
                    ])
                    ->text('新增文章'),
            ])
            ->data([
                'categories' => [],
            ])
            ->methods([
                'loadCategories' => [
                    FetchAction::make('/blog/categories?action_type=all')
                        ->then([
                            SetAction::make('categories', '{{ $response.data }}'),
                        ]),
                ],
            ])
            ->onMounted([
                CallAction::make('loadCategories'),
            ])
            ->modal('form', '{{ editId ? "编辑文章" : "新增文章" }}', $this->getFormSchema());

        return success($schema->build());
    }

    protected function getFormSchema(): array
    {
        return OptForm::make('formData')
            ->fields([
                ['标题', 'title', Input::make()->props(['placeholder' => '请输入标题'])],
                ['Slug', 'slug', Input::make()->props(['placeholder' => '请输入 URL 标识'])],
                ['分类', 'category_id', Select::make()->props(['options' => '{{ categories }}', 'placeholder' => '请选择分类'])],
                ['封面', 'cover', Input::make()->props(['placeholder' => '请输入封面图片 URL'])],
                ['内容', 'content', RichEditor::make()->props(['height' => 400])],
                ['状态', 'status', Select::make()->props([
                    'options' => [
                        ['label' => '草稿', 'value' => 'draft'],
                        ['label' => '已发布', 'value' => 'published'],
                    ],
                ])],
            ])
            ->buttons([
                Button::make()
                    ->on('click', [
                        SetAction::make('formVisible', false),
                    ])
                    ->text('取消'),
                Button::make()
                    ->type('primary')
                    ->on('click', [
                        CallAction::make('handleSubmit'),
                    ])
                    ->text('确定'),
            ])
            ->methods([
                'handleSubmit' => [
                    SetAction::make('submitting', true),
                    FetchAction::make('{{ editId ? "/blog/posts/" + editId : "/blog/posts" }}')
                        ->method('{{ editId ? "PUT" : "POST" }}')
                        ->body('{{ formData }}')
                        ->then([
                            CallAction::make('$message.success', ['保存成功']),
                            SetAction::make('formVisible', false),
                            CallAction::make('loadData'),
                        ])
                        ->catch([
                            CallAction::make('$message.error', ['保存失败']),
                        ])
                        ->finally([
                            SetAction::make('submitting', false),
                        ]),
                ],
            ])
            ->build();
    }

    protected function applySearch(Builder $query, Request $request): void
    {
        if ($request->filled('keyword')) {
            $query->where(function ($q) use ($request) {
                $q->where('title', 'like', '%' . $request->keyword . '%')
                    ->orWhere('content', 'like', '%' . $request->keyword . '%');
            });
        }

        if ($request->filled('category_id')) {
            $query->where('category_id', $request->category_id);
        }

        if ($request->filled('status')) {
            $query->where('status', $request->status);
        }
    }

    protected function getStoreRules(): array
    {
        return [
            'title' => 'required|string|max:255',
            'slug' => 'required|string|max:255|unique:posts',
            'content' => 'required|string',
            'cover' => 'nullable|string',
            'category_id' => 'required|exists:categories,id',
            'status' => 'required|in:draft,published',
        ];
    }

    protected function getUpdateRules(int $id): array
    {
        return [
            'title' => 'required|string|max:255',
            'slug' => 'required|string|max:255|unique:posts,slug,' . $id,
            'content' => 'required|string',
            'cover' => 'nullable|string',
            'category_id' => 'required|exists:categories,id',
            'status' => 'required|in:draft,published',
        ];
    }

    protected function prepareStoreData(array $validated): array
    {
        $validated['user_id'] = auth()->id();

        if ($validated['status'] === 'published' && !isset($validated['published_at'])) {
            $validated['published_at'] = now();
        }

        return $validated;
    }

    protected function afterStatusUpdate(mixed $model, bool $status): void
    {
        if ($status === 'published' && !$model->published_at) {
            $model->update(['published_at' => now()]);
        }
    }

    public function index(Request $request)
    {
        // 获取所有分类(用于下拉选择)
        if ($request->get('action_type') === 'all') {
            $categories = \App\Models\Category::select('id as value', 'name as label')
                ->orderBy('sort')
                ->get();
            return success($categories);
        }

        return parent::index($request);
    }
}

路由

php
Route::middleware(['auth:sanctum'])->group(function () {
    Route::resource('posts', PostController::class)
        ->parameters(['posts' => 'id'])
        ->except(['create', 'edit']);
});

基于 MIT 许可发布