laravel框架实战

手册

https://laravel-china.org/docs/laravel/5.5

安装与配置

安装框架

配置composer.json,修改为中国镜像,或者全局配置好

"repositories": {
    "packagist": {
        "type": "composer",
        "url": "https://packagist.phpcomposer.com"
    }
}

提前配置好国内镜像,否则很慢...

composer create-project --prefer-dist laravel/laravel video

安装提示插件

#打开packagist.org搜索 laravel-ide-helper
#提前配置好中国镜像
composer require barryvdh/laravel-ide-helper

#在config/app.php中,找到providers在最后一行增加配置项
Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider::class,

#然后再执行
php artisan ide-helper:generate

#重启编辑器,找到有一个路由文件Config::试试

配置数据库

mysql版本最好是5.7,找到.env,然后修改配置。

数据迁移

更改config/database.php,

'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',

修改成

'charset' => 'utf8',
'collation' => 'utf8_unicode_ci',

就可以支持mysql低版本了

#执行迁移,如果版本过低就会报错
php artisan migrate

这个时候会有3张表(users, password_reset, migrations)

远程配置(暂时不用)

  1. 项目传到服务器,使用phpstorm同步
  2. 配置远程数据库,使用工具连接

项目开发-后台

模型/迁移/填充操作(以后台用户表为例)

执行以下命令创建模型,如果加上 -m 代表一同创建数据迁移文件。
创建完毕之后在app目录会有一个Model目录并里面就会有模型。
database/migrations也会多一个数据迁移文件。

php artisan make:model Model/Admin -m

默认的users作为前台用户表,我们使用admins作为后台用户表,在迁移文件中,增加admins字段(username 非重,password)

执行数据迁移

php artisan migrate

找到database/factories/UserFactory.php文件,改名为AdminFactory.php修改其中的代码

$factory->define(\App\Model\Admin::class, function (Faker $faker) {
    static $password;//$password第一次为null,再一次就有值了,之后密码都一样。

    return [
        'username' => $faker->name,
        'password' => $password ?: $password = bcrypt('admin888'),
    ];
});

然后在命令行执行tinker命令来灌数据

php artisan tinker

在命令行输入,创建3条用户表的数据

factory(App\Model\Admin::class,3)->create();

控制器处理

创建控制器,会在app/Http/Controllers生成控制器

php artisan make:controller Admin/EntryController

路由处理

在控制器中建立一个login方法,然后需要设置路由访问,
不能直接通过get参数的形式访问。laravel 3 有,laravel 4 取消了这样的模式
找到routes/web.php建立路由

Route::get('/login', 'Admin\EntryController@login');

我们也可以把路由分组,路由组允许共享路由属性,例如中间件和命名空间等,我们没有必要为每个路由单独设置共有属性,共有属性会以数组的形式放到 Route::group 方法的第一个参数中。

Route::group(
['prefix'=>'admin','namespace'=>'Admin'],
function(){
    //prefix是admin,所以匹配包含 "/admin/login" 的 URL
    //namespace为Admin,会自动寻找App\Http\Controllers\Admin下面的EntryController
    Route::get('/login','EntryController@login')
});

也可以在web.php写入,把路由按照文件夹分开

include __DIR__ . '/admin/web.php';

如果get和post同时请求一个方法可以使用match

Route::match( ['get','post'],'/login', 'EntryController@login' );

定义自己的函数和类

在app目录下面建立Libraries目录,建立helper.php写入p函数
更改composer.json,在autoload选项中加入

"files": [
    "app/Libraries/helper.php"
]

还要执行 composer dump

视图处理

默认视图位置resource/views/,载入模板可以通过

return view('welcome');

也可以

return \View::make('welcome');

如果要建立文件夹views/admin/entry/login.blade.php,那么需要:

return view('admin.entry.login');

可将 Mr.Xiong 的后台模板作为我们项目所用,静态资源放入到public目录,可使用{{asset(“静态资源”)}}引入静态资源,asset函数使用当前请求的协议(HTTP或HTTPS)为资源文件生成 URL。

表单提交需要在form元素中加

{{csrf_field()}}

登陆处理

使用laravel提供的用户验证
更改config/auth.php文件

//增加admin守卫(guards),驱动为session,交给提供者(provider)的admins处理,然后再找到App\Model\Admin模型,模型需要继承Illuminate\Foundation\Auth\User
'guards' => [
    'admin' => [
        'driver'   => 'session',
        'provider' => 'admins',
    ],
    'web'   => [
        'driver'   => 'session',
        'provider' => 'users',
    ],

    'api' => [
        'driver'   => 'token',
        'provider' => 'users',
    ],
],

'providers' => [
    'users' => [
        'driver' => 'eloquent',
        'model'  => App\User::class,
    ],
    'admins' => [
        'driver' => 'eloquent',
        'model'  => App\Model\Admin::class,
    ]
],

Admin模型

<?php

namespace App\Model;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Auth\User as Authenticatable;
class Admin extends Authenticatable
{
    //
}

在控制器(先判断是否为post提交):

$status = Auth::guard('admin')->attempt([
    'username'=>Request::input('username'),
    'password'=>Request::input('password'),
]);

如果登陆成功或者登陆失败

if($status){
    //跳转到后台首页,注意写路由
    return redirect('/admin/index');
}
//重新跳转回来,并且分配闪存属性
return redirect('/admin/login')->with('error','用户名或者密码错误');

