基于Laravel开发博客应用系列 —— 后台文章增删改查功能实现(支持Markdown)


本节我们将会完成博客后台管理系统的文章发布功能:我们将会继续完善 posts 表迁移、引入一些额外前端资源、并实现文章创建、修改和删除。

1、修改 posts 表

我们在十分钟创建博客应用中已经创建了posts 表迁移,现在要对其进行修改和完善。

安装 Doctrine 依赖包

在 Laravel 5.1 中如果需要修改数据表的列,则需要安装 Doctrine 依赖包,我们使用 Composer 安装该依赖包:

  1. composer require "doctrine/dbal"

创建表迁移文件

接下来使用 Artisan 命令创建新的迁移文件:

  1. php artisan make:migration --table=posts restructure_posts_table

然后编辑刚刚创建的迁移文件:

  1. <?php
  2.  
  3. use Illuminate\Database\Schema\Blueprint;
  4. use Illuminate\Database\Migrations\Migration;
  5.  
  6. class RestructurePostsTable extends Migration
  7. {
  8. /**
  9. * Run the migrations.
  10. */
  11. public function up()
  12. {
  13. Schema::table('posts', function (Blueprint $table) {
  14. $table->string('subtitle')->after('title');
  15. $table->renameColumn('content', 'content_raw');
  16. $table->text('content_html')->after('content');
  17. $table->string('page_image')->after('content_html');
  18. $table->string('meta_description')->after('page_image');
  19. $table->boolean('is_draft')->after('meta_description');
  20. $table->string('layout')->after('is_draft')->default('blog.layouts.post');
  21. });
  22. }
  23.  
  24. /**
  25. * Reverse the migrations.
  26. */
  27. public function down()
  28. {
  29. Schema::table('posts', function (Blueprint $table) {
  30. $table->dropColumn('layout');
  31. $table->dropColumn('is_draft');
  32. $table->dropColumn('meta_description');
  33. $table->dropColumn('page_image');
  34. $table->dropColumn('content_html');
  35. $table->renameColumn('content_raw', 'content');
  36. $table->dropColumn('subtitle');
  37. });
  38. }
  39. }

我们对表字段略作说明:

  • subtitle:文章副标题
  • content_raw:Markdown格式文本
  • content_html:使用 Markdown 编辑内容但同时保存 HTML 版本
  • page_image:文章缩略图(封面图)
  • meta_description:文章备注说明
  • is_draft:该文章是否是草稿
  • layout:使用的布局

运行迁移

迁移已经创建并编辑好了,接下来我们登录到 Homestead 虚拟机中运行该迁移:

  1. php artisan migrate

现在数据库部分已经完成了!

2、修改相关模型

接下来我们来编辑 Post 模型类和 Tag 模型类来建立两者之间的关联关系。

首先编辑 app\Tag.php 文件内容如下:

  1. <?php
  2.  
  3. namespace App;
  4.  
  5. use Illuminate\Database\Eloquent\Model;
  6.  
  7. class Tag extends Model
  8. {
  9. protected $fillable = [
  10. 'tag', 'title', 'subtitle', 'page_image', 'meta_description','reverse_direction',
  11. ];
  12.  
  13. /**
  14. * 定义文章与标签之间多对多关联关系
  15. *
  16. * @return BelongsToMany
  17. */
  18. public function posts()
  19. {
  20. return $this->belongsToMany('App\Post', 'post_tag_pivot');
  21. }
  22.  
  23. /**
  24. * Add any tags needed from the list
  25. *
  26. * @param array $tags List of tags to check/add
  27. */
  28. public static function addNeededTags(array $tags)
  29. {
  30. if (count($tags) === 0) {
  31. return;
  32. }
  33.  
  34. $found = static::whereIn('tag', $tags)->lists('tag')->all();
  35.  
  36. foreach (array_diff($tags, $found) as $tag) {
  37. static::create([
  38. 'tag' => $tag,
  39. 'title' => $tag,
  40. 'subtitle' => 'Subtitle for '.$tag,
  41. 'page_image' => '',
  42. 'meta_description' => '',
  43. 'reverse_direction' => false,
  44. ]);
  45. }
  46. }
  47. }

