在后台实现文章增删改查功能(支持Markdown)
本节我们将会完成博客后台管理系统的文章发布功能:我们将会继续完善 posts
表迁移、引入一些额外前端资源、并实现文章创建、修改和删除。
1、修改 posts 表
我们在十分钟创建博客应用中已经创建了 posts
表迁移,现在要对其进行修改和完善。
安装 Doctrine 依赖包
在 Laravel 中如果需要修改数据表的列,则需要安装 Doctrine 依赖包,我们使用 Composer 安装该依赖包:
composer require doctrine/dbal
创建表迁移文件
接下来使用 Artisan 命令创建新的迁移文件:
php artisan make:migration restructure_posts_table --table=posts
然后编辑刚刚创建的迁移文件:
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class RestructurePostsTable extends Migration
{
/**
* Run the migrations.
*/
public function up()
{
Schema::table('posts', function (Blueprint $table) {
$table->string('subtitle')->after('title');
$table->renameColumn('content', 'content_raw');
$table->text('content_html')->after('content');
$table->string('page_image')->after('content_html');
$table->string('meta_description')->after('page_image');
$table->boolean('is_draft')->after('meta_description');
$table->string('layout')->after('is_draft')->default('blog.layouts.post');
});
}
/**
* Reverse the migrations.
*/
public function down()
{
Schema::table('posts', function (Blueprint $table) {
$table->dropColumn('layout');
$table->dropColumn('is_draft');
$table->dropColumn('meta_description');
$table->dropColumn('page_image');
$table->dropColumn('content_html');
$table->renameColumn('content_raw', 'content');
$table->dropColumn('subtitle');
});
}
}
我们对表字段略作说明:
-
subtitle
:文章副标题 -
content_raw
:Markdown 格式文本 -
content_html
:使用 Markdown 编辑内容但同时保存 HTML 版本 -
page_image
:文章缩略图(封面图) -
meta_description
:文章备注说明 -
is_draft
:该文章是否是草稿 -
layout
:使用的布局
运行迁移
迁移文件已经创建并编辑好了,接下来我们登录到 Laradock 中运行该迁移:
php artisan migrate
现在数据库部分已经完成了!
2、创建 Markdown 服务
在本博客项目中我们将支持 Markdown 格式编辑文章。Markdown 是一种轻量级的标记语言,Markdown 的理念是能让文档更容易读、写和随意改。Markdown 格式文本可以被轻松转化为 HTML。
安装 Markdown 依赖包
有很多 PHP 包可用于将 Markdown 转化为 HTML。这里我们使用 Michel Fortin 提供的包 SmartyPants,在本地主机上使用 Composer 安装下面两个依赖包:
composer require michelf/php-markdown
composer require michelf/php-smartypants
创建 Markdowner 服务
这里我们创建一个封装前面使用 Composer 安装的 php-markdown
和 php-smartypants
包的简单服务类。
在 app/Services
目录下新建 Markdowner.php
文件,编辑该文件内容如下:
<?php
namespace App\Services;
use Michelf\MarkdownExtra;
use Michelf\SmartyPants;
class Markdowner
{
public function toHTML($text)
{
$text = $this->preTransformText($text);
$text = MarkdownExtra::defaultTransform($text);
$text = SmartyPants::defaultTransform($text);
$text = $this->postTransformText($text);
return $text;
}
protected function preTransformText($text)
{
return $text;
}
protected function postTransformText($text)
{
return $text;
}
}
3、修改相关模型
接下来我们来编辑 Post
模型类和 Tag
模型类来建立两者之间的关联关系。
首先编辑 app/Models/Tag.php
文件内容如下:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class Tag extends Model
{
protected $fillable = [
'tag', 'title', 'subtitle', 'page_image', 'meta_description','reverse_direction',
];
/**
* 定义文章与标签之间多对多关联关系
*
* @return BelongsToMany
*/
public function posts()
{
return $this->belongsToMany(Post::class, 'post_tag_pivot');
}
/**
* Add any tags needed from the list
*
* @param array $tags List of tags to check/add
*/
public static function addNeededTags(array $tags)
{
if (count($tags) === 0) {
return;
}
$found = static::whereIn('tag', $tags)->get()->pluck('tag')->all();
foreach (array_diff($tags, $found) as $tag) {
static::create([
'tag' => $tag,
'title' => $tag,
'subtitle' => 'Subtitle for '.$tag,
'page_image' => '',
'meta_description' => '',
'reverse_direction' => false,
]);
}
}
}
然后修改 app/Models/Post.php
文件内容如下:
<?php
namespace App\Models;
use App\Services\Markdowner;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class Post extends Model
{
protected $dates = ['published_at'];
/**
* The many-to-many relationship between posts and tags.
*
* @return BelongsToMany
*/
public function tags()
{
return $this->belongsToMany(Tag::class, 'post_tag_pivot');
}
/**
* Set the title attribute and automatically the slug
*
* @param string $value
*/
public function setTitleAttribute($value)
{
$this->attributes['title'] = $value;
if (!$this->exists) {
$value = uniqid(str_random(8));
$this->setUniqueSlug($value, 0);
}
}
/**
* Recursive routine to set a unique slug
*
* @param string $title
* @param mixed $extra
*/
protected function setUniqueSlug($title, $extra)
{
$slug = str_slug($title . '-' . $extra);
if (static::where('slug', $slug)->exists()) {
$this->setUniqueSlug($title, $extra + 1);
return;
}
$this->attributes['slug'] = $slug;
}
/**
* Set the HTML content automatically when the raw content is set
*
* @param string $value
*/
public function setContentRawAttribute($value)
{
$markdown = new Markdowner();
$this->attributes['content_raw'] = $value;
$this->attributes['content_html'] = $markdown->toHTML($value);
}
/**
* Sync tag relation adding new tags as needed
*
* @param array $tags
*/
public function syncTags(array $tags)
{
Tag::addNeededTags($tags);
if (count($tags)) {
$this->tags()->sync(
Tag::whereIn('tag', $tags)->get()->pluck('id')->all()
);
return;
}
$this->tags()->detach();
}
}
4、引入 Selectize.js 和 Pickadate.js
下面为后台文章功能引入两个前端 JS 资源,我们使用 NPM 下载资源,然后通过 Laravel Mix 引入。
使用 NPM 下载资源
首先是 Selectize.js。Selectize.js 是一个基于 jQuery 的 UI 控件,对于标签选择和下拉列表功能非常有用。我们将使用它来处理文章标签输入。使用 NPM 下载 Seletize.js:
npm install selectize --save-dev
接下来下载 Pickadate.js。Pickadate.js 是一个轻量级的 jQuery 日期时间选择插件,日期时间插件很多,选择使用 Pickadate.js 的原因是它在小型设备上也有很好的体验。下面我们使用 NPM 下载安装 Pickadate.js:
npm install pickadate --save-dev
使用 Laravel Mix 管理前端资源
现在相应的前端资源已经下载好了,接下来我们使用 Laravel Mix 来管理这些资源。编辑根目录下的 webpack.mix.js
配置文件如下:
const mix = require('laravel-mix');
/*
|--------------------------------------------------------------------------
| Mix Asset Management
|--------------------------------------------------------------------------
|
| Mix provides a clean, fluent API for defining some Webpack build steps
| for your Laravel application. By default, we are compiling the Sass
| file for the application as well as bundling up all the JS files.
|
*/
mix.js('resources/js/app.js', 'public/js')
.sass('resources/sass/app.scss', 'public/css');
# Selectize 暂不支持 Bootstrap 4
mix.combine([
'node_modules/selectize/dist/css/selectize.css',
'node_modules/selectize/dist/css/selectize.bootstrap3.css'
], 'public/css/selectize.default.css');
mix.combine([
'node_modules/pickadate/lib/compressed/themes/default.css',
'node_modules/pickadate/lib/compressed/themes/default.date.css',
'node_modules/pickadate/lib/compressed/themes/default.time.css',
], 'public/css/pickadate.min.css');
mix.copy('node_modules/selectize/dist/js/standalone/selectize.min.js',
'public/js/selectize.min.js');
mix.combine([
'node_modules/pickadate/lib/compressed/picker.js',
'node_modules/pickadate/lib/compressed/picker.date.js',
'node_modules/pickadate/lib/compressed/picker.time.js'
], 'public/js/pickadate.min.js');
运行 npm run dev
重新编译前端资源。
5、创建表单请求类
正如我们在上一节处理标签时所做的一样,我们使用表单请求类来验证文件创建及更新请求。
首先,使用 Artisan 命令创建表单请求处理类,对应文件会生成在 app/Http/Requests
目录下:
php artisan make:request PostCreateRequest
php artisan make:request PostUpdateRequest
编辑新创建的 PostCreateRequest.php
内容如下:
<?php
namespace App\Http\Requests;
use Carbon\Carbon;
use Illuminate\Foundation\Http\FormRequest;
class PostCreateRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'title' => 'required',
'subtitle' => 'required',
'content' => 'required',
'publish_date' => 'required',
'publish_time' => 'required',
'layout' => 'required',
];
}
/**
* Return the fields and values to create a new post from
*/
public function postFillData()
{
$published_at = new Carbon(
$this->publish_date . ' ' . $this->publish_time
);
return [
'title' => $this->title,
'subtitle' => $this->subtitle,
'page_image' => $this->page_image,
'content_raw' => $this->get('content'),
'meta_description' => $this->meta_description,
'is_draft' => (bool)$this->is_draft,
'published_at' => $published_at,
'layout' => $this->layout,
];
}
}
这是一个包含 authorize()
和 rules()
方法的标准表单请求类,此外我们还添加了一个 postFillData()
方法,使用该方法可以轻松从请求中获取数据填充 Post
模型。
然后修改 PostUpdateRequest.php
内容如下:
<?php
namespace App\Http\Requests;
class PostUpdateRequest extends PostCreateRequest
{
//
}
该类继承自 PostCreateRequest
,当然目前来看这连个类做的事情完全一样,我们也可以使用同一个请求类处理文章创建和修改,但是为了方便以后扩展这里我们使用两个请求类分别处理创建和更新请求。
6、添加辅助函数
我们还需要两个辅助函数,因此我们编辑 app/helpers.php
文件内容添加这两个函数:
/**
* Return "checked" if true
*/
function checked($value)
{
return $value ? 'checked' : '';
}
/**
* Return img url for headers
*/
function page_image($value = null)
{
if (empty($value)) {
$value = config('blog.page_image');
}
if (! starts_with($value, 'http') && $value[0] !== '/') {
$value = config('blog.uploads.webpath') . '/' . $value;
}
return $value;
}
checked()
方法用于在视图的复选框和单选框中设置 checked
属性。
page_image()
方法用于返回上传图片的完整路径。
7、修改 Post 模型
你可能已经注意到我们将 published_at
分割成了 publish_date
和 publish_time
,下面我们在 Post
模型中添加这两个字段:
// 在 Post 类的 $dates 属性后添加 $fillable 属性
protected $fillable = [
'title', 'subtitle', 'content_raw', 'page_image', 'meta_description','layout', 'is_draft', 'published_at',
];
// 然后在 Post 模型类中添加如下几个方法
/**
* 返回 published_at 字段的日期部分
*/
public function getPublishDateAttribute($value)
{
return $this->published_at->format('Y-m-d');
}
/**
* 返回 published_at 字段的时间部分
*/
public function getPublishTimeAttribute($value)
{
return $this->published_at->format('g:i A');
}
/**
* content_raw 字段别名
*/
public function getContentAttribute($value)
{
return $this->content_raw;
}
此外我们还添加了 getContentAttribute()
方法作为访问器以便返回 $this->content_raw
。现在如果我们使用 $post->content
就会执行该方法。
8、修改 PostController 控制器
现在我们在 PostController
类中实现所有需要的功能。由于我们将表单验证分散到表单请求类中完成,控制器的代码量将会很小:
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Requests\PostCreateRequest;
use App\Http\Requests\PostUpdateRequest;
use App\Jobs\PostFormFields;
use App\Models\Post;
use App\Models\Tag;
use Carbon\Carbon;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Illuminate\Http\Response;
class PostController extends Controller
{
protected $fieldList = [
'title' => '',
'subtitle' => '',
'page_image' => '',
'content' => '',
'meta_description' => '',
'is_draft' => "0",
'publish_date' => '',
'publish_time' => '',
'layout' => 'blog.layouts.post',
'tags' => [],
];
/**
* Display a listing of the posts.
*/
public function index()
{
return view('admin.post.index', ['posts' => Post::all()]);
}
/**
* Show the new post form
*/
public function create()
{
$fields = $this->fieldList;
$when = Carbon::now()->addHour();
$fields['publish_date'] = $when->format('Y-m-d');
$fields['publish_time'] = $when->format('g:i A');
foreach ($fields as $fieldName => $fieldValue) {
$fields[$fieldName] = old($fieldName, $fieldValue);
}
$data = array_merge(
$fields,
['allTags' => Tag::all()->pluck('tag')->all()]
);
return view('admin.post.create', $data);
}
/**
* Store a newly created Post
*
* @param PostCreateRequest $request
*/
public function store(PostCreateRequest $request)
{
$post = Post::create($request->postFillData());
$post->syncTags($request->get('tags', []));
return redirect()
->route('post.index')
->with('success', '新文章创建成功.');
}
/**
* Show the post edit form
*
* @param int $id
* @return Response
*/
public function edit($id)
{
$fields = $this->fieldsFromModel($id, $this->fieldList);
foreach ($fields as $fieldName => $fieldValue) {
$fields[$fieldName] = old($fieldName, $fieldValue);
}
$data = array_merge(
$fields,
['allTags' => Tag::all()->pluck('tag')->all()]
);
return view('admin.post.edit', $data);
}
/**
* 更新文章
*
* @param PostUpdateRequest $request
* @param $id
* @return \Illuminate\Http\RedirectResponse
*/
public function update(PostUpdateRequest $request, $id)
{
$post = Post::findOrFail($id);
$post->fill($request->postFillData());
$post->save();
$post->syncTags($request->get('tags', []));
if ($request->action === 'continue') {
return redirect()
->back()
->with('success', '文章已保存.');
}
return redirect()
->route('post.index')
->with('success', '文章已保存.');
}
/**
* Remove the specified resource from storage.
*
* @param int $id
* @return Response
*/
public function destroy($id)
{
$post = Post::findOrFail($id);
$post->tags()->detach();
$post->delete();
return redirect()
->route('post.index')
->with('success', '文章已删除.');
}
/**
* Return the field values from the model
*
* @param integer $id
* @param array $fields
* @return array
*/
private function fieldsFromModel($id, array $fields)
{
$post = Post::findOrFail($id);
$fieldNames = array_keys(array_except($fields, ['tags']));
$fields = ['id' => $id];
foreach ($fieldNames as $field) {
$fields[$field] = $post->{$field};
}
$fields['tags'] = $post->tags->pluck('tag')->all();
return $fields;
}
}
接下来唯一要做的就是创建相应视图了。
9、创建文章视图
现在我们来创建 PostController
中用到的所有视图。
首先修改已经存在的位于 resources/views/admin/post
目录下的 index.blade.php
:
@extends('admin.layout')
@section('content')
<div class="container">
<div class="row page-title-row">
<div class="col-md-6">
<h3>文章 <small>» 列表</small></h3>
</div>
<div class="col-md-6 text-right">
<a href="/admin/post/create" class="btn btn-success btn-md">
<i class="fa fa-plus-circle"></i> 创建新文章
</a>
</div>
</div>
<div class="row">
<div class="col-sm-12">
@include('admin.partials.errors')
@include('admin.partials.success')
<table id="posts-table" class="table table-striped table-bordered">
<thead>
<tr>
<th>发布时间</th>
<th>标题</th>
<th>副标题</th>
<th data-sortable="false">操作</th>
</tr>
</thead>
<tbody>
@foreach ($posts as $post)
<tr>
<td data-order="{{ $post->published_at->timestamp }}">
{{ $post->published_at->format('Y-m-d g:ia') }}
</td>
<td>{{ $post->title }}</td>
<td>{{ $post->subtitle }}</td>
<td>
<a href="/admin/post/{{ $post->id }}/edit" class="btn btn-xs btn-info">
<i class="fa fa-edit"></i> 编辑
</a>
<a href="/blog/{{ $post->slug }}" class="btn btn-xs btn-warning">
<i class="fa fa-eye"></i> 查看
</a>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
@stop
@section('scripts')
<script>
$(function() {
$("#posts-table").DataTable({
order: [[0, "desc"]]
});
});
</script>
@stop
该视图很简单,就是使用文章数据填充表格然后使用 DataTables 初始化表格。
接下来,在 resources/views/admin/post
目录下新建一个 create.blade.php
:
@extends('admin.layout')
@section('styles')
<link href="{{ asset('css/pickadate.min.css') }}" rel="stylesheet">
<link href="{{ asset('css/selectize.default.css') }}" rel="stylesheet">
@stop
@section('content')
<div class="container">
<div class="row page-title-row">
<div class="col-md-12">
<h3>文章 <small>» 创建新文章</small></h3>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<div class="card">
<div class="card-header">
<h3 class="card-title">创建新文章表单</h3>
</div>
<div class="card-body">
@include('admin.partials.errors')
<form role="form" method="POST" action="{{ route('post.store') }}">
<input type="hidden" name="_token" value="{{ csrf_token() }}">
@include('admin.post._form')
<div class="row">
<div class="col-md-8">
<div class="form-group row">
<div class="col-md-10 offset-md-2">
<button type="submit" class="btn btn-primary">
<i class="fa fa-disk-o"></i>
保存新文章
</button>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@stop
@section('scripts')
<script src="{{ asset('js/pickadate.min.js') }}"></script>
<script src="{{ asset('js/selectize.min.js') }}"></script>
<script>
$(function() {
$("#publish_date").pickadate({
format: "yyyy-mm-dd"
});
$("#publish_time").pickatime({
format: "h:i A"
});
$("#tags").selectize({
create: true
});
});
</script>
@stop
这里我们引入了 Selectize 和 Pickadate 库。你可能还注意到了我们还引入了一个尚未创建的局部视图 admin.post._form
。下面我们就在 resources/views/admin/post
目录下创建这个视图,在该目录先新建一个 _form.blade.php
,编辑其内容如下:
<div class="row">
<div class="col-md-8">
<div class="form-group row">
<label for="title" class="col-md-2 col-form-label">
标题
</label>
<div class="col-md-10">
<input type="text" class="form-control" name="title" autofocus id="title" value="{{ $title }}">
</div>
</div>
<div class="form-group row">
<label for="subtitle" class="col-md-2 col-form-label">
副标题
</label>
<div class="col-md-10">
<input type="text" class="form-control" name="subtitle" id="subtitle" value="{{ $subtitle }}">
</div>
</div>
<div class="form-group row">
<label for="page_image" class="col-md-2 col-form-label">
缩略图
</label>
<div class="col-md-10">
<div class="row">
<div class="col-md-8">
<input type="text" class="form-control" name="page_image" id="page_image" onchange="handle_image_change()" alt="Image thumbnail" value="{{ $page_image }}">
</div>
<script>
function handle_image_change() {
$("#page-image-preview").attr("src", function () {
var value = $("#page_image").val();
if ( ! value) {
value = {!! json_encode(config('blog.page_image')) !!};
if (value == null) {
value = '';
}
}
if (value.substr(0, 4) != 'http' && value.substr(0, 1) != '/') {
value = {!! json_encode(config('blog.uploads.webpath')) !!} + '/' + value;
}
return value;
});
}
</script>
<div class="visible-sm space-10"></div>
<div class="col-md-4 text-right">
<img src="{{ page_image($page_image) }}" class="img img_responsive" id="page-image-preview" style="max-height:40px">
</div>
</div>
</div>
</div>
<div class="form-group row">
<label for="content" class="col-md-2 col-form-label">
内容
</label>
<div class="col-md-10">
<textarea class="form-control" name="content" rows="14" id="content">{{ $content }}</textarea>
</div>
</div>
</div>
<div class="col-md-4">
<div class="form-group row">
<label for="publish_date" class="col-md-3 col-form-label">
发布日期
</label>
<div class="col-md-8">
<input class="form-control" name="publish_date" id="publish_date" type="text" value="{{ $publish_date }}">
</div>
</div>
<div class="form-group row">
<label for="publish_time" class="col-md-3 col-form-label">
发布时间
</label>
<div class="col-md-8">
<input class="form-control" name="publish_time" id="publish_time" type="text" value="{{ $publish_time }}">
</div>
</div>
<div class="form-group row">
<div class="col-md-8 col-md-offset-3">
<div class="checkbox">
<label>
<input {{ checked($is_draft) }} type="checkbox" name="is_draft">
草稿?
</label>
</div>
</div>
</div>
<div class="form-group row">
<label for="tags" class="col-md-3 col-form-label">
标签
</label>
<div class="col-md-8">
<select name="tags[]" id="tags" class="form-control" multiple>
@foreach ($allTags as $tag)
<option @if (in_array($tag, $tags)) selected @endif value="{{ $tag }}">
{{ $tag }}
</option>
@endforeach
</select>
</div>
</div>
<div class="form-group row">
<label for="layout" class="col-md-3 col-form-label">
布局
</label>
<div class="col-md-8">
<input type="text" class="form-control" name="layout" id="layout" value="{{ $layout }}">
</div>
</div>
<div class="form-group row">
<label for="meta_description" class="col-md-3 col-form-label">
摘要
</label>
<div class="col-md-8">
<textarea class="form-control" name="meta_description" id="meta_description" rows="6">
{{ $meta_description }}
</textarea>
</div>
</div>
</div>
</div>
我们创建这个局部视图的目的是让 create
和 edit
视图可以共享它。
下面我们在同一目录下创建 edit.blade.php
:
@extends('admin.layout')
@section('styles')
<link href="{{ asset('css/pickadate.min.css') }}" rel="stylesheet">
<link href="{{ asset('css/selectize.default.css') }}" rel="stylesheet">
@stop
@section('content')
<div class="container">
<div class="row page-title-row">
<div class="col-md-12">
<h3>文章 <small>» 编辑文章</small></h3>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<div class="card">
<div class="card-header">
<h3 class="card-title">文章编辑表单</h3>
</div>
<div class="card-body">
@include('admin.partials.errors')
@include('admin.partials.success')
<form role="form" method="POST" action="{{ route('post.update', $id) }}">
<input type="hidden" name="_token" value="{{ csrf_token() }}">
<input type="hidden" name="_method" value="PUT">
@include('admin.post._form')
<div class="row">
<div class="col-md-8">
<div class="form-group row">
<div class="col-md-10 offset-md-2">
<button type="submit" class="btn btn-primary" name="action" value="continue">
<i class="fa fa-floppy-o"></i>
保存 - 继续
</button>
<button type="submit" class="btn btn-success" name="action" value="finished">
<i class="fa fa-floppy-o"></i>
保存 - 完成
</button>
<button type="button" class="btn btn-danger" data-toggle="modal" data-target="#modal-delete">
<i class="fa fa-times-circle"></i>
删除
</button>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
{{-- 确认删除 --}}
<div class="modal fade" id="modal-delete" tabIndex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">请确认</h4>
<button type="button" class="close" data-dismiss="modal">
×
</button>
</div>
<div class="modal-body">
<p class="lead">
<i class="fa fa-question-circle fa-lg"></i>
确定要删除这篇文章?
</p>
</div>
<div class="modal-footer">
<form method="POST" action="{{ route('post.destroy', $id) }}">
<input type="hidden" name="_token" value="{{ csrf_token() }}">
<input type="hidden" name="_method" value="DELETE">
<button type="button" class="btn btn-secondary" data-dismiss="modal">关闭</button>
<button type="submit" class="btn btn-danger">
<i class="fa fa-times-circle"></i> 是的
</button>
</form>
</div>
</div>
</div>
</div>
</div>
@stop
@section('scripts')
<script src="{{ asset('js/pickadate.min.js') }}"></script>
<script src="{{ asset('js/selectize.min.js') }}"></script>
<script>
$(function() {
$("#publish_date").pickadate({
format: "mmm-d-yyyy"
});
$("#publish_time").pickatime({
format: "h:i A"
});
$("#tags").selectize({
create: true
});
});
</script>
@stop
至此,所有后台文章管理所需的视图都已经创建好了。
10、移除 show 路由
最后要做的收尾工作是移除显示文章详情路由 show
。编辑 routes/web.php
如下:
// 将如下这行
resource('admin/post', 'PostController');
// 修改成
resource('admin/post', 'PostController', ['except' => 'show']);
好了,接下来可以在后台测试文章创建、编辑、删除了。
11、测试后台文章增删改查功能
在浏览器中访问 http://blog57.test/admin/post
,点击「创建新文章」按钮,进入发布文章页面:
点击「保存新文章」按钮,发布文章成功后跳转到后台文章列表:
第一篇即为我们刚刚发布的文章,其它是之前十分钟创建博客的测试数据。接下来我们可以点击「编辑」按钮编辑文章:
在该页面可以删除文章。「保存 - 继续」与「保存 - 完成」区别在于前者保存后会停留在编辑页面,后台保存后跳转到文章列表页。
在文章列表页我们还可以点击「查看」按钮查看文章详情:
当然现在页面还比较丑,而且并没有对 Markdown 格式内容做处理(其实只要将 posts
表中 content_html
字段内容输出即可),下一节我们将开始优化博客前台页面。
41 Comments
两个关联关系中App\Models\Post,命名空间没改过来
感谢反馈 已处理
创建文章的时间为什么要把发布时间延时一小时?
$when = Carbon::now()->addHour();
随便设置了下默认值而已 你可以在发布表单里手动设置来覆盖
好的,谢谢
最後要做的收尾工作是移除顯示文章詳情路由 show。編輯 app/Http/routes.php 如下:
應是routes/web.php?
感谢反馈 已修正
第4步,引入 引入 Selectize.js 和 Pickadate.js后,运行 npm run dev 重新编译前端资源后报错,页面js404是什么情况?
原来是这里 # Selectize 暂不支持 Bootstrap 4
不知为何在点击按钮确认修改时出现如下异常 DateTime::__construct(): Failed to parse time string (2019-01-13 13:43:00 2019-01-13 13:43:00) at position 20 (2): Double date specification 希望解答:)