页面代码:

@if(session('error'))
    <div class="alert alert-danger">
        {{session('error')}}
    </div>
@endif

通过中间件来验证登陆

php artisan make:middleware LoginMiddleware

在中间件中写入检测是否登陆

if(!Auth::guard('admin')->check()){
    return redirect('/admin/login');
}

写入配置项app/Http/Kernel.php的$routeMiddleware

'admin.auth'=>LoginMiddleware::class

在Entry控制器构造方法调用中间件,除了登陆方法

public function __construct() {
    $this->middleware('admin.auth')->except(['login']);
}

后台视图处理

模板继承:

resources/views/admin/entry新建index.blade.php,让后台首页方法载入

return view('admin.entry.index')

然后建立resources/views/admin/layout文件夹放入继承的父模板master.blade.php找到不是共用的部分,写入占位符

@yield('content')

在index.blade.php写入以下代码

@extends('admin.layout.master')
@section('content')
中间内容
@endsection

退出处理

构建控制器方法logout

Auth::guard('admin')->logout();
return redirect('/admin/login');

更改令牌app/Model/Admin.php

 protected $rememberTokenName = '';

获取用户信息

 //html页面
 {{Auth::guard('admin')->user()->username}}

更改密码

创建控制器,配置路由指向控制器方法changePassword,更改父模板a链接,构建修改密码页面

php artisan make:controller Admin/MyController
public function password(){
    return view('admin.my.password');
}

在password.blade.php页面的form表单指向到/admin/changePassword,别忘记设计路由

表单验证

创建表单验证,在 app/Http/Requests 会生成 PasswordPost.php 文件。这样的验证时单写一个类,好处就是在任何控制器中需要处理直接引入改类即可,类似于 Tp5 中的验证器。

php artisan make:request PasswordPost

authorize方法
表单的请求类内包含了 authorize 方法。在这个方法中,你可以确认用户是否真的通过了授权,以便更新指定数据,比如是否登陆?如果自己处理验证,那么返回true

public function authorize()
{
    return true;
}

rules方法是规则,
confirme
验证字段值必须和 foo_confirmation 的字段值一致。例如,如果要验证的字段是 password,就必须和输入数据里的 password_confirmation 的值保持一致。

public function rules() {
    return [
        'old_password'          => 'sometimes|required',
        'password'              => 'sometimes|required|confirmed',
        'password_confirmation' => 'sometimes|required',
    ];
}

在password.blade.php页面显示错误消息

@if (count($errors) > 0)
    <div class="alert alert-danger">
        <ul>
            @foreach ($errors->all() as $error)
                <li>{{ $error }}</li>
            @endforeach
        </ul>
    </div>
@endif

在app/Http/Controllers/Admin/MyController::changePassword方法中,注入验证对象

public function changePassword(PasswordPost $request){
    $model = Auth::guard('admin')->user();
    $model->password = bcrypt($request->input('password'));
    $model->save();
}

改成汉语的错误提示:
在app/Http/Requests/PasswordPost.php你可以通过重写表单请求的 messages 方法来自定义错误消息。此方法必须返回一个数组,其中含有成对的属性或规则以及对应的错误消息

public function messages()
{
  return [
        'old_password.required'          => '原始密码不能为空',
        'old_password.required'          => '原始密码不能为空',
        'password.required'              => '新密码不能为空',
        'password.confirmed'             => '两次密码不一致',
        'password_confirmation.required' => '确认密码不能为空'
    ];
}

验证原密码是否正确,新建方法passwordValidator,然后在rules调用

use Validator;
use Hash;
use Auth;
public function passwordValidator(){
    Validator::extend('check_password', function ($attribute, $value, $parameters, $validator) {
        return Hash::check($value,Auth::guard('admin')->user()->password);
    });
}

rule调用,并且制定check_password规则,别忘记在messages增加提示信息

public function rules() {
    $this->passwordValidator();
    return [
        'old_password'          => 'sometimes|required|check_password',
        'password'              => 'sometimes|required|confirmed',
        'password_confirmation' => 'sometimes|required',
    ];
}

消息提示

packagist.org搜索flash组件

composer require laracasts/flash

把以下代码放在config/app.php的provider配置下

Laracasts\Flash\FlashServiceProvider::class,

在master.blade.php文件尾部写入

@include('flash::message')
<script>
    require(['bootstrap'],function($){
        $('#flash-overlay-modal').modal();
        setTimeout(function(){
            $('#flash-overlay-modal').modal('hide');

        },2000)
    });
</script>

生成消息提示模板,也可以直接更改模板,会在resources/views生成vendor目录

php artisan vendor:publish --provider="Laracasts\Flash\FlashServiceProvider"

在控制器方法中使用flash方法

public function changePassword(PasswordPost $request){
    if($request->isMethod('post')){
        $model = Auth::guard('admin')->user();
        $model->password = bcrypt($request->input('password'));
        $model->save();
        flash()->overlay('密码修改成功', 'Mr.Xiong-友情提示');
    }
    return view('admin.my.changePassword');
}

内容标签

创建资源控制器

Laravel 资源路由可以将典型的「CURD」路由指定到一个控制器上

php artisan make:controller Admin/TagController --resource

设置路由器