然后修改 app\Post.php 文件内容如下:

  1. <?php
  2.  
  3. namespace App;
  4.  
  5. use App\Services\Markdowner;
  6. use Illuminate\Database\Eloquent\Model;
  7.  
  8. class Post extends Model
  9. {
  10. protected $dates = ['published_at'];
  11.  
  12. /**
  13. * The many-to-many relationship between posts and tags.
  14. *
  15. * @return BelongsToMany
  16. */
  17. public function tags()
  18. {
  19. return $this->belongsToMany('App\Tag', 'post_tag_pivot');
  20. }
  21.  
  22. /**
  23. * Set the title attribute and automatically the slug
  24. *
  25. * @param string $value
  26. */
  27. public function setTitleAttribute($value)
  28. {
  29. $this->attributes['title'] = $value;
  30.  
  31. if (! $this->exists) {
  32. $this->setUniqueSlug($value, '');
  33. }
  34. }
  35.  
  36. /**
  37. * Recursive routine to set a unique slug
  38. *
  39. * @param string $title
  40. * @param mixed $extra
  41. */
  42. protected function setUniqueSlug($title, $extra)
  43. {
  44. $slug = str_slug($title.'-'.$extra);
  45.  
  46. if (static::whereSlug($slug)->exists()) {
  47. $this->setUniqueSlug($title, $extra + 1);
  48. return;
  49. }
  50.  
  51. $this->attributes['slug'] = $slug;
  52. }
  53.  
  54. /**
  55. * Set the HTML content automatically when the raw content is set
  56. *
  57. * @param string $value
  58. */
  59. public function setContentRawAttribute($value)
  60. {
  61. $markdown = new Markdowner();
  62.  
  63. $this->attributes['content_raw'] = $value;
  64. $this->attributes['content_html'] = $markdown->toHTML($value);
  65. }
  66.  
  67. /**
  68. * Sync tag relation adding new tags as needed
  69. *
  70. * @param array $tags
  71. */
  72. public function syncTags(array $tags)
  73. {
  74. Tag::addNeededTags($tags);
  75.  
  76. if (count($tags)) {
  77. $this->tags()->sync(
  78. Tag::whereIn('tag', $tags)->lists('id')->all()
  79. );
  80. return;
  81. }
  82.  
  83. $this->tags()->detach();
  84. }
  85. }

3、添加 Selectize.js 和 Pickadate.js

下面为后台文章功能引入两个前端 JS 资源,我们使用 Bower 下载资源,然后使用 Gulp 将这些资源放到指定位置。

使用 Bower 下载资源

首先是 Selectize.js。Selectize.js 是一个基于 jQuery 的 UI 控件,对于标签选择和下拉列表功能非常有用。我们将使用它来处理文章标签输入。使用 Bower 下载 Seletize.js:

  1. bower install selectize --save

接下来下载 Pickadate.js。Pickadate.js 是一个轻量级的 jQuery 日期时间选择插件,日期时间插件很多,选择使用  Pickadate.js 的原因是它在小型设备上也有很好的体验。下面我们使用 Bower 下载安装 Pickadate.js:

  1. bower install pickadate --save

使用 Gulp 管理前端资源

现在相应的前端资源已经下载好了,接下来我们使用 Gulp 来管理这些资源,编辑 gulpfile.js 文件内容如下:

  1. var gulp = require('gulp');
  2. var rename = require('gulp-rename');
  3. var elixir = require('laravel-elixir');
  4.  
  5. /**
  6. * 拷贝文件
  7. *
  8. * Do a 'gulp copyfiles' after bower updates
  9. */
  10. gulp.task("copyfiles", function() {
  11.  
  12. // Copy jQuery, Bootstrap, and FontAwesome
  13. gulp.src("vendor/bower_dl/jquery/dist/jquery.js")
  14. .pipe(gulp.dest("resources/assets/js/"));
  15.  
  16. gulp.src("vendor/bower_dl/bootstrap/less/**")
  17. .pipe(gulp.dest("resources/assets/less/bootstrap"));
  18.  
  19. gulp.src("vendor/bower_dl/bootstrap/dist/js/bootstrap.js")
  20. .pipe(gulp.dest("resources/assets/js/"));
  21.  
  22. gulp.src("vendor/bower_dl/bootstrap/dist/fonts/**")
  23. .pipe(gulp.dest("public/assets/fonts"));
  24.  
  25. gulp.src("vendor/bower_dl/fontawesome/less/**")
  26. .pipe(gulp.dest("resources/assets/less/fontawesome"));
  27.  
  28. gulp.src("vendor/bower_dl/fontawesome/fonts/**")
  29. .pipe(gulp.dest("public/assets/fonts"));
  30.  
  31. // Copy datatables
  32. var dtDir = 'vendor/bower_dl/datatables-plugins/integration/';
  33.  
  34. gulp.src("vendor/bower_dl/datatables/media/js/jquery.dataTables.js")
  35. .pipe(gulp.dest('resources/assets/js/'));
  36.  
  37. gulp.src(dtDir + 'bootstrap/3/dataTables.bootstrap.css')
  38. .pipe(rename('dataTables.bootstrap.less'))
  39. .pipe(gulp.dest('resources/assets/less/others/'));
  40.  
  41. gulp.src(dtDir + 'bootstrap/3/dataTables.bootstrap.js')
  42. .pipe(gulp.dest('resources/assets/js/'));
  43.  
  44. // Copy selectize
  45. gulp.src("vendor/bower_dl/selectize/dist/css/**")
  46. .pipe(gulp.dest("public/assets/selectize/css"));
  47.  
  48. gulp.src("vendor/bower_dl/selectize/dist/js/standalone/selectize.min.js")
  49. .pipe(gulp.dest("public/assets/selectize/"));
  50.  
  51. // Copy pickadate
  52. gulp.src("vendor/bower_dl/pickadate/lib/compressed/themes/**")
  53. .pipe(gulp.dest("public/assets/pickadate/themes/"));
  54.  
  55. gulp.src("vendor/bower_dl/pickadate/lib/compressed/picker.js")
  56. .pipe(gulp.dest("public/assets/pickadate/"));
  57.  
  58. gulp.src("vendor/bower_dl/pickadate/lib/compressed/picker.date.js")
  59. .pipe(gulp.dest("public/assets/pickadate/"));
  60.  
  61. gulp.src("vendor/bower_dl/pickadate/lib/compressed/picker.time.js")
  62. .pipe(gulp.dest("public/assets/pickadate/"));
  63.  
  64. });
  65.  
  66. /**
  67. * Default gulp is to run this elixir stuff
  68. */
  69. elixir(function(mix) {
  70.  
  71. // 合并 JS
  72. mix.scripts(
  73. [
  74. 'js/jquery.js',
  75. 'js/bootstrap.js',
  76. 'js/jquery.dataTables.js',
  77. 'js/dataTables.bootstrap.js'
  78. ],
  79. 'public/assets/js/admin.js',
  80. 'resources/assets'
  81. );
  82.  
  83. // 编译 Less
  84. mix.less('admin.less', 'public/assets/css/admin.css');
  85. });