Route::resource('tag','TagController');
GET  => /tag           => index方法
GET  => /tag/create    => create方法
POST => /tag           => store方法
GET  => /tag/{id}      => show方法
GET  => /tag/{id}/edit => edit方法
...
添加

构建标签列表和标签添加模板,让各个方法createindex载入模板。

创建标签模型,顺便创建表

php artisan make:model Model/Tag -m

数据迁移文件中创建字段name并执行迁移

Schema::create('tags', function (Blueprint $table) {
    $table->increments('id');
    $table->string('name',50);
    $table->timestamps();
});

post请求就会到store方法,form表单写法

<form action="/admin/tag" method="post" ...

创建表单验证,会在app/Http/Requests目录生成表单验证类,按照passwordPost.php修改就好了。

php artisan make:request TagPost

更改控制器中的store方法注入

public function store(TagPost $request)
{
    if($request->isMethod('post')){
        p($request->input());
    }
}

别忘记在模板添加错误提示

@if (count($errors) > 0)
    <div class="alert alert-danger">
        <ul>
            @foreach ($errors->all() as $error)
                <li>{{ $error }}</li>
            @endforeach
        </ul>
    </div>
@endif

批量填充,默认不允许

public function store(TagPost $request,Tag $model)
{
    if($request->isMethod('post')){
        $model->create($request->all());
        flash()->overlay('添加成功','友情提示');
    }
}

需要在App\Model\Tag模型中指定guarded = []

class Tag extends Model
{
    //guarded警戒的意思,为空代表谁也不警戒
    protected $guarded = [];
}

添加成功提示

public function store(TagPost $request,Tag $model)
{
    if($request->isMethod('post')){
        $model->create($request->all());
        flash()->overlay('添加成功','友情提示');
        return redirect('/admin/tag');
    }
}
列表

列表页处理

public function index(Tag $model)
{
    $data = $model->get();
    //或者不传参数获取所有数据 一个集合
    $data = Tag::all();
    return view('admin.tag.index',compact('data'));
}

页面foreach循环

@foreach($data as $v)
    <tr>
        <td>{{$v['id']}}</td>
        <td>{{$v['name']}}</td>
        <td>
            <div class="btn-group">
                    <a href="/admin/tag/{{$v['id']}}/edit" class="btn btn-default">编辑</a>
                    <a href="javascript:del({{$v['id']}});" class="btn btn-danger">删除</a>
                </div>
        </td>
    </tr>
@endforeach
删除操作

书写js的del方法

<script>
        function del(id){
            require(['hdjs'],function(hdjs){
                hdjs.confirm('确定删除吗?',function(){
                    $.ajax({
                        url:"{{asset('/admin/tag')}}/" + id,
                        method:'DELETE',//Laravel要求,查看手册资源控制器操作处理
                        success:function(response){
                                
                        }
                    })
                });
            })
        }
    </script>
@endsection

但是监控异步发现需要使用csrf_token,可以全局异步设置csrf_token传输,在master.blade.php模板中设置以下代码

<meta name="csrf-token" content="{{ csrf_token() }}">


<script>
//放在require.js下面
    require(['jquery'],function($){
        $.ajaxSetup({
            headers: {
                'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
            }
        });
    })
</script>

控制器destroy方法操作

public function destroy($id)
{
    Tag::destroy($id);
    return response()->json(['status'=>true,'message'=>'删除成功']);
    //也可以自己定义成功失败方法在公共控制器当中,来一个$message参数即可
    return $this->success('删除成功');
}

异步的success方法

success:function(response){
    hdjs.message(response.message,'refresh');
}
编辑操作

edit方法

public function edit($id)
{
     $oldData = Tag::find($id);
     return view('admin.tag.edit',compact('oldData'));
}

模板

<input type="text" name="name" id="inputID" class="form-control" value="{{$oldData['name']}}" title="">

修改动作,需要模拟put请求,action需要带上参数,才能访问update方法

<form action="{{asset('admin/tag')}}/{{$oldData['id']}}"....
{{method_field('PUT')}}

update方法

public function update(Request $request, $id)
{
     $model = Tag::find($id);
     $model['name'] = $request->input('name');
     $model->save();
     flash()->overlay('编辑成功','友情提示');
     return redirect('/admin/tag');
}

公共继承

建立公共控制器,通过建立构造方法,然后让自控制器继承,来完成验证,需要排除一些方法。

php artisan make:controller Admin/CommonController

课程视频管理

创建控制器

php artisan make:controller Admin/LessonController --resource

配置资源路由

Route::resource('lesson','LessonController');

创建模型,顺便创建数据迁移文件,别忘记执行数据迁移。

php artisan make:model Model/Lesson -m

更改迁移文件,增加字段

Schema::create('lessons', function (Blueprint $table) {
    $table->increments('id');
    $table->string('title');
    $table->string('introduce');
    $table->string('preview');
    $table->tinyInteger('iscommend');
    $table->tinyInteger('ishot');
    $table->smallInteger('click');
    $table->timestamps();
});

执行迁移

php artisan migrate

模板处理,直接复制tag,然后修改。

在课程下面使用Vue处理视频

<script>
    require(['vue'],function(Vue){
        new Vue({
            el:'#app',
            data:{
                videos:[{title:'',path:''}]
            },
            methods:{
                add(){
                    this.videos.push({title:'',path:''});
                },
                del(k){
                    this.videos.splice(k,1);
                }
            }
        })
    })
</script>

页面可以查看数据,注意加上@符号,代表laravel不要以模板引擎方法来解析,因为这是vue模板引擎。

@{{videos}}

创建视频video模型与表

php artisan make:model Model/Video -m 

数据迁移文件

Schema::create('videos', function (Blueprint $table) {
    $table->increments('id');
    $table->string('title');
    $table->string('path');
    $table->integer('lesson_id');
    $table->timestamps();
});

执行迁移

php artisan migrate

更改Model/Video模型,允许批量填充

protected $guarded = [];

在Model/Lesson模型中,声明多表关联,也就是一门课程拥有多个视频

//允许批量填充
protected $guarded = [];

public function videos(){
    //videos表和lessons表关联自动,会自动以当前模型(lesson)的名称加上_id,也就是lesson_id就是video表的关联字段【Snake Case】
    return $this->hasMany(Video::class);
}

手动在表中插入一些模拟数据,然后测试:

$data = Lesson::find(1)->videos()->get();
p($data->toArray());
上传

上传需要用到fileinfo扩展,需要在宝塔后台安装:fileinfo,如果安装失败,在Linux工具箱,把swap交换分区调整成1024M

将以下代码放在课程添加页面,别忘记改name名称

<div class="col-sm-11">
    <div class="input-group">
        <input type="text" class="form-control" name="preview" readonly="" value="">
        <div class="input-group-btn">
            <button onclick="upImage(this)" class="btn btn-default" type="button">选择图片</button>
        </div>
    </div>
    <div class="input-group" style="margin-top:5px;">
        <img src="{{asset('images/nopic.jpg')}}" class="img-responsive img-thumbnail" width="150">
        <em class="close" style="position:absolute; top: 0px; right: -14px;" title="删除这张图片" onclick="removeImg(this)">×</em>
    </div>
</div>
<script>
    //上传图片
    function upImage(obj) {
        require(['util'], function (util) {
            options = {
                multiple: false,//是否允许多图上传
            };
            util.image(function (images) {          //上传成功的图片,数组类型

                $("[name='preview']").val(images[0]);
                $(".img-thumbnail").attr('src', images[0]);
            }, options)
        });
    }

    //移除图片
    function removeImg(obj) {
        $(obj).prev('img').attr('src', '{{asset('images/nopic.jpg')}}');
        $(obj).parent().prev().find('input').val('');
    }
</script>

图片地址可以是

<img src="/images/nopic.jpg"...

也可以使用asset('images/nopic.jpg')生成,区别就是asset函数可以生成带有http的地址,并且指向public目录,例如:http://nickblog.cn/images/nopic.jpg

<img src="{{asset('images/nopic.jpg')}}"...

修改配置项config/filesystems.php,复制一份local配置,把localstorage_path('app')改成attachment,这样attachment就会在public目录下面生成,因为storage_path()会指向storage目录,不在public目录下将来不便于访问

'attachment' => [
    'driver' => 'local',
    'root' => 'attachment',
],

建立后台控制器配合hdjs上传

php artisan make:controller Component/UploadController

建立uploaderfilesLists方法:

//上传文件  查看手册综合话题->文件存储->文件上传
public function uploader( Request $request ) {
    $upload = $request->file;
    //可以查看$upload的方法,比如获得大小,类型等...
    //$arr  = get_class_methods($upload);
    //p($arr);

    //判断是否上传成功 查看手册基础功能->请求->验证成功上传
    if($upload->isValid()){
        $path = $upload->store(date('ymd'));
        return ['valid'=>1,'message'=>asset('attachment/' . $path)];
    }
    return ['valid'=>0,'message'=>'上传失败'];

}

配置路由,不属于后台,直接在routes/web.php配置就好了

Route::match(['get', 'post'], '/component/uploader', 'Component\UploadController@uploader');
Route::match(['get', 'post'], '/component/filesLists', 'Component\UploadController@uploader');

或者写成另一种写法

Route::any('/component/uploader', 'Component\UploadController@uploader');
Route::any('/component/filesLists', 'Component\UploadController@uploader');

更改master.blade.php中的hdjs的配置,之后进行图片上传测试

//HDJS组件需要的配置
window.hdjs={};
//组件目录必须绝对路径(在网站根目录时不用设置)
window.hdjs.base = '{{asset("admin/node_modules/hdjs")}}';
//上传文件后台地址
window.hdjs.uploader = '{{asset("/component/uploader")}}';
//获取文件列表的后台地址
window.hdjs.filesLists = '{{asset("/component/filesLists")}}?';

创建标签与课程的中间表

 php artisan make:model Model/LessonTag -m

设置模型

protected $guarded = [];

数据迁移

Schema::create('lesson_tags', function (Blueprint $table) {
    $table->integer('lesson_id');
    $table->integer('tag_id');
    $table->timestamps();
});
php artisan migrate
课程添加

在LessonController中的store方法添加数据