配置和之前基本一致,不同之处在于新增了 Selectize 和 Pickadate 配置。

下面我们运行 gulp copyfiles 命令将上述两个前端资源拷贝到 public 目录下:

使用Gulp发布Selectize.js和pickadate.js

4、创建表单请求类

正如我们在上一节处理标签时所做的一样,我们使用表单请求类来验证文件创建及更新请求。

首先,使用 Artisan 命令创建表单请求处理类,对应文件会生成在 app/Http/Requests 目录下:

  1. php artisan make:request PostCreateRequest 
  2. php artisan make:request PostUpdateRequest

编辑新创建的 PostCreateRequest.php 内容如下:

  1. <?php
  2.  
  3. namespace App\Http\Requests;
  4.  
  5. use Carbon\Carbon;
  6.  
  7. class PostCreateRequest extends Request
  8. {
  9. /**
  10. * Determine if the user is authorized to make this request.
  11. */
  12. public function authorize()
  13. {
  14. return true;
  15. }
  16.  
  17. /**
  18. * Get the validation rules that apply to the request.
  19. *
  20. * @return array
  21. */
  22. public function rules()
  23. {
  24. return [
  25. 'title' => 'required',
  26. 'subtitle' => 'required',
  27. 'content' => 'required',
  28. 'publish_date' => 'required',
  29. 'publish_time' => 'required',
  30. 'layout' => 'required',
  31. ];
  32. }
  33.  
  34. /**
  35. * Return the fields and values to create a new post from
  36. */
  37. public function postFillData()
  38. {
  39. $published_at = new Carbon(
  40. $this->publish_date.' '.$this->publish_time
  41. );
  42. return [
  43. 'title' => $this->title,
  44. 'subtitle' => $this->subtitle,
  45. 'page_image' => $this->page_image,
  46. 'content_raw' => $this->get('content'),
  47. 'meta_description' => $this->meta_description,
  48. 'is_draft' => (bool)$this->is_draft,
  49. 'published_at' => $published_at,
  50. 'layout' => $this->layout,
  51. ];
  52. }
  53. }

这是一个包含 authorize()rules() 方法的标准表单请求类,此外我们还添加了一个 postFillData() 方法,使用该方法可以轻松从请求中获取数据填充 Post 模型。

然后修改 PostUpdateRequest.php 内容如下:

  1. <?php
  2.  
  3. namespace App\Http\Requests;
  4.  
  5. class PostUpdateRequest extends PostCreateRequest
  6. {
  7. //
  8. }

该类继承自 PostCreateRequest,当然目前来看这连个类做的事情完全一样,我们也可以使用同一个请求类处理文章创建和修改,但是为了方便以后扩展这里我们使用两个请求类分别处理创建和更新请求。

5、创建 PostFormFields 任务

接下来我们创建一个公用的、可以从 PostController 中调用的任务类,我们将其称之为 PostFormFields。该任务会在我们想要获取文章所有字段填充文章表单时被执行。

首先使用 Artisan 命令创建任务类模板:

  1. php artisan make:job PostFormFields

创建的任务类位于 app/Jobs 目录下。编辑新生成的 PostFormFields.php 文件内容如下:

  1. <?php
  2.  
  3. namespace App\Jobs;
  4.  
  5. use App\Post;
  6. use App\Tag;
  7. use Carbon\Carbon;
  8. use Illuminate\Contracts\Bus\SelfHandling;
  9.  
  10. class PostFormFields extends Job implements SelfHandling
  11. {
  12. /**
  13. * The id (if any) of the Post row
  14. *
  15. * @var integer
  16. */
  17. protected $id;
  18.  
  19. /**
  20. * List of fields and default value for each field
  21. *
  22. * @var array
  23. */
  24. protected $fieldList = [
  25. 'title' => '',
  26. 'subtitle' => '',
  27. 'page_image' => '',
  28. 'content' => '',
  29. 'meta_description' => '',
  30. 'is_draft' => "0",
  31. 'publish_date' => '',
  32. 'publish_time' => '',
  33. 'layout' => 'blog.layouts.post',
  34. 'tags' => [],
  35. ];
  36.  
  37. /**
  38. * Create a new command instance.
  39. *
  40. * @param integer $id
  41. */
  42. public function __construct($id = null)
  43. {
  44. $this->id = $id;
  45. }
  46.  
  47. /**
  48. * Execute the command.
  49. *
  50. * @return array of fieldnames => values
  51. */
  52. public function handle()
  53. {
  54. $fields = $this->fieldList;
  55.  
  56. if ($this->id) {
  57. $fields = $this->fieldsFromModel($this->id, $fields);
  58. } else {
  59. $when = Carbon::now()->addHour();
  60. $fields['publish_date'] = $when->format('M-j-Y');
  61. $fields['publish_time'] = $when->format('g:i A');
  62. }
  63.  
  64. foreach ($fields as $fieldName => $fieldValue) {
  65. $fields[$fieldName] = old($fieldName, $fieldValue);
  66. }
  67.  
  68. return array_merge(
  69. $fields,
  70. ['allTags' => Tag::lists('tag')->all()]
  71. );
  72. }
  73.  
  74. /**
  75. * Return the field values from the model
  76. *
  77. * @param integer $id
  78. * @param array $fields
  79. * @return array
  80. */
  81. protected function fieldsFromModel($id, array $fields)
  82. {
  83. $post = Post::findOrFail($id);
  84.  
  85. $fieldNames = array_keys(array_except($fields, ['tags']));
  86.  
  87. $fields = ['id' => $id];
  88. foreach ($fieldNames as $field) {
  89. $fields[$field] = $post->{$field};
  90. }
  91.  
  92. $fields['tags'] = $post->tags()->lists('tag')->all();
  93.  
  94. return $fields;
  95. }
  96. }

该任务最终返回文章字段和值的键值对数组,我们将使用其返回结果用来填充文章编辑表单。如果 Post 模型未被加载(比如创建文章时),那么就会返回默认值。如果 Post 被成功加载(文章更新),那么就会从数据库获取值。

此外,还有两个额外的字段被返回,即 tagsallTagstags 是与该 Post 模型实例关联的所有标签数组;allTags 是所有标签数组。

6、添加辅助函数

我们还需要两个辅助函数,因此我们编辑 app/helpers.php 文件内容添加这两个函数:

  1. /**
  2. * Return "checked" if true
  3. */
  4. function checked($value)
  5. {
  6. return $value ? 'checked' : '';
  7. }
  8.  
  9. /**
  10. * Return img url for headers
  11. */
  12. function page_image($value = null)
  13. {
  14. if (empty($value)) {
  15. $value = config('blog.page_image');
  16. }
  17. if (! starts_with($value, 'http') && $value[0] !== '/') {
  18. $value = config('blog.uploads.webpath') . '/' . $value;
  19. }
  20.  
  21. return $value;
  22. }

checked() 方法用于在视图的复选框和单选框中设置 checked 属性。

page_image() 方法用于返回上传图片的完整路径。

7、修改 Post 模型