public function store( Request $request ) {
    //添加课程
    $lesson            = new Lesson();
    $lesson->title     = $request->input( 'title' );
    $lesson->introduce = $request->input( 'introduce' );
    $lesson->preview   = $request->input( 'preview' );
    $lesson->iscommend = $request->input( 'iscommend' );
    $lesson->ishot     = $request->input( 'ishot' );
    $lesson->click     = $request->input( 'click' );
    $lesson->save();
    
    //添加中间表
    foreach ( $request->input( 'tid' ) as $tid ) {
        //添加标签与课程的中间表
        ( new LessonTag() )->create(
            [
                'lesson_id' => $lesson->id,
                'tag_id'    => $tid
            ]
        );
    }

    //添加视频
    foreach ( json_decode( $request->videos, true ) as $v ) {
        $lesson->videos()->create( 
            [
            'title'=>$v['title'],
            'path'=>$v['path']
            ]
         );
    }
    flash()->overlay('添加成功','友情提示');
    return redirect( '/admin/lesson' );
}
OSS上传

新建oss,进入到
https://oss.console.aliyun.com/overview
开通阿里云的oss,

友情提示:在阿里云存入1-2元钱,以免播放没钱,不能播放了。

在组件中新建控制器

php artisan make:controller Component/OssController

打开HDJS手册
https://www.kancloud.cn/houdunwang/hdjs/387581

复制手册中的sign方法到OssController中,注意把static去掉,然后设置信息。

class OssController extends Controller
{
   //生成供前台使用的签名
   public function sign()
   {
      //阿里云 AccessKeyId
      $id = '**********';
      //阿里云  AccessKeySecret
      $key = '********************';
      //OSS外网域名: 在阿里云后台OSS bucket中查看
      $host = 'http://******.oss-cn-qingdao.aliyuncs.com';
      //oss中本次上传存放文件的目录
      $dir = $_GET['dir'];
      function gmt_iso8601($time)
      {
         $dtStr      = date("c", $time);
         $mydatetime = new \DateTime($dtStr);
         $expiration = $mydatetime->format(\DateTime::ISO8601);
         $pos        = strpos($expiration, '+');
         $expiration = substr($expiration, 0, $pos);

         return $expiration."Z";
      }

      $now        = time();
      $expire     = 30; //设置该policy超时时间是10s. 即这个policy过了这个有效时间,将不能访问
      $end        = $now + $expire;
      $expiration = gmt_iso8601($end);

      //最大文件大小.用户可以自己设置
      $condition    = [0 => 'content-length-range', 1 => 0, 2 => 1048576000];
      $conditions[] = $condition;

      //表示用户上传的数据,必须是以$dir开始, 不然上传会失败,这一步不是必须项,只是为了安全起见,防止用户通过policy上传到别人的目录
      $start        = [0 => 'starts-with', 1 => '$key', 2 => $dir];
      $conditions[] = $start;

      $arr = ['expiration' => $expiration, 'conditions' => $conditions];
      //return;
      $policy         = json_encode($arr);
      $base64_policy  = base64_encode($policy);
      $string_to_sign = $base64_policy;
      $signature      = base64_encode(hash_hmac('sha1', $string_to_sign, $key, true));

      $response              = [];
      $response['accessid']  = $id;
      $response['host']      = $host;
      $response['policy']    = $base64_policy;
      $response['signature'] = $signature;
      $response['expire']    = $end;
      //这个参数是设置用户上传指定的前缀
      $response['dir'] = $dir;

      return json_encode($response);
   }
}

配置路由

Route::any('/component/oss', 'Component\OssController@sign');

复制以下代码页面做测试

<div class="panel panel-default father-box" v-for="(v,k) in videos" style="position: relative">
	<i class="fa fa-times-circle fa-2x son-box" @click="del(k)" style="position: absolute;top:-6px;left: -6px;display: none;cursor: pointer" aria-hidden="true"></i>
	<div class="panel-body">
		<div class="form-group">
			<label for="" class="col-sm-2 control-label">标题</label>
			<div class="col-sm-10">
				<input type="text" v-model="v.title" class="form-control" >
			</div>
		</div>
		<div class="form-group">
			<label for="" class="col-sm-2 control-label">视频地址</label>
			<div class="col-sm-10">
				<div class="input-group">
					<input class="form-control" v-model="v.path"   value="">
					<div class="input-group-btn">
						<button type="button" class="btn btn-default" :id="'selectfiles'+v.id">选择文件</button>
						<button type="button" class="btn btn-default" :id="'postfiles'+v.id">开始上传</button>
					</div>
				</div>
				<div class="progress"  style="display: none">
					<div  :id="'progress'+v.id" class="progress-bar progress-bar-success progress-bar-striped" role="progressbar" style="width: 0%">
						0%
					</div>
				</div>
			</div>
		</div>
	</div>
</div>
<script>
    require(['vue'],function (Vue) {
        new Vue({
            el:'#app',
            data:{
                videos:[]
            },
            methods:{
                add:function () {
                    var field= {title:'',path:'',id:Date.parse(new Date())};
                    this.videos.push(field);
                    setTimeout(function () {
                        upload(field);
                    },100);
                },
                del:function(k){
                    this.videos.splice(k,1);
                }
            },
        })
    })

    function upload(field) {
        require(['oss'], function (oss) {
            var uploader = oss.upload({
                //容器
                container: 'container',
                //文件选择按钮
                pick: 'selectfiles'+field.id,
                //开始上传按钮
                upButton: 'postfiles'+field.id,
                //获取签名
                serverUrl: "{{asset('/component/oss')}}?",
                //上传目录
                dir: 'file/',
                //local_name本地文件名 random_name随机文件名
                name_type: 'random_name',
                //允许上传类型
                filters: {
                    //文件类型
                    mime_types: [
                        //只允许上传图片和zip,rar文件
                        {title: "Image files", extensions: "jpg,gif,png,bmp,jpeg"},
                        {title: "Zip files", extensions: "zip,rar"},
                        {title: "Video", extensions: "mp4"}
                    ],
                    //最大只能上传10mb的文件
                    max_file_size: '10mb',
                    //不允许选取重复文件
                    prevent_duplicates: true
                },
                event: {
                    //选择文件
                    select: function (file) {
                        $('#progress'+field.id).text('0%').css({width:'0%'})
                        //$('h1').html('0%');
                    },
                    //开始上传
                    start: function (up, file) {
                        //console.log('开始上传');
                        $('#progress'+field.id).parent().show();
                    },
                    progress: function (up, file) {
                        // console.log();
                        $('#progress'+field.id).text(file.percent + '%').css({width:file.percent + '%'})
                    },
                    success: function (up, file, info) {
                        field.path = 'http://laravel-video-oss.oss-cn-beijing.aliyuncs.com/'+file.name;
                        // console.log('http://laravel-video-oss.oss-cn-beijing.aliyuncs.com/'+file.name);
                        //file.name = "{!! \Config::get('oss.host') !!}/" + file.name;
                        $('#progress'+field.id).parent().hide();
                    },
                    error: function (up, file, info) {
                        alert(info.response);
                    }
                }
            });
        })
    }
</script>
编辑(旧数据遍历不提)

代码如下