你可能已经注意到我们将 published_at 分割成了 publish_datepublish_time,下面我们在 Post 模型中添加这两个字段:

  1. <?php
  2. // 在 Post 类的 $dates 属性后添加 $fillable 属性
  3. protected $fillable = [
  4. 'title', 'subtitle', 'content_raw', 'page_image', 'meta_description','layout', 'is_draft', 'published_at',
  5. ];
  6.  
  7. // 然后在 Post 模型类中添加如下几个方法
  8.  
  9. /**
  10. * Return the date portion of published_at
  11. */
  12. public function getPublishDateAttribute($value)
  13. {
  14. return $this->published_at->format('M-j-Y');
  15. }
  16.  
  17. /**
  18. * Return the time portion of published_at
  19. */
  20. public function getPublishTimeAttribute($value)
  21. {
  22. return $this->published_at->format('g:i A');
  23. }
  24.  
  25. /**
  26. * Alias for content_raw
  27. */
  28. public function getContentAttribute($value)
  29. {
  30. return $this->content_raw;
  31. }

此外我们还添加了 getContentAttribute() 方法作为访问器以便返回 $this->content_raw。现在如果我们使用 $post->content  就会执行该方法。

8、修改 PostController 控制器

现在我们在 PostController 类中实现所有需要的功能。由于我们将表单验证和填充数据分散到表单请求类和 PostFormFields 类中完成,控制器的代码量将会很小:

  1. <?php
  2.  
  3. namespace App\Http\Controllers\Admin;
  4.  
  5. use App\Jobs\PostFormFields;
  6. use App\Http\Requests;
  7. use App\Http\Requests\PostCreateRequest;
  8. use App\Http\Requests\PostUpdateRequest;
  9. use App\Http\Controllers\Controller;
  10. use App\Post;
  11.  
  12. class PostController extends Controller
  13. {
  14. /**
  15. * Display a listing of the posts.
  16. */
  17. public function index()
  18. {
  19. return view('admin.post.index')
  20. ->withPosts(Post::all());
  21. }
  22.  
  23. /**
  24. * Show the new post form
  25. */
  26. public function create()
  27. {
  28. $data = $this->dispatch(new PostFormFields());
  29.  
  30. return view('admin.post.create', $data);
  31. }
  32.  
  33. /**
  34. * Store a newly created Post
  35. *
  36. * @param PostCreateRequest $request
  37. */
  38. public function store(PostCreateRequest $request)
  39. {
  40. $post = Post::create($request->postFillData());
  41. $post->syncTags($request->get('tags', []));
  42.  
  43. return redirect()
  44. ->route('admin.post.index')
  45. ->withSuccess('New Post Successfully Created.');
  46. }
  47.  
  48. /**
  49. * Show the post edit form
  50. *
  51. * @param int $id
  52. * @return Response
  53. */
  54. public function edit($id)
  55. {
  56. $data = $this->dispatch(new PostFormFields($id));
  57.  
  58. return view('admin.post.edit', $data);
  59. }
  60.  
  61. /**
  62. * Update the Post
  63. *
  64. * @param PostUpdateRequest $request
  65. * @param int $id
  66. */
  67. public function update(PostUpdateRequest $request, $id)
  68. {
  69. $post = Post::findOrFail($id);
  70. $post->fill($request->postFillData());
  71. $post->save();
  72. $post->syncTags($request->get('tags', []));
  73.  
  74. if ($request->action === 'continue') {
  75. return redirect()
  76. ->back()
  77. ->withSuccess('Post saved.');
  78. }
  79.  
  80. return redirect()
  81. ->route('admin.post.index')
  82. ->withSuccess('Post saved.');
  83. }
  84.  
  85. /**
  86. * Remove the specified resource from storage.
  87. *
  88. * @param int $id
  89. * @return Response
  90. */
  91. public function destroy($id)
  92. {
  93. $post = Post::findOrFail($id);
  94. $post->tags()->detach();
  95. $post->delete();
  96.  
  97. return redirect()
  98. ->route('admin.post.index')
  99. ->withSuccess('Post deleted.');
  100. }
  101. }

接下来唯一要做的就是创建相应视图了。

9、创建文章视图

现在我们来创建 PostController 中用到的所有视图。

首先修改已经存在的位于 resources/views/admin/post 目录下的 index.blade.php

  1. @extends('admin.layout')
  2.  
  3. @section('content')
  4. <div class="container-fluid">
  5. <div class="row page-title-row">
  6. <div class="col-md-6">
  7. <h3>Posts <small>» Listing</small></h3>
  8. </div>
  9. <div class="col-md-6 text-right">
  10. <a href="/admin/post/create" class="btn btn-success btn-md">
  11. <i class="fa fa-plus-circle"></i> New Post
  12. </a>
  13. </div>
  14. </div>
  15.  
  16. <div class="row">
  17. <div class="col-sm-12">
  18.  
  19. @include('admin.partials.errors')
  20. @include('admin.partials.success')
  21.  
  22. <table id="posts-table" class="table table-striped table-bordered">
  23. <thead>
  24. <tr>
  25. <th>Published</th>
  26. <th>Title</th>
  27. <th>Subtitle</th>
  28. <th data-sortable="false">Actions</th>
  29. </tr>
  30. </thead>
  31. <tbody>
  32. @foreach ($posts as $post)
  33. <tr>
  34. <td data-order="{{ $post->published_at->timestamp }}">
  35. {{ $post->published_at->format('j-M-y g:ia') }}
  36. </td>
  37. <td>{{ $post->title }}</td>
  38. <td>{{ $post->subtitle }}</td>
  39. <td>
  40. <a href="/admin/post/{{ $post->id }}/edit" class="btn btn-xs btn-info">
  41. <i class="fa fa-edit"></i> Edit
  42. </a>
  43. <a href="/blog/{{ $post->slug }}" class="btn btn-xs btn-warning">
  44. <i class="fa fa-eye"></i> View
  45. </a>
  46. </td>
  47. </tr>
  48. @endforeach
  49. </tbody>
  50. </table>
  51. </div>
  52. </div>
  53.  
  54. </div>
  55. @stop
  56.  
  57. @section('scripts')
  58. <script>
  59. $(function() {
  60. $("#posts-table").DataTable({
  61. order: [[0, "desc"]]
  62. });
  63. });
  64. </script>
  65. @stop

该视图很简单,就是使用文章数据填充表格然后使用 DataTables 初始化表格。

接下来,在 resources/views/admin/post 目录下新建一个 create.blade.php

  1. @extends('admin.layout')
  2.  
  3. @section('styles')
  4. <link href="/assets/pickadate/themes/default.css" rel="stylesheet">
  5. <link href="/assets/pickadate/themes/default.date.css" rel="stylesheet">
  6. <link href="/assets/pickadate/themes/default.time.css" rel="stylesheet">
  7. <link href="/assets/selectize/css/selectize.css" rel="stylesheet">
  8. <link href="/assets/selectize/css/selectize.bootstrap3.css" rel="stylesheet">
  9. @stop
  10.  
  11. @section('content')
  12. <div class="container-fluid">
  13. <div class="row page-title-row">
  14. <div class="col-md-12">
  15. <h3>Posts <small>» Add New Post</small></h3>
  16. </div>
  17. </div>
  18.  
  19. <div class="row">
  20. <div class="col-sm-12">
  21. <div class="panel panel-default">
  22. <div class="panel-heading">
  23. <h3 class="panel-title">New Post Form</h3>
  24. </div>
  25. <div class="panel-body">
  26.  
  27. @include('admin.partials.errors')
  28.  
  29. <form class="form-horizontal" role="form" method="POST" action="{{ route('admin.post.store') }}">
  30. <input type="hidden" name="_token" value="{{ csrf_token() }}">
  31.  
  32. @include('admin.post._form')
  33.  
  34. <div class="col-md-8">
  35. <div class="form-group">
  36. <div class="col-md-10 col-md-offset-2">
  37. <button type="submit" class="btn btn-primary btn-lg">
  38. <i class="fa fa-disk-o"></i>
  39. Save New Post
  40. </button>
  41. </div>
  42. </div>
  43. </div>
  44.  
  45. </form>
  46.  
  47. </div>
  48. </div>
  49. </div>
  50. </div>
  51. </div>
  52.  
  53. @stop
  54.  
  55. @section('scripts')
  56. <script src="/assets/pickadate/picker.js"></script>
  57. <script src="/assets/pickadate/picker.date.js"></script>
  58. <script src="/assets/pickadate/picker.time.js"></script>
  59. <script src="/assets/selectize/selectize.min.js"></script>
  60. <script>
  61. $(function() {
  62. $("#publish_date").pickadate({
  63. format: "mmm-d-yyyy"
  64. });
  65. $("#publish_time").pickatime({
  66. format: "h:i A"
  67. });
  68. $("#tags").selectize({
  69. create: true
  70. });
  71. });
  72. </script>
  73. @stop