public function edit( $id ) {
    $oldData = Lesson::find($id);
    $tagData = Tag::all();
    $checkTag = DB::table('lesson_tags')->where('lesson_id',$id)->pluck('tag_id')->toArray();
    //JSON_UNESCAPED_UNICODE(中文不转为unicode ,对应的数字 256)
    //JSON_UNESCAPED_SLASHES (不转义反斜杠,对应的数字 64)
    $videos = json_encode($oldData->videos()->get()->toArray(),JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    return view('admin.lesson.edit',compact('tagData','oldData','checkTag','videos'));
}

Vue代码

require(['vue'], function (Vue) {
    new Vue({
        el: '#app',
        data: {
            //videos:JSON.parse('{{$videos}}'),//会报错,所以要原样输出
            videos: JSON.parse('{!! $videos !!}')
        },
        mounted(){
            this.videos.forEach(function(v){
                upload(v);
            });
        },
        methods: {
            add() {
                var field = {title: '', path: '', id: 'hd' + Date.parse(new Date())};
                this.videos.push(field);
                setTimeout(function () {
                    upload(field);
                }, 200);
            },
            del(k) {
                this.videos.splice(k, 1);
            }
        }
    })
})

执行修改
html页面

<form action="{{asset('admin/lesson')}}/{{$oldData['id']}}" method="post" ....
{{method_field('PUT')}}

update方法

public function update( Request $request, $id ) {
    //编辑课程
    $lesson            = Lesson::find($id);
    $lesson->title     = $request->input( 'title' );
    $lesson->introduce = $request->input( 'introduce' );
    $lesson->preview   = $request->input( 'preview' );
    $lesson->iscommend = $request->input( 'iscommend' );
    $lesson->ishot     = $request->input( 'ishot' );
    $lesson->click     = $request->input( 'click' );
    $lesson->save();

    //编辑中间表
        //删除
        DB::table('lesson_tags')->where('lesson_id',$id)->delete();
        //添加中间表
        foreach ( $request->input( 'tid' ) as $tid ) {
            //添加标签与课程的中间表
            ( new LessonTag())->create(
                [
                    'lesson_id' => $lesson->id,
                    'tag_id'    => $tid
                ]
            );
        }

    //编辑视频
        //删除视频
        $lesson->videos()->delete();
        //添加视频
        foreach ( json_decode( $request->videos, true ) as $v ) {
            $lesson->videos()->create(
                [
                    'title' => $v['title'],
                    'path'  => $v['path']
                ]
            );
        }
    flash()->overlay('编辑成功','友情提示');
    return redirect( '/admin/lesson' );
}
删除操作
public function destroy($id)
{
    Lesson::destroy($id);
    LessonTag::where('lesson_id',$id)->delete();
    Video::where('lesson_id',$id)->delete();
    return $this->success('删除成功');
}

项目开发-前台

利用vue的脚手架来创建一个webapp应用,具体流程查看 Mr.Xiong 博客
运行脚手架

cd webapp
cnpm run dev

提取公共的css,放到index.html

<link rel="stylesheet" type="text/css" href="css/bootstrap.min.css" />
<link rel="stylesheet" type="text/css" href="css/iconfont.css"/>

把css,js,images都放入到静态资源static目录
把链接指向static目录

<link rel="stylesheet" href="static/css/swiper-3.4.1.min.css" />
<link rel="stylesheet" type="text/css" href="static/css/bootstrap.min.css" />
<link rel="stylesheet" type="text/css" href="static/css/iconfont.css"/>

首页

复制一个Hello.vue改名为Home.vue,其中的name值就是在vue报错误的时候,能标明是哪个组件

<template></template>
<script>
export default {
  name: 'home',
  data () {
    return {
      
    }
  }
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped></style>

复制模板index.html的body中间的内容,放入到Home.vue中的template中,但是template中间不能有任何的Js代码。

设置路由
在router/index.js中

import Vue from 'vue'
import Router from 'vue-router'
import Home from '@/components/Home'

Vue.use(Router)

export default new Router({
  routes: [
    {
      path: '/',
      name: 'Home',
      component: Home
    }
  ]
})

设置首页的css,把index.css里面的代码复制到Home.vue中的style中,再把App.vue中的多余的元素删掉,另外“今日推荐”留一个就可以了。

列表页

内容页

和上面一样,不再赘述

增加router-link完成跳转

其实就是把之前的a,替换成router-link,例如Page.vue:

<a href="" class="iconfont back">&#xe612;</a>
//替换成
<router-link to="/" class="iconfont back">&#xe612;</router-link>

vue+swiper使用

安装插件(command+t新建Terminal)

cd webapp
npm install vue-awesome-swiper --save

在main.js让vue扩展出swiper功能

import Vue from 'vue'
import VueAwesomeSwiper from 'vue-awesome-swiper'
Vue.use(VueAwesomeSwiper)

Home.vue的html代码

<!--轮播图-->
<swiper :options="swiperOption" :not-next-tick="notNextTick" ref="mySwiper">
    <!-- slides -->
    <swiper-slide v-for="v in slide" :key="v.id">
        <router-link to="/video">
            <img :src="v.path" alt="">
        </router-link>
    </swiper-slide>
    <!-- Optional controls -->
    <!-- Optional controls -->
    <div class="swiper-pagination"  slot="pagination"></div>
</swiper>
<!--轮播图结束-->

然后在Home.vue页面的Js代码

export default {
        name: 'home',
        data() {
            return {
                slide: [
                    {id:1,path:'static/images/1.jpg'},
                    {id:2,path:'static/images/2.jpg'},
                    {id:3,path:'static/images/3.jpg'},
                    ],
                notNextTick: true,
                swiperOption: {
                    autoplay: 3000,
//                    direction : 'vertical',
                    grabCursor : true,
                    setWrapperSize :true,
                    autoHeight: true,
                    pagination : '.swiper-pagination',
                    paginationClickable :true,
                    mousewheelControl : true,
                    observeParents:true,
                }
            }
        }
    }

列表页标签页是用swiper
调整Video.vue的swiper,可以查看swiper的demo
https://surmon-china.github.io/vue-awesome-swiper/

<!-- swiper -->
<swiper :options="swiperOption">
    <swiper-slide v-for="v in tags" :key="v.id">{{v.title}}</swiper-slide>
</swiper>
<!--导航条结束-->

export default {
    name: 'video',
    data() {
        return {
            tags:[
                {id:1,'title':'PHP'},
                {id:2,'title':'JS'},
                {id:3,'title':'PHP'},
                {id:4,'title':'JS'},
                {id:5,'title':'PHP'},
                {id:6,'title':'JS'},
            ],
            swiperOption: {
                pagination: '.swiper-pagination',
                slidesPerView: 3,
                spaceBetween: 30
            }
        }
    },
}

构建接口

标签接口

CommonController的代码

abstract class CommonControlller extends Controller {
    protected function response( $data, $code = '200' ) {
        return [ 'data' => $data, 'code' => $code ];
    }
}

ContentController

class ContentController extends CommonControlller
{
    public function tags(){
        return $this->response(Tag::get());
    }
}

课程接口

Route::group(
    [ 'prefix' => 'api', 'namespace' => 'Api' ],
    function () {
        Route::match( [ 'get' ], '/tags', 'ContentController@tags' );
        Route::match( [ 'get' ], '/lessons/{tid}', 'ContentController@lessons' );
    } );
public function lessons( $tid ) {
    if ( $tid ) {
        $data = DB::table( 'lessons' )
                  ->select( 'lessons.*' )
                  ->join( 'lesson_tags', 'id', '=', 'lesson_id' )
                  ->where( [ 'tag_id' => $tid ] )
                  ->get();
    } else {
        $data = Lesson::get();
    }

    return $this->response( $data );
}

推荐课程与热门课程接口

public function commendLessons($rows){
    $data = Lesson::where(['iscommend'=>1])->limit($rows)->get();
    return $this->response($data);
}

public function hotLessons($rows){
    $data = Lesson::where(['ishot'=>1])->limit($rows)->get();
    return $this->response($data);
}
Route::match( [ 'get' ], '/commendLessons/{rows}', 'ContentController@commendLessons' );
Route::match( [ 'get' ], '/hotLessons/{rows}', 'ContentController@hotLessons' );

视频接口

public function videos($lid){
    $data = Video::where('lesson_id',$lid)->get();
    return $this->response($data);
}
Route::match( [ 'get' ], '/videos/{lid}', 'ContentController@videos' );

安装axios调用接口数据

在Npm官网搜索vue-axios
或者直接打开网址
https://www.npmjs.com/search?q=vue-axios
https://github.com/imcvampire/vue-axios
安装

cd webapp
cnpm install --save axios vue-axios

在main.js

import axios from 'axios'
import VueAxios from 'vue-axios'
Vue.use(VueAxios, axios)

解决ajax跨域访问

默认情况下前台发送Ajax是不允许跨域请求的。我们可以在后台进行相关设置然后允许前台跨域请求。

允许单个域名访问

header('Access-Control-Allow-Origin:http://www.xiongweiyang.com');

允许多个域名
$origin = isset($_SERVER['HTTP_ORIGIN'])? $_SERVER['HTTP_ORIGIN'] : '';

$allow_origin = array(
    'http://www.xiongweiyang.com',
    'http://www.xiongweiyang.com'
);

if(in_array($origin, $allow_origin)){
 header('Access-Control-Allow-Origin:'.$origin);
}

允许所有域名请求,放入到router/api.php中
header('Access-Control-Allow-Origin:*');

推荐课程与热门课程接口调用

data() {
    return {
        slide: [
            {id:1,path:'static/images/1.jpg'},
            {id:2,path:'static/images/2.jpg'},
            {id:3,path:'static/images/3.jpg'},
            ],
        notNextTick: true,
        swiperOption: {
            autoplay: 3000,
//                    direction : 'vertical',
            grabCursor : true,
            setWrapperSize :true,
            autoHeight: true,
            pagination : '.swiper-pagination',
            paginationClickable :true,
            mousewheelControl : true,
            observeParents:true,
        },
        commend:[],
        hot:[],
    }

},
mounted(){
    this.axios.get('http://test.com/api/commendLessons/4').then((response)=>{
        this.commend = response.data.data;
    });
    this.axios.get('http://test.com/api/hotLessons/4').then((response)=>{
        this.hot = response.data.data;
    });
},
<router-link :to="{name:'Page',params:{lid:v.id}}" v-for="v in commend" :key="v.id">
    <img :src="v.preview" alt="" />
    <i class="iconfont icon-bofang"></i>
    <span class="time">22:56</span>
    <span class="title">{{v.title}}</span>
</router-link>
<router-link :to="{name:'Page',params:{lid:v.id}}" v-for="v in hot" :key="v.id">
    <img :src="v.preview"/>
</router-link>

路由

export default new Router({
    routes: [
        {
            path: '/',
            name: 'Home',
            component: Home
        },
        {
            //?在正则表达式当中表示可有可无
            path: '/video/:tid?',
            name: 'Video',
            component: Video
        },
        {
            path: '/Page/:lid',
            name: 'Page',
            component: Page
        }
    ]
})

视频Page的接口调用

<li v-for="v in videos"><a @click.prevent="play(v)">{{v.title}}</a></li>
export default {
    name: 'Page',
    data() {
        return {
            videos:[],
            current:{}
        }
    },
    mounted(){
        let lid = this.$route.params.lid;
        this.axios.get('http://test.com/api/videos/' + lid).then((response)=>{
            if(response.status==200 && response.data.code==200){
                this.videos = response.data.data;
                this.current = this.videos[0];
            }else{
                alert('视频获取失败,稍后再试');
            }
        })
    },
    methods:{
         play(v){
         this.current = v;
        }
    }

}

默认第一个视频

<video :src="current.path" controls="controls"></video>
<h1>{{current.title}}</h1>

返回按钮处理

<a href="" class="iconfont back" @click.prevent="back()">&#xe612;</a>

methods:{
    play(v){
        this.current = v;
    },
    back(){
        this.$router.back();
    }
}

获取标签

<!--导航条-->
<!-- swiper -->
<swiper :options="swiperOption">
    <swiper-slide v-for="v in tags" :key="v.id">
        <router-link :to="{name:'Video',params:{tid:v.id}}" >
            {{v.name}}
        </router-link>
    </swiper-slide>
</swiper>
<!--导航条结束-->

<!--视频列表-->
<ul id="videolist">
    <li v-for="v in lessons">
        <router-link :to="{name:'Page',params:{lid:v.id}}" class="pic">
            <img :src="v.preview"/>
            <span>08:26</span>
            <i class="iconfont icon-bofang"></i>
        </router-link>
        <a href="" class="title">{{v.title}}</a>
    </li>
</ul>
<!--视频列表结束-->
export default {
    name: 'video',
    data() {
        return {
            tags: [],
            lessons: [],
            swiperOption: {
                pagination: '.swiper-pagination',
                slidesPerView: 3,
                spaceBetween: 30
            }
        }
    },
    mounted() {
        this.loadData();
    },
    watch:{
        '$route'(to,from){
            this.loadData();
        }
    },
    methods:{
        loadData(){
            this.axios.get('http://test.com/api/tags').then((response) => {
                this.tags = response.data.data;
            })
            let tid = this.$route.params.tid;
            tid = tid ? tid : 0;
            this.axios.get('http://test.com/api/lessons/' + tid).then((response) => {
                this.lessons = response.data.data;
            })
        }
    },
}

过渡动画效果

bootcdn.cn搜搜animate.css,在index.html中引入https://cdn.bootcss.com/animate.css/3.5.2/animate.min.css

在App.vue设置

<transition enter-active-class="animated tada">
    <router-view></router-view>
</transition>

编译

把所有代码上传到服务器,然后设置好地址,

cnpm run build
//生成线上运行的编译文件

让子域名绑定到dist目录

打包APP

注册登陆
http://www.apicloud.com/signin
1. 左侧创建应用
2. 选择web
3. 输入你上线项目的网址;
4. 选择右侧的云编译
5. 选择Android,然后选择正式版;点击云编译
6. 正式版要求上传证书;
7. 选择右上角的一键上传证书
8. 密码填写就可以了;
9. 找到左侧的端设置然后上传图片;
9. 再次云编译就可以上传证书了;
10. 等一会就编译好了,就可以安装了。