这里我们引入了 Selectize 和 Pickadate 库。你可能还注意到了我们还引入了一个尚未创建的局部视图 admin.post._form。下面我们就在 resources/views/admin/post 目录下创建这个视图,在该目录先新建一个 _form.blade.php,编辑其内容如下:

  1. <div class="row">
  2. <div class="col-md-8">
  3. <div class="form-group">
  4. <label for="title" class="col-md-2 control-label">
  5. Title
  6. </label>
  7. <div class="col-md-10">
  8. <input type="text" class="form-control" name="title" autofocus id="title" value="{{ $title }}">
  9. </div>
  10. </div>
  11. <div class="form-group">
  12. <label for="subtitle" class="col-md-2 control-label">
  13. Subtitle
  14. </label>
  15. <div class="col-md-10">
  16. <input type="text" class="form-control" name="subtitle" id="subtitle" value="{{ $subtitle }}">
  17. </div>
  18. </div>
  19. <div class="form-group">
  20. <label for="page_image" class="col-md-2 control-label">
  21. Page Image
  22. </label>
  23. <div class="col-md-10">
  24. <div class="row">
  25. <div class="col-md-8">
  26. <input type="text" class="form-control" name="page_image" id="page_image" onchange="handle_image_change()" alt="Image thumbnail" value="{{ $page_image }}">
  27. </div>
  28. <script>
  29. function handle_image_change() {
  30. $("#page-image-preview").attr("src", function () {
  31. var value = $("#page_image").val();
  32. if ( ! value) {
  33. value = {!! json_encode(config('blog.page_image')) !!};
  34. if (value == null) {
  35. value = '';
  36. }
  37. }
  38. if (value.substr(0, 4) != 'http' && value.substr(0, 1) != '/') {
  39. value = {!! json_encode(config('blog.uploads.webpath')) !!} + '/' + value;
  40. }
  41. return value;
  42. });
  43. }
  44. </script>
  45. <div class="visible-sm space-10"></div>
  46. <div class="col-md-4 text-right">
  47. <img src="{{ page_image($page_image) }}" class="img img_responsive" id="page-image-preview" style="max-height:40px">
  48. </div>
  49. </div>
  50. </div>
  51. </div>
  52. <div class="form-group">
  53. <label for="content" class="col-md-2 control-label">
  54. Content
  55. </label>
  56. <div class="col-md-10">
  57. <textarea class="form-control" name="content" rows="14" id="content">{{ $content }}</textarea>
  58. </div>
  59. </div>
  60. </div>
  61. <div class="col-md-4">
  62. <div class="form-group">
  63. <label for="publish_date" class="col-md-3 control-label">
  64. Pub Date
  65. </label>
  66. <div class="col-md-8">
  67. <input class="form-control" name="publish_date" id="publish_date" type="text" value="{{ $publish_date }}">
  68. </div>
  69. </div>
  70. <div class="form-group">
  71. <label for="publish_time" class="col-md-3 control-label">
  72. Pub Time
  73. </label>
  74. <div class="col-md-8">
  75. <input class="form-control" name="publish_time" id="publish_time" type="text" value="{{ $publish_time }}">
  76. </div>
  77. </div>
  78. <div class="form-group">
  79. <div class="col-md-8 col-md-offset-3">
  80. <div class="checkbox">
  81. <label>
  82. <input {{ checked($is_draft) }} type="checkbox" name="is_draft">
  83. Draft?
  84. </label>
  85. </div>
  86. </div>
  87. </div>
  88. <div class="form-group">
  89. <label for="tags" class="col-md-3 control-label">
  90. Tags
  91. </label>
  92. <div class="col-md-8">
  93. <select name="tags[]" id="tags" class="form-control" multiple>
  94. @foreach ($allTags as $tag)
  95. <option @if (in_array($tag, $tags)) selected @endif value="{{ $tag }}">
  96. {{ $tag }}
  97. </option>
  98. @endforeach
  99. </select>
  100. </div>
  101. </div>
  102. <div class="form-group">
  103. <label for="layout" class="col-md-3 control-label">
  104. Layout
  105. </label>
  106. <div class="col-md-8">
  107. <input type="text" class="form-control" name="layout" id="layout" value="{{ $layout }}">
  108. </div>
  109. </div>
  110. <div class="form-group">
  111. <label for="meta_description" class="col-md-3 control-label">
  112. Meta
  113. </label>
  114. <div class="col-md-8">
  115. <textarea class="form-control" name="meta_description" id="meta_description" rows="6">
  116. {{ $meta_description }}
  117. </textarea>
  118. </div>
  119. </div>
  120.  
  121. </div>
  122. </div>

我们创建这个局部视图的目的是让 create 和 edit 视图可以共享它。

下面我们在同一目录下创建 edit.blade.php

  1. @extends('admin.layout')
  2.  
  3. @section('styles')
  4. <link href="/assets/pickadate/themes/default.css" rel="stylesheet">
  5. <link href="/assets/pickadate/themes/default.date.css" rel="stylesheet">
  6. <link href="/assets/pickadate/themes/default.time.css" rel="stylesheet">
  7. <link href="/assets/selectize/css/selectize.css" rel="stylesheet">
  8. <link href="/assets/selectize/css/selectize.bootstrap3.css" rel="stylesheet">
  9. @stop
  10.  
  11. @section('content')
  12. <div class="container-fluid">
  13. <div class="row page-title-row">
  14. <div class="col-md-12">
  15. <h3>Posts <small>» Edit Post</small></h3>
  16. </div>
  17. </div>
  18.  
  19. <div class="row">
  20. <div class="col-sm-12">
  21. <div class="panel panel-default">
  22. <div class="panel-heading">
  23. <h3 class="panel-title">Post Edit Form</h3>
  24. </div>
  25. <div class="panel-body">
  26.  
  27. @include('admin.partials.errors')
  28. @include('admin.partials.success')
  29.  
  30. <form class="form-horizontal" role="form" method="POST" action="{{ route('admin.post.update', $id) }}">
  31. <input type="hidden" name="_token" value="{{ csrf_token() }}">
  32. <input type="hidden" name="_method" value="PUT">
  33.  
  34. @include('admin.post._form')
  35.  
  36. <div class="col-md-8">
  37. <div class="form-group">
  38. <div class="col-md-10 col-md-offset-2">
  39. <button type="submit" class="btn btn-primary btn-lg" name="action" value="continue">
  40. <i class="fa fa-floppy-o"></i>
  41. Save - Continue
  42. </button>
  43. <button type="submit" class="btn btn-success btn-lg" name="action" value="finished">
  44. <i class="fa fa-floppy-o"></i>
  45. Save - Finished
  46. </button>
  47. <button type="button" class="btn btn-danger btn-lg" data-toggle="modal" data-target="#modal-delete">
  48. <i class="fa fa-times-circle"></i>
  49. Delete
  50. </button>
  51. </div>
  52. </div>
  53. </div>
  54. </form>
  55.  
  56. </div>
  57. </div>
  58. </div>
  59. </div>
  60.  
  61. {{-- 确认删除 --}}
  62. <div class="modal fade" id="modal-delete" tabIndex="-1">
  63. <div class="modal-dialog">
  64. <div class="modal-content">
  65. <div class="modal-header">
  66. <button type="button" class="close" data-dismiss="modal">
  67. ×
  68. </button>
  69. <h4 class="modal-title">Please Confirm</h4>
  70. </div>
  71. <div class="modal-body">
  72. <p class="lead">
  73. <i class="fa fa-question-circle fa-lg"></i>
  74. Are you sure you want to delete this post?
  75. </p>
  76. </div>
  77. <div class="modal-footer">
  78. <form method="POST" action="{{ route('admin.post.destroy', $id) }}">
  79. <input type="hidden" name="_token" value="{{ csrf_token() }}">
  80. <input type="hidden" name="_method" value="DELETE">
  81. <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
  82. <button type="submit" class="btn btn-danger">
  83. <i class="fa fa-times-circle"></i> Yes
  84. </button>
  85. </form>
  86. </div>
  87. </div>
  88. </div>
  89. </div>
  90. </div>
  91.  
  92. @stop
  93.  
  94. @section('scripts')
  95. <script src="/assets/pickadate/picker.js"></script>
  96. <script src="/assets/pickadate/picker.date.js"></script>
  97. <script src="/assets/pickadate/picker.time.js"></script>
  98. <script src="/assets/selectize/selectize.min.js"></script>
  99. <script>
  100. $(function() {
  101. $("#publish_date").pickadate({
  102. format: "mmm-d-yyyy"
  103. });
  104. $("#publish_time").pickatime({
  105. format: "h:i A"
  106. });
  107. $("#tags").selectize({
  108. create: true
  109. });
  110. });
  111. </script>
  112. @stop

至此,所有后台文章管理所需的视图都已经创建好了。

10、移除 show 路由

最后要做的收尾工作是移除显示文章详情路由 show。编辑 app/Http/routes.php 如下:

  1. // 讲如下这行
  2. resource('admin/post', 'PostController');
  3. // 修改成
  4. resource('admin/post', 'PostController', ['except' => 'show']);

好了,接下来可以在后台测试文章创建、编辑、删除了。

11、测试后台文章增删改查功能

在浏览器中访问 http://blog.app/admin/post,点击“发布文章”(New Post)按钮,进入发布文章页面:

Laravel博客文章发布页面

点击“Save New Post”按钮,发布文章成功后跳转到后台文章列表:

Laravel博客后台文章列表

第一篇即为我们刚刚发布的文章,其它是之前十分中创建博客的测试数据。接下来我们可以点击“Edit”编辑文章:

Laravel博客文章编辑页面

在该页面可以删除文章。“Save – Continue”与“Save – Finished”区别在于前者保存后会停留在编辑页面,后台保存后跳转到文章列表页。

在文章列表页我们还可以点击“View”查看文章详情:

Laravel博客文章详情页面

当然现在页面还比较丑,而且并没有对 Markdown 格式内容做处理(其实只要将 posts 表中 content_html 字段内容输出即可),下一节我们将开始优化博客前台页面。