毕业设计

来源:互联网 发布:淘宝网企业店铺标志 编辑:程序博客网 时间:2024/06/05 15:57
发表文章:
1.后台:
(1)Model
Article:
<?php
namespace App\Models;


class Article extends Model
{
    protected $table = 'article';
    protected $primaryKey = 'article_id';
    public $timestamps = true;


    public function user()
    {
        return $this->belongsTo('App\Models\User', 'user_id', 'user_id');
    }


    public function medias()
    {
        return $this->belongsToMany('App\Models\Media', 'article_media', 'article_id', 'media_id');
    }


    public function scopeActived($query)
    {
        return $query->where('article.is_active', 1)
            ->where('article.is_deleted', 0);
    }


    public function getArticleList(array $params)
    {
        $query = $this->newQuery();
        $query->select([
            'article_id',
            'user_id',
            'title',
            'is_active',
            'created',
            'updated',
        ])->orderBy('created', 'DESC');


        $articleId = trim(array_get($params, 'article_id'));
        if ($articleId !== '') {
            $query->where('article_id', $articleId);
        }


        $userId = trim(array_get($params, 'user_id'));
        if ($userId !== '') {
            $query->where('user_id', $userId);
        }


        $title = array_get($params, 'title');
        if (trim($title !== '')) {
            $title = '%' . db_escape($title) . '%';
            $query->whereRaw('title LIKE ? ESCAPE "\\\\"', [$title]);
        }


        $isActive = trim(array_get($params, 'status'));
        if ($isActive !== '') {
            $query->where('is_active', $isActive);
        }


        $query->where('is_deleted', 0);


        $pageSize = array_get($params, 'page_size');
        return $query->paginate($pageSize);
    }


    public function updateWithCallback(callable $callback)
    {
        $connection = $this->getConnection();
        $connection->beginTransaction();
        try {
            $this->save();


            if ($callback) {
                call_user_func($callback, $this);
            }


            $connection->commit();
        } catch (\Exception $e) {
            $connection->rollBack();
            return false;
        }


        return true;
    }


    public function getArticles(array $params)
    {
        $query = self::actived();;


        $query->with([
            'user',
            'medias'
        ]);


        $query->select([
            'article_id' => 'article.article_id',
            'title' => 'article.title',
            'user_id' => 'article.user_id',
            'text_content' => 'text_content',
            'favorited_count' => 'article.favorited_count',
            'created' => 'article.created',
            'updated' => 'article.updated'
        ]);


        $query->orderBy('article.created', 'desc');


        $pageSize = array_get($params, 'page_size', 20);
        $page = array_get($params, 'page', 1);
        $query->forPage($page, $pageSize);


        return $query->get();
    }


    public function updateArticleViewCount($userId, $articleId)
    {
        $connection = $this->getConnection();
        $connection->beginTransaction();


        try {
            $logViewArticle = new LogArticle();
            $logViewArticle['article_id'] = $articleId;
            $logViewArticle['user_id'] = $userId;
            $logViewArticle['ip'] = get_ip(true);
            $logViewArticle['user_agent'] = $_SERVER['HTTP_USER_AGENT'];
            $logViewArticle->save();
            $article = Article::find($articleId);
            $article['review_count'] = $article['review_count'] + 1;
            $article['updated'] = now();
            $article->save();
            $connection->commit();
        } catch (\Exception $e) {
            $connection->rollBack();
        }
    }


    public function getUserFavoritedArticleCount($userId)
    {
        $query = self::actived();
        $count = $query->selectRaw('COUNT(article.article_id) as count')
            ->join('user_article', 'article.article_id', '=', 'user_article.article_id')
            ->where('user_article.user_id', $userId)->count();


        return $count;
    }


    public function getUserArticleCount($userId, $actived = false)
    {
        if ($actived) {
            $query = self::actived();
        } else {
            $query = $this->newQuery();
            $query->where('is_deleted', 0);
        }


        $count = $query->selectRaw('COUNT(article_id) as count')
            ->where('user_id', $userId)->count();


        return $count;
    }


    public function getUserArticles($userId, array $params, $isCount = false)
    {
        $query = $this->newQuery();
        $query->with([
                'user',
                'medias'
            ])
            ->select([
                'article.article_id',
                'article.user_id',
                'article.title',
                'article.text_content',
                'article.review_count',
                'article.favorited_count',
                'article.created',
            ])
            ->where('article.user_id', $userId)
            ->where('article.is_deleted', 0)
            ->orderBy('article.created', 'desc');


        $isActive = array_get($params, 'is_active');
        if ($isActive !== null) {
            $query->where('is_active', $isActive);
        }


        if ($isCount) {
            return $query->count();
        } else {
            $offset = array_get($params, 'offset');
            $limit = array_get($params, 'limit');
            if ($offset && $limit) {
                return $query->offset($offset)->limit($limit)->get();
            } else {
                $page = array_get($params, 'page', 1);
                $pageSize = array_get($params, 'page_size', 10);
                return $query->paginate($pageSize, ['*'], 'page', $page);
            }
        }
    }


    public function getUserFavoritedArticles($userId, array $params, $isCount = false)
    {
        $query = $this->newQuery();
        $query->select([
                'article.article_id',
                'article.user_id as user_id',
                'article.title',
                'article.content',
                'article.text_content',
                'article.preview_content',
                'article.review_count',
                'article.favorited_count',
                'article.created'
            ])
            ->join('user_article', 'article.article_id', '=', 'user_article.article_id')
            ->where('user_article.user_id', $userId)
            ->where('article.is_active', 1)
            ->where('article.is_deleted', 0)
            ->orderBy('user_article.created', 'desc');


        if ($isCount) {
            return $query->count();
        } else {
            $offset = array_get($params, 'offset');
            $limit = array_get($params, 'limit');
            if ($offset && $limit) {
                return $query->offset($offset)->limit($limit)->get();
            } else {
                $page = array_get($params, 'page', 1);
                $pageSize = array_get($params, 'page_size', 10);
                return $query->paginate($pageSize, ['*'], 'page', $page);
            }
        }
    }
}


(2)Route:
<?php


/*
|--------------------------------------------------------------------------
| Application Routes
|--------------------------------------------------------------------------
|
| Here is where you can register all of the routes for an application.
| It's a breeze. Simply tell Laravel the URIs it should respond to
| and give it the controller to call when that URI is requested.
|
*/


Route::get('/', 'HomeController@index');


/*Route::controllers([
    'auth' => 'Auth\AuthController',
]);*/


Route::group(['prefix' => 'auth'], function () {
    Route::get('login', 'Auth\AuthController@getLogin');
    Route::post('login', 'Auth\AuthController@postLogin');
    Route::get('logout', 'Auth\AuthController@getLogout');
});


Route::group(['prefix' => 'admin'], function () {
    Route::get('list', 'AdminController@index');
});
Route::controllers(['admin' => 'AdminController']);


Route::controllers(['role' => 'RoleController']);


Route::group(['prefix' => 'log'], function () {
    Route::get('admin', 'LogController@getAdminLogs');
    Route::get('user', 'LogController@getUserLogs');
    Route::get('api', 'LogController@getApiLogs');
});




Route::group(['prefix' => 'order'], function () {
    Route::get('list', 'OrderController@index');
});
Route::controllers(['order' => 'OrderController']);


Route::group(['prefix' => 'transaction'], function () {
    Route::get('list', 'TransactionController@index');
});
Route::controllers(['transaction' => 'TransactionController']);


Route::group(['prefix' => 'settlement'], function () {
    Route::get('list', 'SettlementController@index');
});
Route::controllers(['settlement' => 'SettlementController']);


Route::group(['prefix' => 'user'], function () {
    Route::get('list', 'UserController@index');
});
Route::controllers(['user' => 'UserController']);


Route::group(['prefix' => 'user-verify'], function () {
    Route::get('list', 'UserVerifyController@index');
});
Route::controllers(['user-verify' => 'UserVerifyController']);


Route::group(['prefix' => 'service', 'namespace' => 'Service'], function () {
    Route::controllers(['cdn' => 'QiniuCdnController']);
    Route::controllers(['user' => 'UserController']);
    Route::controllers(['order' => 'OrderController']);
    Route::controllers(['product' => 'ProductController']);
    Route::controllers(['report' => 'ReportController']);
    Route::controllers(['article' => 'ArticleController']);
    Route::controllers(['app' => 'AppController']);
});


Route::group(['prefix' => 'product'], function () {
    Route::get('list', 'ProductController@index');
});
Route::controllers(['product' => 'ProductController']);


Route::controllers(['setting' => 'SettingController']);


Route::group(['prefix' => 'payment-method'], function () {
    Route::get('list', 'PaymentMethodController@index');
});
Route::controllers(['payment-method' => 'PaymentMethodController']);


Route::group(['prefix' => 'page'], function () {
    Route::get('list', 'PageController@index');
});
Route::controllers(['page' => 'PageController']);


Route::controllers(['account' => 'AccountController']);


Route::controllers(['tag' => 'TagController']);


Route::controllers(['word' => 'WordController']);


Route::group(['prefix' => 'seller'], function () {
    Route::get('list', 'SellerController@index');
});


Route::group(['prefix' => 'article'], function () {
    Route::get('list', 'ArticleController@index');
});
Route::controllers(['article' => 'ArticleController']);


Route::controllers(['store' => 'StoreController']);


Route::controllers(['comment' => 'CommentController']);


Route::controllers(['app' => 'AppController']);


Route::controllers(['category' => 'CategoryController']);


(3)blade
article-list
@extends('layout.app')


@section('scripts')
<script type="text/javascript" src="{{ asset('static/libs/require.js') }}"
    data-main="{{ asset('static/js/article/list') }}"></script>
@endsection


@section('stylesheets')
@parent
@stop


@section('content')
<div class="container-fluid">
    <div class="row">
        <div class="col-sm-10 col-sm-offset-1">
            <ol class="breadcrumb">
                <li><a href="/">首页</a></li>
                <li class="active">文章列表</li>
            </ol>
            <div class="panel panel-default">
                <div class="panel-body">
                    <form class="form-inline filter-form" action="{{ url('article/list') }}" method="get">
                        <div class="form-group">
                            <label for="articleId">文章ID</label>
                            <input type="text" class="form-control" name="article_id"
                                placeholder=""
                                value="{{ array_get($search, 'article_id') }}">
                        </div>
                        <div class="form-group">
                            <label for="userId">用户ID</label>
                            <input type="text" class="form-control" name="user_id"
                                placeholder=""
                                value="{{ array_get($search, 'user_id') }}">
                        </div>
                        <div class="form-group">
                            <label for="title">标题</label>
                            <input type="text" class="form-control" id="title" name="title"
                                placeholder=""
                                value="{{ array_get($search, 'title') }}">
                        </div>
                        <div class="form-group">
                            <label for="status">状态</label>
                            <select class="form-control" id="status" name="status">
                                <option value="">全部</option>
                                <option value="1" {{ ('1' == array_get($search, 'status')) ? 'selected="selected"' : '' }}>{{ trans('common.status.active') }}</option>
                                <option value="0" {{ ('0' == array_get($search, 'status')) ? 'selected="selected"' : '' }}>{{ trans('common.status.inactive') }}</option>
                            </select>
                        </div>
                        <div class="form-group">
                            <button type="submit" class="btn btn-default">搜索</button>
                        </div>
                    </form>
                </div>
            </div>
            @if (app('acl')->isGranted('create', 'article'))
            <div class="clearfix">
                <a href="{{ url('article/create') }}" class="btn btn-primary">添加文章</a>
            </div>
            @endif
            <div class="table-responsive">
                <table id="article-list" class="table">
                    <thead>
                        <tr>
                            <th>文章ID</th>
                            <th>用户ID</th>
                            <th>标题</th>
                            <th>状态</th>
                            <th>创建时间</th>
                            <th>最近更新</th>
                            <th>操作</th>
                        </tr>
                    </thead>
                    <tbody>
                        @if (count($articles))
                            @foreach ($articles as $article)
                            <tr>
                                <td>
                                    <a href="{{ url('article/detail?article_id=' . $article['article_id']) }}">
                                    {{ $article['article_id'] }}
                                    </a>
                                </td>
                                <td>
                                    @if ($article['user_id'] == 0)
                                    {{ '系统用户' }}
                                    @else
                                    <a href="{{ url('user/detail?user_id=' . $article['user_id']) }}">
                                    {{ $article['user_id'] }}
                                    </a>
                                    @endif
                                </td>
                                <td>
                                    <a href="{{ url('article/detail?article_id=' . $article['article_id']) }}">
                                    {{ $article['title'] }}
                                    </a>
                                </td>
                                <td>
                                    @if($article['is_active'])
                                    {{ trans('common.status.active') }}
                                    @else
                                    {{ trans('common.status.inactive') }}
                                    @endif
                                </td>
                                <td>{{ $article['created'] }}</td>
                                <td>{{ $article['updated'] }}</td>
                                <td>
                                @if (app('acl')->isGranted('view', 'article'))
                                <a title="" href="{{ url('article/detail?article_id=' . $article['article_id']) }}"><span class="glyphicon glyphicon-info-sign"></span></a>
                                @endif
                                @if (app('acl')->isGranted('edit', 'article'))
                                <a href="{{ url('article/edit?article_id=' . $article['article_id']) }}"><span class="glyphicon glyphicon-edit"></span></a>
                                @endif
                                @if ($article['user_id'] == 0 && app('acl')->isGranted('delete', 'article'))
                                <a class="action-delete" href="javascript:void(0)" data-id="{{ $article['article_id'] }}"><span class="glyphicon glyphicon-trash"></span></a>
                                @endif
                                </td>
                            </tr>
                            @endforeach
                        @else
                        <tr class="text-center">
                            <td colspan="7">无记录</td>
                        </tr>
                        @endif
                    </tbody>
                </table>
            </div>
            {!! $articles->appends($search)->render() !!}
        </div>
    </div>
</div>
@endsection


create-blade
@extends('layout.app')


@section('scripts')
<script type="text/javascript" src="{{ asset('static/libs/require.js') }}"
    data-main="{{ asset('static/js/article/create') }}"></script>
@endsection


@section('stylesheets')
@parent
<link rel="stylesheet" type="text/css" href="{{ url('static/css/common/editor.css') }}" >
<link rel="stylesheet" type="text/css" href="{{ url('static/css/article/create.css') }}" >
@stop


@section('content')
<div class="container-fluid">
    <div class="row">
        <div class="col-sm-10 col-sm-offset-1">
            <ol class="breadcrumb">
                <li><a href="/">首页</a></li>
                <li><a href="{{ url('article/list') }}">文章列表</a></li>
                <li class="active">添加文章</li>
            </ol>
            <div class="panel panel-default">
                <div class="panel-heading">添加文章</div>
                <div class="panel-body">
                    <div id="errors-box" class="alert alert-danger {{ count($errors) > 0 ? '' : 'hidden' }}">
                        <strong>Whoops!</strong> There were some problems with your input.<br>
                        @if (count($errors) > 0)
                        <ul>
                            @foreach ($errors->all() as $error)
                                <li>{{ $error }}</li>
                            @endforeach
                        </ul>
                        @endif
                    </div>
                    <form class="form-horizontal" id="create-form">
                        <input type="hidden" name="_token" value="{{ csrf_token() }}">
                        <input type="hidden" name="user_id" id="user-id" value="0">
                        <div class="form-group">
                            <label class="col-sm-3 col-md-2 control-label">标题</label>
                            <div class="col-sm-8">
                                <input type="text" class="form-control" name="title" value="{{ old('title') }}">
                                <p class="help-block">格式:1到120个字符</p>
                            </div>
                        </div>
                        <div class="form-group">
                            <label class="col-sm-3 col-md-2 control-label">内容</label>
                            <div class="col-sm-9 col-md-10 rich-editor">
                                <div class="content-wrapper">
                                    <div id="article-content" class="edit-content">
                                    </div>
                                    <div class="media-container">
                                        <div id="media-draggable-container">
                                            <div id="drag-button"></div>
                                            <div id="drop-container">
                                                <div class="media-header">
                                                    <h4>直接拖拽您需要上传的文件到虚线框内</h4>
                                                    <h4>或点击下方按钮</h4>
                                                </div>
                                                <div id="media-type-list" class="row">
                                                    <div class="col-xs-3 col-sm-2 col-sm-offset-2 text-center">
                                                        <a class="btn-primary media-type-button" href="javascript:void(0)" id="text-button">
                                                            <img class="btn-image" src="{{ asset('/static/image/words_108x79.png') }}">
                                                            <span class="btn-text">文本</span>
                                                        </a>
                                                    </div>
                                                    <div class="col-xs-3 col-sm-2 text-center">
                                                        <a class="btn-primary media-type-button" href="javascript:void(0)" id="image-button">
                                                            <img class="btn-image" src="{{ asset('/static/image/pic_108x79.png') }}">
                                                            <span class="btn-text">图片</span>
                                                        </a>
                                                    </div>
                                                    <div class="col-xs-3 col-sm-2 text-center" >
                                                        <a class="btn-primary media-type-button" href="javascript:void(0)" id="video-button">
                                                            <img class="btn-image" src="{{ asset('/static/image/video_109x79.png') }}">
                                                            <span class="btn-text">视频</span>
                                                        </a>
                                                    </div>
                                                    <div class="col-xs-3 col-sm-2 text-center">
                                                        <a class="btn-primary media-type-button" data-target="#embDialog" data-toggle="modal"
                                                            id="embeds-button">
                                                            <img class="btn-image" src="{{ asset('/static/image/link_108x79.png') }}">
                                                            <span class="btn-text">链接</span>
                                                        </a>
                                                    </div>
                                                </div>
                                            </div>
                                        </div>
                                        <div class="modal fade" id="embDialog" tabindex="-1" role="dialog">
                                            <div class="modal-dialog" role="document">
                                                <div class="modal-content">
                                                    <div class="modal-header">
                                                        <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
                                                        <h4 class="modal-title" id="myModalLabel">嵌入外部视频</h4>
                                                    </div>
                                                    <div class="modal-body">
                                                    <input type="text" id="embeds-input" class="form-control">
                                                    </div>
                                                    <div class="modal-footer">
                                                        <button type="button" class="btn btn-default archist-cancel-btn" data-dismiss="modal">取消</button>
                                                        <button type="button" id="commit-embeds" class="btn btn-primary archist-submit-btn">确定</button>
                                                    </div>
                                                </div>
                                            </div>
                                        </div>
                                    </div>
                                </div>
                                <div id="sortable" class="sortable" data-spy="affix" data-target=".rich-editor">
                                    <ul id="sortitem-container"></ul>
                                </div>
                            </div>
                        </div>
                        <div class="form-group">
                            <label class="col-sm-3 col-md-2 control-label">状态</label>
                            <div class="col-sm-8">
                                <label class="radio-inline">
                                    <input name="is_active" type="radio" value="1" checked="checked">
                                    {{ trans('common.status.active') }}
                                </label>
                                <label class="radio-inline">
                                    <input name="is_active" type="radio" value="0">
                                    {{ trans('common.status.inactive') }}
                                </label>
                            </div>
                        </div>
                        <div class="form-group">
                            <div class="col-sm-6 col-sm-offset-3 col-md-4 col-md-offset-2">
                                <input type="button" id="save-article" class="btn btn-primary" value="保存">
                            </div>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>
@endsection


detail-blade
@extends('layout.app')


@section('scripts')
<script type="text/javascript" src="{{ asset('static/libs/require.js') }}"
    data-main="{{ asset('static/js/article/detail') }}"></script>
@endsection


@section('stylesheets')
@parent
<link rel="stylesheet" type="text/css" href="{{ url('static/css/common/editor.css') }}" >
<link rel="stylesheet" type="text/css" href="{{ url('static/css/article/detail.css') }}" >
@stop


@section('content')
<div class="container-fluid">
    <div class="row">
        <div class="col-sm-10 col-sm-offset-1">
            <ol class="breadcrumb">
                <li><a href="/">首页</a></li>
                <li><a href="{{ url('article/list') }}">文章列表</a></li>
                <li class="active">文章详情</li>
            </ol>
            <div class="panel panel-default">
                <div class="panel-heading">文章详情</div>
                <div class="panel-body">
                    <form class="form-horizontal">
                        <input type="hidden" name="_token" value="{{ csrf_token() }}">


                        <div class="form-group">
                            <label class="col-sm-2 col-md-2 control-label">标题</label>
                            <div class="col-sm-8">
                                <p class="form-control-static">
                                    {{ $article['title'] }}
                                </p>
                            </div>
                        </div>


                        <div class="form-group">
                            <label class="col-sm-2 col-md-2 control-label">用户</label>
                            <div class="col-sm-8">
                                <p class="form-control-static">
                                    @if ($article->user_id)
                                    <a href="{{ url('user/detail?user_id=' . $article['user_id']) }}">{{ $article['user_id'] }} ({{ $article->user->getDisplayName() }})</a>
                                    @else
                                    系统用户
                                    @endif
                                </p>
                            </div>
                        </div>


                        <div class="form-group">
                            <label class="col-sm-2 col-md-2 control-label">内容</label>
                            <div class="col-sm-9">
                                <div class="content-wrapper">
                                    <div id="article-content" class="edit-content">
                                        {!! $article['content'] !!}
                                    </div>
                                </div>
                            </div>
                        </div>
                        <div class="form-group">
                            <label class="col-sm-2 col-md-2 control-label">状态</label>
                            <div class="col-sm-8">
                                <p class="form-control-static">
                                    {{ trans('common.status.' . ($article['is_active'] ? 'active' : 'inactive')) }}
                                </p>
                            </div>
                        </div>
                        <div class="form-group">
                            <label class="col-sm-2 col-md-2 control-label">浏览量</label>
                            <div class="col-sm-8">
                                <p class="form-control-static">
                                    {{ $article['review_count'] }}
                                </p>
                            </div>
                        </div>
                        <div class="form-group">
                            <label class="col-sm-2 col-md-2 control-label">收藏量</label>
                            <div class="col-sm-8">
                                <p class="form-control-static">
                                    {{ $article['favorited_count'] }}
                                </p>
                            </div>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>
@endsection


edit-blade
@extends('layout.app')


@section('scripts')
<script type="text/javascript" src="{{ asset('static/libs/require.js') }}"
    data-main="{{ asset('static/js/article/detail') }}"></script>
@endsection


@section('stylesheets')
@parent
<link rel="stylesheet" type="text/css" href="{{ url('static/css/common/editor.css') }}" >
<link rel="stylesheet" type="text/css" href="{{ url('static/css/article/detail.css') }}" >
@stop


@section('content')
<div class="container-fluid">
    <div class="row">
        <div class="col-sm-10 col-sm-offset-1">
            <ol class="breadcrumb">
                <li><a href="/">首页</a></li>
                <li><a href="{{ url('article/list') }}">文章列表</a></li>
                <li class="active">文章详情</li>
            </ol>
            <div class="panel panel-default">
                <div class="panel-heading">文章详情</div>
                <div class="panel-body">
                    <form class="form-horizontal">
                        <input type="hidden" name="_token" value="{{ csrf_token() }}">


                        <div class="form-group">
                            <label class="col-sm-2 col-md-2 control-label">标题</label>
                            <div class="col-sm-8">
                                <p class="form-control-static">
                                    {{ $article['title'] }}
                                </p>
                            </div>
                        </div>


                        <div class="form-group">
                            <label class="col-sm-2 col-md-2 control-label">用户</label>
                            <div class="col-sm-8">
                                <p class="form-control-static">
                                    @if ($article->user_id)
                                    <a href="{{ url('user/detail?user_id=' . $article['user_id']) }}">{{ $article['user_id'] }} ({{ $article->user->getDisplayName() }})</a>
                                    @else
                                    系统用户
                                    @endif
                                </p>
                            </div>
                        </div>


                        <div class="form-group">
                            <label class="col-sm-2 col-md-2 control-label">内容</label>
                            <div class="col-sm-9">
                                <div class="content-wrapper">
                                    <div id="article-content" class="edit-content">
                                        {!! $article['content'] !!}
                                    </div>
                                </div>
                            </div>
                        </div>
                        <div class="form-group">
                            <label class="col-sm-2 col-md-2 control-label">状态</label>
                            <div class="col-sm-8">
                                <p class="form-control-static">
                                    {{ trans('common.status.' . ($article['is_active'] ? 'active' : 'inactive')) }}
                                </p>
                            </div>
                        </div>
                        <div class="form-group">
                            <label class="col-sm-2 col-md-2 control-label">浏览量</label>
                            <div class="col-sm-8">
                                <p class="form-control-static">
                                    {{ $article['review_count'] }}
                                </p>
                            </div>
                        </div>
                        <div class="form-group">
                            <label class="col-sm-2 col-md-2 control-label">收藏量</label>
                            <div class="col-sm-8">
                                <p class="form-control-static">
                                    {{ $article['favorited_count'] }}
                                </p>
                            </div>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>
@endsection


list.js:
requirejs(['../config'], function () {
    require(['jquery', 'bootbox', 'bootstrap', 'bootstrap-growl'], function ($, bootbox) {
        bootbox.setDefaults({
            locale: 'zh_CN'
        });


        $(function () {
            $('#article-list').on('click', '.action-delete', function () {
                var articleId = $(this).data('id');
                bootbox.confirm({
                    locale: 'zh_CN',
                    title: '删除',
                    message: '确认删除该文章?',
                    size: 'small',
                    callback: function (result) {
                        if (!result) {
                            return;
                        }


                        $.ajax({
                            url: '/service/article/delete',
                            type: 'POST',
                            data: {
                                article_id: articleId
                            },
                        }).done(function (response) {
                            if (response.status === 'SUCCESS') {
                                location.reload();
                            } else if (response.status == 'FAILED') {
                                $.bootstrapGrowl(response.body.error, {
                                    type: 'danger',
                                    offset: {
                                        from: 'top',
                                        amount: 80
                                    }
                                });
                            } else {
                                $.bootstrapGrowl('系统错误,请稍后再试', {
                                    type: 'danger',
                                    offset: {
                                        from: 'top',
                                        amount: 80
                                    }
                                });
                            }
                        });
                    }
                });
            });
        });
    });
});


create.js
requirejs(['../config'], function () {
    require(['jquery', 'qiniu', 'editor', 'util', 'underscore', 'bootstrap', 'bootstrap-growl'], function ($, Qiniu, Editor, util, _) {
        function saveMedia(data, callback) {
            $.ajax({
                url: '/service/article/save-media',
                type: 'POST',
                data: data
            }).done(function (response) {
                callback && callback(response);
            });
        }


        function initEditor() {
            var articleContentContainer = $('#article-content'),
                sortContainer = $('#sortitem-container'),
                form = $('#create-form'),
                articleId = $('#article-id').val(),
                $errorBox = $('#errors-box');


            var editor = Editor.init({
                sortableContainer: sortContainer,
                blockContainer: articleContentContainer
            });


            var containerUploader = Qiniu.uploader({
                runtimes: 'html5,flash,html4',
                browse_button: 'drag-button',
                container: 'media-draggable-container',
                dragdrop: true,
                drop_element: 'drop-container',
                max_file_size: '10mb',
                flash_swf_url: 'libs/plupload/Moxie.swf',
                chunk_size: '4mb',
                uptoken_url: '/service/cdn/uptoken',
                domain: APP_CONFIG.cdn.domain,
                get_new_uptoken: false,
                unique_names: true,
                auto_start: false,
                multi_selection: false,
                filters: {
                    mime_types: [
                        {
                            title : 'Image Video files',
                            extensions : 'jpg,jpeg,gif,png,mp4,ogg'
                        }
                    ]
                },
                init: {
                    FilesAdded: function (up, files) {
                        $.each(files, function (index, file) {
                            var type = file.type.split('/')[0];
                            if (type === 'video') {
                                util.getAudioDuration(file.getNative(), function (data) {
                                    if (data && data.duration > 10) {
                                        up.removeFile(file);
                                        $.bootstrapGrowl('视频超过10秒', {
                                            type: 'danger',
                                            offset: {
                                                from: 'top',
                                                amount: 80
                                            }
                                        });
                                    } else {
                                        up.start();
                                    }
                                });
                            } else if (type === 'image') {
                                up.start();
                            }
                        });
                    },
                    FileUploaded: function (up, file, info) {
                        var domain = up.getOption('domain'),
                            result = $.parseJSON(info),
                            sourceLink = 'http://' + domain + '/' + result.key,
                            type = file.type.split('/')[0],
                            blockId;


                        if (type === 'image') {
                            blockId = editor.addBlock({
                                type: Editor.BLOCK_TYPE_IMAGE,
                                src: result.key,
                                caption: ''
                            });


                            saveMedia({
                                type: 'image',
                                path: result.key,
                                article_id: articleId
                            }, function (response) {
                                if (response.status === 'SUCCESS') {
                                    editor.updateBlock(blockId, {
                                        mediaId: response.body.media_id
                                    });
                                }
                            });
                        } else if (type === 'video') {
                            blockId = editor.addBlock({
                                type: Editor.BLOCK_TYPE_VIDEO,
                                src: result.key,
                                caption: ''
                            });


                            saveMedia({
                                type: 'video',
                                path: result.key,
                                article_id: articleId
                            }, function (response) {
                                if (response.status === 'SUCCESS') {
                                    editor.updateBlock(blockId, {
                                        mediaId: response.body.media_id
                                    });
                                }
                            });
                        }
                    },
                    Error: function (up, err, errTip) {
                        $.bootstrapGrowl(errTip, {
                            type: 'danger',
                            offset: {
                                from: 'top',
                                amount: 80
                            }
                        });
                    }
                }
            });


            var imageUploader = Qiniu.uploader({
                runtimes: 'html5,flash,html4',
                browse_button: 'image-button',
                container: 'media-draggable-container',
                max_file_size: '10mb',
                flash_swf_url: 'libs/plupload/Moxie.swf',
                chunk_size: '4mb',
                drop_element: false,
                uptoken_url: '/service/cdn/uptoken',
                domain: APP_CONFIG.cdn.domain,
                get_new_uptoken: false,
                unique_names: true,
                auto_start: true,
                multi_selection: false,
                filters: {
                    mime_types: [
                        {
                            title : 'Image files',
                            extensions : 'jpg,jpeg,gif,png'
                        }
                    ]
                },
                init: {
                    FileUploaded: function (up, file, info) {
                        var domain = up.getOption('domain'),
                            result = $.parseJSON(info),
                            sourceLink = 'http://' + domain + '/' + result.key,
                            blockId;


                        blockId = editor.addBlock({
                            type: Editor.BLOCK_TYPE_IMAGE,
                            src: result.key,
                            caption: ''
                        });


                        saveMedia({
                            type: 'image',
                            path: result.key,
                        }, function (response) {
                            if (response.status === 'SUCCESS') {
                                editor.updateBlock(blockId, {
                                    mediaId: response.body.media_id
                                });
                            }
                        });
                    },
                    Error: function (up, err, errTip) {
                        $.bootstrapGrowl(errTip, {
                            type: 'danger',
                            offset: {
                                from: 'top',
                                amount: 80
                            }
                        });
                    }
                }
            });


            var videoUploader = Qiniu.uploader({
                runtimes: 'html5,flash,html4',
                browse_button: 'video-button',
                container: 'media-draggable-container',
                max_file_size: '10mb',
                flash_swf_url: 'libs/plupload/Moxie.swf',
                dragdrop: false,
                drop_element: false,
                chunk_size: '4mb',
                uptoken_url: '/service/cdn/uptoken',
                domain: APP_CONFIG.cdn.domain,
                get_new_uptoken: false,
                unique_names: true,
                auto_start: false,
                multi_selection: false,
                filters: {
                    mime_types: [
                        {
                            title : 'Video files',
                            extensions : 'mp4,ogg'
                        }
                    ]
                },
                init: {
                    FilesAdded: function (up, files) {
                        $.each(files, function (index, file) {
                            util.getAudioDuration(file.getNative(), function (data) {
                                if (data && data.duration > 10) {
                                    up.removeFile(file);
                                    $.bootstrapGrowl('视频超过10秒', {
                                        type: 'danger',
                                        offset: {
                                            from: 'top',
                                            amount: 80
                                        }
                                    });
                                } else {
                                    up.start();
                                }
                            });
                        });
                    },
                    FileUploaded: function (up, file, info) {
                        var domain = up.getOption('domain'),
                            result = $.parseJSON(info),
                            sourceLink = 'http://' + domain + '/' + result.key,
                            blockId;


                        blockId = editor.addBlock({
                            type: Editor.BLOCK_TYPE_VIDEO,
                            src: result.key,
                            caption: ''
                        });


                        saveMedia({
                            type: 'video',
                            path: result.key,
                            article_id: articleId
                        }, function (response) {
                            if (response.status === 'SUCCESS') {
                                editor.updateBlock(blockId, {
                                    mediaId: response.body.media_id
                                });
                            }
                        });
                    },
                    Error: function (up, err, errTip) {
                        $.bootstrapGrowl(errTip, {
                            type: 'danger',
                            offset: {
                                from: 'top',
                                amount: 80
                            }
                        });
                    }
                }
            });


            /*click the embeds button to add embed*/
            $('#commit-embeds').click(function() {
                var embedsValue = $.trim($('#embeds-input').val());
                if (embedsValue != '') {
                    editor.addBlock({
                        type: 'embed',
                        code: embedsValue
                    });
                };
                $('#embDialog').modal('hide');
            });


            /*click the text button to add text editor*/
            $('#text-button').click(function() {
                editor.addBlock({
                    type: 'text'
                });
            });


            var saveArticle = function() {
                var contentData = editor.getDataArray(),
                    data = form.serializeArray();


                data.push({
                    name: 'content',
                    value: JSON.stringify(contentData)
                });


                $.ajax({
                    url: '/service/article/save',
                    type: 'POST',
                    data: data,
                }).done(function (response) {
                    var errorHtml;
                    if (response.status === 'SUCCESS') {
                        location.href = '/article/list';
                    } else if (response.status == 'INVALID') {
                        errorHtml = _.map(_.flatten(_.values(response.errors)), function (error) {
                            return '<li>' + _.escape(error) + '</li>';
                        });
                        $errorBox.find('ul').remove();
                        $errorBox.append('<ul>' + errorHtml.join('') + '</ul>').removeClass('hidden');
                    } else if (response.status == 'FAILED') {
                        $errorBox.addClass('hidden');
                        $.bootstrapGrowl(response.body.error, {
                            type: 'danger',
                            offset: {
                                from: 'top',
                                amount: 80
                            }
                        });
                    } else {
                        $errorBox.addClass('hidden');
                        $.bootstrapGrowl('系统错误,请稍后再试', {
                            type: 'danger',
                            offset: {
                                from: 'top',
                                amount: 80
                            }
                        });
                    }
                });
            }


            $('#save-article').click(function() {
                saveArticle();
            });


            var richEditor = $('.rich-editor');
            $('#sortable').affix({
                offset: {
                    top: function() {
                        return richEditor.offset().top;
                    },
                    bottom: function() {
                        return $('body').outerHeight(true) - richEditor.offset().top - richEditor.outerHeight(true) + 40;
                    }
                }
            }).on('affix.bs.affix', function () {
                $(this).css({
                    left: $(this).offset().left,
                    right: 'auto'
                });
            }).on('affix-bottom.bs.affix affix-top.bs.affix', function () {
                $(this).css({
                    left: '',
                    right: ''
                });
            });
        }


        $(function () {
            if ($('#user-id').val() == 0) {
                initEditor();
            }
        });
    });
});


detail.js
requirejs(['../config'], function () {
    require(['jquery', 'layzr', 'bootstrap'], function ($, Layzr) {
        new Layzr();
    })
});


edit.js
requirejs(['../config'], function () {
    require(['jquery', 'underscore', 'layzr', 'bootbox', 'create', 'bootstrap-growl'], function ($, _, Layzr, bootbox) {
        new Layzr();
        var $errorBox = $('#errors-box');


        if ($('#user-id').val() != 0) {
            $('#save-article').on('click', function() {
                $.ajax({
                    url: '/service/article/update',
                    type: 'POST',
                    data: $('#create-form').serializeArray(),
                }).done(function (response) {
                    if (response.status === 'SUCCESS') {
                        location.href = '/article/list';
                    } else if (response.status == 'INVALID') {
                        errorHtml = _.map(_.flatten(_.values(response.errors)), function (error) {
                            return '<li>' + _.escape(error) + '</li>';
                        });
                        $errorBox.find('ul').remove();
                        $errorBox.append('<ul>' + errorHtml.join('') + '</ul>').removeClass('hidden');
                    } else if (response.status == 'FAILED') {
                        $errorBox.addClass('hidden');
                        $.bootstrapGrowl(response.body.error, {
                            type: 'danger',
                            offset: {
                                from: 'top',
                                amount: 80
                            }
                        });
                    } else {
                        $errorBox.addClass('hidden');
                        $.bootstrapGrowl('系统错误,请稍后再试', {
                            type: 'danger',
                            offset: {
                                from: 'top',
                                amount: 80
                            }
                        });
                    }
                });
            });
        }
    })
});


create.css
/* editor style */
.rich-editor {
    position: relative;
    padding-right: 120px;
}


.content-wrapper {
    border: 1px solid #ccc;
    border-radius: 4px;
}


.content-wrapper .edit-content .block-wrapper:first-child,
.content-wrapper .edit-content .block-wrapper:first-child .block {
    margin-top: 0;
}


.content-wrapper .edit-content {
    padding: 20px;
}


#media-type-list .media-type-button {
    display: inline-block;
    width: 80%;
    max-width: 108px;
    height: auto;
    line-height: 1;
    color: #000;
    border: 0;
    box-shadow: none;
    border-radius: 0;
    outline: none;
    background-color: transparent;
    cursor: pointer;
}


.media-type-button .btn-image {
    width: 100%;
}


.media-type-button .btn-text {
    display: inline-block;
    margin: 10px 0 20px;
    font-size: 12px;
}


.media-container {
    margin: 20px;
    border: 1px dashed #a9a9a9;
    text-align: center;
}


.media-header {
    margin: 20px 0 40px;
}


/* sortable */
#sortable {
    position: absolute;
    background: #f2f2f2;
    padding: 12px 14px;
    min-height: 160px;
    top: 5px;
    right: 20px;
    z-index: 100;
    overflow-y: auto;
    overflow-x: hidden;
}


#sortable.affix {
    position: fixed;
}


(4)controller
<?php
namespace  App\Http\Controllers\Admin;


use Illuminate\Http\Request;
use App\Models\Article;
use App\Contracts\Editor\Renderer;


class ArticleController extends BaseController
{
    protected $resource = 'article';


    public function __construct()
    {
        $this->middleware('auth');
    }


    public function index()
    {
        $this->checkAcl('view');


        $search = \Request::input();
        $params = $search;
        $params['page_size'] = array_get($search, 'page_size', config('config.default_page_size'));


        $articleDao = new Article();
        $articles = $articleDao->getArticleList($params);


        $data = [
            'articles' => $articles,
            'search' => $search
        ];
        return view('article.list', $data);
    }


    public function getCreate()
    {
        $this->checkAcl('create');
        return view('article.create');
    }


    public function getDetail(Renderer $renderer, Request $request)
    {
        $this->checkAcl('view');
        $articleId = $request->input('article_id');
        $article = Article::find($articleId);


        if (!$article || $article->is_deleted) {
            abort(404);
        }


        $article['content'] = $renderer->render($article['content']);


        $data = [
            'article' => $article,
        ];


        return view('article.detail', $data);
    }


    public function getEdit(Renderer $renderer, Request $request)
    {
        $this->checkAcl('edit');


        if ($articleId = $request->input('article_id')) {
            $article = Article::find($articleId);
        }


        if (!isset($article) || $article->is_deleted) {
            abort(404);
        }


        $article['content'] = $renderer->render($article['content']);


        $data = [
            'article' => $article,
        ];


        return view('article.edit', $data);
    }
}


service-controller:
<?php
namespace App\Http\Controllers\Admin\Service;


use App\Http\Controllers\Admin\BaseController;
use Illuminate\Http\Request;
use App\Models\Media;
use App\Models\ArticleMedia;
use App\Models\Article;


class ArticleController extends BaseController
{
    protected $resource = 'article';


    public function __construct()
    {
        $this->middleware('auth');
    }


    public function postSaveMedia(Request $request)
    {
        $this->validate($request, [
            'type' => 'required|in:' . join(',', [Media::TYPE_IMAGE, Media::TYPE_VIDEO]),
            'path' => 'required',
        ]);


        $type = $request->input('type');
        $path = $request->input('path');
        $now = now();
        $media = Media::firstOrNew(['cdn_path' => $path]);
        if (!$media->media_id) {
            $media['type'] = $type;
            $media['cdn_path'] = $path;
            $media['user_id'] = 0;
            $media['created'] = $now;
            $media->save();
        }


        if ($media->media_id) {
            $articleId = $request->input('article_id');
            $article = Article::find($articleId);
            if ($article) {
                $articleMedia = ArticleMedia::firstOrNew([
                    'article_id' => $articleId,
                    'media_id' => $media->media_id,
                ]);
                $articleMedia['created'] = data_get($articleMedia, 'created', $now);
                $articleMedia->save();
            }


            $response = format_json_success([
                'media_id' => $media->media_id,
            ]);
        } else {
            $response = format_json_failed('request_failed');
        }


        return response()->json($response);
    }


    public function postSave(Request $request)
    {
        $this->checkAcl('create');
        $this->validate($request, [
            'title' => 'required|string|max:120',
            'article_id' => 'exists:article,article_id,is_deleted,0',
            'is_active' => 'in:0,1',
        ]);


        $articleId = $request->input('article_id');
        if ($articleId) {
            $article = Article::find($articleId);
        } else {
            $article = new Article();
            $article->user_id = 0;
            $article->is_active = 1;
            $article->is_deleted = 0;
            $article->review_count = 0;
            $article->favorited_count = 0;
        }


        $article->title = $request->input('title');
        $content = $request->input('content');
        $article->content = $content;
        $article->is_active = $request->input('is_active');


        $blocks = @json_decode($content, true);
        $mediaIds = [];
        $textContent = [];


        if ($blocks) {
            foreach ($blocks as $block) {
                $blockType = array_get($block, 'type');
                if (in_array($blockType, ['image', 'video'])
                    && ($mediaId = array_get($block, 'mediaId'))
                ) {
                    $mediaIds[] = $mediaId;
                }


                if ($blockType == 'text') {
                    $blockContent = str_replace('&nbsp;', ' ', array_get($block, 'content'));
                    $blockContent = strip_tags(html_entity_decode($blockContent));
                    $blockContent = trim($blockContent);
                    if ($blockContent !== '') {
                        $textContent[] = $blockContent;
                    }
                }
            }
        }


        if (count($textContent)) {
            $article->text_content = join("\n", $textContent);
            // TODO $article->preview_content = '';
        }


        $datas = [
            'media_ids' => $mediaIds,
        ];


        $result = $article->updateWithCallback(function ($article) use ($datas, $request) {
            $now = now();
            $articleId = $article->article_id;


            $mediaIds = array_get($datas, 'media_ids', []);


            $oldMediaIds = [];
            $removeMediaIds = [];
            $articleMedias = ArticleMedia::where('article_id', $articleId)->get();
            foreach ($articleMedias as $articleMedia) {
                if (!in_array($articleMedia->media_id, $mediaIds)) {
                    $removeMediaIds[] = $articleMedia->media_id;
                } else {
                    $oldMediaIds[] = $articleMedia->media_id;
                }
            }


            if ($removeMediaIds) {
                ArticleMedia::where('article_id', $articleId)
                        ->whereIn('media_id', $removeMediaIds)
                        ->delete();
            }


            $newMediaIds = array_diff($mediaIds, $oldMediaIds);
            if ($newMediaIds) {
                foreach ($newMediaIds as $mediaId) {
                    $articleMedia = new ArticleMedia([
                        'article_id' => $articleId,
                        'media_id' => $mediaId,
                        'is_cover' => 0,
                        'created' => $now,
                    ]);
                    $articleMedia->save();
                }
            }
        });


        if ($result) {
            $this->fireEvent('create_article', $article);
            $response = format_json_success();
        } else {
            $response = format_json_failed('request_failed');
        }


        return response()->json($response);
    }


    public function postDelete(Request $request)
    {
        $this->checkAcl('delete');
        $this->validate($request, [
            'article_id' => 'required|exists:article,article_id,is_deleted,0',
        ]);


        $articleId = $request->input('article_id');
        $article = Article::find($articleId);
        if ($articleId && $article) {
            $article->is_deleted = 1;
            $article->save();
            $this->fireEvent('delete_article', $article);
            return response()->json(format_json_success());
        } else {
            return response()->json(format_json_failed('request_failed'));
        }
    }


    public function postUpdate(Request $request)
    {
        $this->checkAcl('edit');
        $this->validate($request, [
            'article_id' => 'required|exists:article,article_id,is_deleted,0',
            'is_active' => 'in:0,1',
        ]);


        $articleId = $request->input('article_id');
        $article = Article::find($articleId);


        if ($articleId && $article) {
            $article->is_active = $request->input('is_active');
            $article->save();
            $this->fireEvent('update_article', $article);
            return response()->json(format_json_success());
        } else {
            return response()->json(format_json_failed('request_failed'));
        }
    }
}


rich-editor
editor.js
define(['jquery', 'sortable', 'tinymce', 'util', 'underscore', 'layzr', 'bootbox', 'animatescroll'], function ($, Sortable, tinymce, util, _, Layzr, bootbox) {
    var exports = {
            IS_CHANGED: false,
            BLOCK_TYPE_IMAGE: 'image',
            BLOCK_TYPE_VIDEO: 'video',
            BLOCK_TYPE_EMBED: 'embed',
            BLOCK_TYPE_TEXT: 'text'
        },
        defaultOptions = {
            blockSelector: '.block'
        },
        blockTemplates,
        toolbarTemplates,
        index = 1,
        nextIndex = function () {
            return index++;
        };


    bootbox.setDefaults({
        locale: 'zh_CN'
    });


    blockTemplates = {
        image: _.template('<div class="block block-<%- type %>" data-src="<%- src %>"> <div class="block-content"> <figure class="normal image-container"> <img src="<%- fullSrc %>"> <figcaption class="caption-container"> <input type="text" class="caption" value="<%- caption %>" placeholder="添加标题"> </figcaption> </figure> </div> </div>'),
        video: _.template('<div class="block block-<%- type %>" data-src="<%- src %>"> <div class="block-content"> <div class="video-container"> <video controls src="<%- fullSrc %>"></video> <div class="caption-container"> <input type="text" class="caption" value="<%- caption %>" placeholder="添加标题"> </div> </div> </div> </div>'),
        text: _.template('<div class="block block-<%- type %>"> <div class="block-content"> <div class="text-content placeholder" placeholder="输入描述文本"></div> </div> </div>'),
        embed: _.template('<div class="block block-<%- type %>"> <div class="block-content"> <%= code %> <div class="caption-container"> <input type="text" class="caption" value="<%- caption %>" placeholder="添加标题"> </div> </div> </div>'),
    };


    toolbarTemplates = {
        image: '<div class="block-toolbar d-none clearfix">'
                + '<div class="tool-delete">'
                + '<button type="button" class="btn btn-danger delete">删除<span class="glyphicon glyphicon-remove"></span></button>'
                + '</div>'
                + '</div>',
        video: '<div class="block-toolbar d-none clearfix">'
                + '<div class="tool-delete">'
                + '<button type="button" class="btn btn-danger delete">删除<span class="glyphicon glyphicon-remove"></span></button>'
                + '</div>'
                + '</div>',
        embed: '<div class="block-toolbar d-none clearfix">'
                + '<div class="tool-delete">'
                + '<button type="button" class="btn btn-danger delete">删除<span class="glyphicon glyphicon-remove"></span></button>'
                + '</div>'
                + '</div>',
        text: '<div class="block-toolbar d-none clearfix">'
                + '<div class="tool-delete">'
                + '<button type="button" class="btn btn-danger delete">删除<span class="glyphicon glyphicon-remove"></span></button>'
                + '</div>'
                + '</div>',
    };


    function Block() {}


    Block.init = function (block, options) {
        block.id = nextIndex();
        block.size = options.size || 'normal';
        block.options = options;
        if (options.element) {
            block.element = $(options.element);
            options = $.extend(true, {}, block.element.data(), options);
            block.options = options;
        } else {
            block.element = block.buildBlockElement();
        }
    }


    Block.create = function (options) {
        var block;
        switch (options.type) {
            case exports.BLOCK_TYPE_IMAGE:
                block = new ImageBlock(options);
                break;
            case exports.BLOCK_TYPE_VIDEO:
                block = new VideoBlock(options);
                break;
            case exports.BLOCK_TYPE_TEXT:
                block = new TextBlock(options);
                break;
            case exports.BLOCK_TYPE_EMBED:
                block = new EmbedBlock(options);
                break;
            default:
                block = null;
                break;
        }
        return block;
    };


    Block.prototype = {
        wrap: function () {
            return $(this.element).wrap('<div class="block-wrapper clearfix" id="block-' + this.id
                + '" data-id="' + this.id + '"></div>').closest('.block-wrapper');
        },
        addToolbar: function () {
            var toolbarHtml = toolbarTemplates[this.type];


            $(this.element).prepend(toolbarHtml);
        },
        buildBlockElement: function () {
            var blockHtml;
            if (this.type in blockTemplates) {
                blockHtml = blockTemplates[this.type](this);
            }


            return $(blockHtml);
        },
        init: function () {
            this.wrapElement = this.wrap();
            this.addToolbar();
            this.afterInit();
        },
        append: function (index) {
            if (index === undefined) {
                this.container.append(this.wrapElement);
            } else {
                if (index === 0) {
                    this.container.prepend(this.wrapElement);
                } else {
                    this.container.find('.block-wrapper:nth-child(' + (index + 1) + ')').after(this.wrapElement);
                }
            }


            this.afterAppend();
        },
        afterAppend: function () {
            // empty
        },
        afterInit: function () {
            // empty
        },
        remove: function () {
            this.wrapElement.remove();
        },
        update: function (data) {
            // TODO
        }
    };


    function ImageBlock(options) {
        this.type = options.type;
        this.src = options.src;
        this.caption = options.caption;
        this.fullSrc = util.getCdnResource(options.src, {mode: 2, w: 1440});
        Block.init(this, options);
    }


    ImageBlock.prototype = $.extend({}, Block.prototype, {
        afterInit: function () {
            $(this.element).find('.caption').replaceWith($('<input type="text" class="caption" placeholder="添加标题">').val(this.caption));
        },
        getData: function () {
            return {
                type: this.type,
                size: this.size || 'normal',
                src: this.src,
                caption: this.caption,
                mediaId: this.options.mediaId
            };
        }
    });


    function VideoBlock(options) {
        this.type = options.type;
        this.src = options.src;
        this.caption = options.caption;
        this.fullSrc = util.getCdnResource(options.src);
        Block.init(this, options);
    }


    VideoBlock.prototype = $.extend({}, Block.prototype, {
        afterInit: function () {
            $(this.element).find('.caption').replaceWith($('<input type="text" class="caption" placeholder="添加标题">').val(this.caption));
        },
        getData: function () {
            return {
                type: this.type,
                size: this.size || 'normal',
                src: this.src,
                caption: this.caption,
                mediaId: this.options.mediaId
            };
        }
    });


    function EmbedBlock(options) {
        this.type = options.type;
        this.caption = options.caption;
        this.code = util.strip_tags(options.code, '<object><param><embed><video><iframe>');
        Block.init(this, options);
    }


    EmbedBlock.prototype = $.extend({}, Block.prototype, {
        afterInit: function () {
            $(this.element).find('.caption').replaceWith($('<input type="text" class="caption" placeholder="添加标题">').val(this.caption));
        },
        getData: function () {
            return {
                type: this.type,
                size: this.size || 'normal',
                code: this.code,
                caption: this.caption
            };
        }
    });


    function TextBlock(options) {
        options.content = options.content || '';
        this.type = options.type;
        this.content = util.strip_tags(options.content, '<div><p><span><label>'
            + '<h1><h2><h3><h4><h5><h6>',
            + '<a><br>',
            + '<small><strong><sub><sup><del><em><i><b>',
            + '<blockquote><ins><code><output><pre>',
            + '<dd><dl><dt><li><ol><ul>');
        Block.init(this, options);
    }


    TextBlock.prototype = $.extend({}, Block.prototype, {
        afterInit: function () {
            var block = this;
            tinymce.init({
                selector: '#block-' + this.id + ' div.text-content',
                menubar: false,
                inline: true,
                theme: 'modern',
                language: 'zh_CN',
                plugins: 'lists link paste textcolor stylebuttons',
                toolbar: 'bold italic underline alignleft aligncenter alignright alignjustify bullist numlist Heading-h1',
                setup: function(editor) {
                    editor.on('input change', function () {
                        if ($(editor.getElement()).text() === '') {
                            $(block.element).find('.text-content').addClass('placeholder');
                        } else {
                            $(block.element).find('.text-content').removeClass('placeholder');
                        }
                    });


                    editor.on('blur', function () {
                        if ($(editor.getElement()).text() === '') {
                            $(block.element).find('.text-content').addClass('placeholder');
                        }
                    });


                    editor.on('focus', function () {
                        $(block.element).find('.text-content').removeClass('placeholder');
                    });
                    block.tinyeditor = editor;
                }
            });
        },
        afterAppend: function () {
            this.afterInit();
        },
        getData: function () {
            return {
                type: this.type,
                size: this.size || 'normal',
                content: this.tinyeditor.getContent()
            };
        }
    });


    function EditorStore() {
        this.blocks = [];
    }


    EditorStore.prototype = {
        pushBlock: function (block) {
            this.blocks.push(block);
        },
        insertBlock: function (block, index) {
            this.blocks.splice(index, 0, block);
        },
        getBlock: function (id) {
            return _.findWhere(this.blocks, {id: id});
        },
        removeBlock: function (id) {
            var i = _.findIndex(this.blocks, {id: id});
            this.blocks.splice(i, 1);
        },
        getIndexById: function (id) {
            return _.findIndex(this.blocks, {id: id});
        },
        moveBlock: function (id, toIndex) {
            var index = _.findIndex(this.blocks, {id: id}),
                block;


            if (index !== undefined) {
                block = this.blocks[index];
                if (index == toIndex) {
                    return;
                }


                this.blocks.splice(index, 1);
                this.blocks.splice(toIndex, 0, block);
            }
        },
        getDatas: function () {
            return _.map(this.blocks, function (block) {
                return block.getData();
            });
        }
    };


    function initEvent(editor) {
        editor.blockContainer.on('click', '.delete', function () {
            var $this = $(this);
            var blockElement = $this.closest('.block-wrapper'),
            blockId = blockElement.data('id');
            editor.removeBlock(blockId);
            //delete action
            // bootbox.confirm({
            //     title: '删除',
            //     message: '确认删除该内容?',
            //     size: 'small',
            //     callback: function(result) {
            //         if (!result) {
            //             return;
            //         }


            //         var blockElement = $this.closest('.block-wrapper'),
            //         blockId = blockElement.data('id');
            //         editor.removeBlock(blockId);
            //         exports.IS_CHANGED = true;
            //     }
            // });
        }).on('click', '.resizer-normal', function () {
            //resize image or video to normal size
            var blockElement = $(this).closest('.block-wrapper'),
                blockId = blockElement.data('id'),
                target = blockElement.find('.image-container, .video-container'),
                block = editor.store.getBlock(blockId);


            target.removeClass('full').addClass('normal');
            $(this).closest('.block').removeClass('full').addClass('normal');


            $(this).closest('.tool-resizer').find('.active').removeClass('active');
            $(this).addClass('active');


            block.size = 'normal';
        }).on('click', '.resizer-full', function() {
            //resize image or video to full screen size
            var blockElement = $(this).closest('.block-wrapper'),
                blockId = blockElement.data('id'),
                target = blockElement.find('.image-container, .video-container'),
                block = editor.store.getBlock(blockId);


            target.removeClass('normal').addClass('full');
            $(this).closest('.block').removeClass('normal').addClass('full');


            $(this).closest('.tool-resizer').find('.active').removeClass('active');
            $(this).addClass('active');


            block.size = 'full';
        }).on('change', '.caption', function () {
            var blockId = $(this).closest('.block-wrapper').data('id'),
                block = editor.store.getBlock(blockId);
            block.caption = $(this).val();
            exports.IS_CHANGED = true;
        });


        editor.sortableContainer.on('click', '.sort-item', function() {
            var targetId = 'block-' + $(this).data('blockId');
            $('#' + targetId).animatescroll();
        });
    }


    function initSortable(editor) {
        if (editor.sortableContainer.length) {
            Sortable.create(editor.sortableContainer[0], {
                animation: 150,
                onMove: function (event) {
                    var current = $(event.dragged);
                    current.addClass('img-selected');
                },
                onEnd: function (event) {
                    var current = $(event.item),
                        blockId = current.data('block-id');
                    current.removeClass('img-selected');


                    editor.moveBlock(blockId, event.newIndex);
                    exports.IS_CHANGED = true;
                }
            });
        }
    }


    function Editor(options) {
        this.blockContainer = $(options.blockContainer),
        this.sortableContainer = $(options.sortableContainer);
        this.blockSelector = options.blockSelector;
        this.store = new EditorStore();
    }


    Editor.prototype = {
        init: function () {
            var editor = this;
            this.blockContainer.find(this.blockSelector).each(function () {
                var options = $(this).data(),
                    block;


                options.element = this;
                block = Block.create(options);
                if (block) {
                    //Block.init(block, options);
                    block.container = editor.blockContainer;
                    block.init();
                    editor.addSortItem(block);
                    editor.store.pushBlock(block);
                }
            });


            initEvent(this);
            initSortable(this);


            this.blockContainer.data('editor', this);
        },
        addBlock: function (options, index) {
            var block = Block.create(options);


            if (block) {
                block.init();
                if (index === undefined) {
                    this.pushBlock(block);
                } else {
                    this.insertBlock(block, index);
                }


                return block.id;
            }
        },
        pushBlock: function (block) {
            block.container = this.blockContainer;
            this.store.pushBlock(block);
            block.append();
            this.addSortItem(block);
        },
        insertBlock: function (block, index) {
            block.container = this.blockContainer;
            this.store.insertBlock(block, index);
            block.append(index);
            this.addSortItem(block, index);
        },
        removeBlock: function (id) {
            var index = this.store.getIndexById(id),
                block = this.store.getBlock(id);


            block.remove();
            this.removeSortItem(index);
            this.store.removeBlock(id);
        },
        moveBlock: function (id, toIndex) {
            var index = this.store.getIndexById(id),
                oldIndex,
                block;


            if (index !== undefined) {
                block = this.store.getBlock(id);
                oldIndex = block.wrapElement.index();
                if (index == toIndex) {
                    return;
                }


                if (oldIndex != toIndex) {
                    if (toIndex == 0) {
                        this.blockContainer.prepend(block.wrapElement);
                    } else if (toIndex > oldIndex) {
                        this.blockContainer.find('.block-wrapper:nth-child(' + (toIndex + 1) + ')').after(block.wrapElement);
                    } else {
                        this.blockContainer.find('.block-wrapper:nth-child(' + toIndex + ')').after(block.wrapElement);
                    }
                }


                this.store.moveBlock(id, toIndex);
            }
        },
        updateBlock: function (id, options) {
            var block = this.store.getBlock(id);
            block.options = $.extend(true, {}, block.options, options);
            // TODO
        },
        addSortItem: function (block, index) {
            var itemHtml = '<li id="sort-item-' + block.id
                + '" data-block-id="' + block.id + '" class="sort-item thumb-' + block.type + '">';


            if (block.type === 'image' && block.src) {
                itemHtml += '<img src="' + util.getCdnResource(block.src, {w: 60, h: 40}) + '">';
            }
            itemHtml += '</li>';


            if (index !== undefined) {
                if (index == 0) {
                    this.sortableContainer.prepend(itemHtml);
                } else {
                    this.sortableContainer.find('.sort-item:nth-child(' + index + ')').after(itemHtml);
                }
            } else {
                this.sortableContainer.append(itemHtml);
            }
        },
        removeSortItem: function (index) {
            this.sortableContainer.find('.sort-item:nth-child(' + (index + 1) + ')').remove();
        },
        getDataArray: function () {
            return this.store.getDatas();
        }
    };


    exports.init = function (options) {
        options = $.extend(true, {}, defaultOptions, options);
        var blockContainer = $(options.blockContainer),
            editor;
        if (editor = blockContainer.data('editor')) {
            return editor;
        }


        editor = new Editor(options);
        editor.init();


        new Layzr();


        return editor;
    };


    return exports;
});


editor.css
.sortable {
    padding: 0 14px;
}


.sortable ul {
    list-style: none;
    width: 60px;
    padding: 0;
}


.sortable .sort-item {
    margin-bottom: 10px;
    width: 60px;
    height: 44px;
    border: 1px solid #ddd;
    background-position: center;
    background-repeat: no-repeat;
    background-size: 100%;
    cursor: move;
}


.sortable .thumb-image img {
    width: 100%;
    height: 100%;
}


.sortable .thumb-text {
    background-image: url(/static/image/words_108x79.png);
}


.sortable .thumb-embed {
    background-image: url(/static/image/link_108x79.png);
}


.sortable .thumb-video {
    background-image: url(/static/image/video_109x79.png);
}


.sortable .thumb-product {
    background-image: url(/static/image/archist_109x79.png);
}


.block-wrapper {
    position: relative;
    min-height: 40px;
    margin-top: 30px;
}


.block-wrapper > .block {
    margin: 30px auto;
}


.block-toolbar {
    position: absolute;
    right: 0;
    top: 5px;
    z-index: 10;
}


.block-toolbar .tool-delete {
    float: right;
    margin-right: 60px;
    margin-left: 40px;
}


.block-toolbar .tool-delete .delete {
    background-color: #373634;
    border-color: #373634;
    color: #fff;
    border-radius: 2px;
    box-shadow: none;
    outline: none;
}


.block-toolbar .tool-resizer {
    float: right;
}


.block-toolbar .tool-resizer ul {
    list-style: none;
    padding: 8px 10px;
    height: 36px;
    background: #333;
}


.block-toolbar .tool-resizer .resizer-item {
    display: inline-block;
    margin: 0 5px;
    height: 20px;
    width: 30px;
    background: #333;
    border: 2px solid #fff;
    cursor: pointer;
}


.block-toolbar .tool-resizer .resizer-item.resizer-normal {
    margin: 4px 5px;
    height: 12px;
    width: 24px;
}


.block-toolbar .tool-resizer .resizer-item.resizer-full {
    margin: 0 5px;
    height: 20px;
}


.block-toolbar .tool-resizer .resizer-item.active {
    background: #fff;
}


.block {
    position: relative;
    margin: 60px auto;
    line-height: 2;
}


.block .image-container {
    text-align: center;
}


.block .image-container img {
    width: 100%;
    height: auto;
}


.block .video-container video {
    width: 100%;
    height: auto;
}


.block-product {
    line-height: 1.5;
    margin-top: 60px;
    margin-bottom: 68px;
}


.block-image {
    margin-top: 80px;
    margin-bottom: 74px;
}


.block-video {
    margin-top: 86px;
    margin-bottom: 60px;
}


.block-product,
.block-text {
    width: 67%;
    margin-left: auto;
    margin-right: auto;
}


.block-text .text-content {
    padding: 10px 0;
    letter-spacing: 0.1em;
    min-height: 50px;
    word-break: break-all;
    border: 2px solid transparent;
}


.block-text .text-content.placeholder::after {
    position: absolute;
    top: 10px;
    content: attr(placeholder);
    color: #a9a9a9;
}


.block-text .mce-edit-focus {
    border: 2px dashed #ccc;
    outline: none;
}


.block .caption {
    width: 100%;
    height: 30px;
    text-align: center;
    margin: 8px 0 0;
    color: #666;
    border: none;
    outline: none;
    line-height: 1.4;
}


.block-embed .block-content {
    text-align: center;
}


.product-wrapper {
    border: 1px solid #ddd;
    height: 170px;
}


.product-wrapper > .img-wrapper {
    float: left;
    width: 38%;
    line-height: 170px;
    padding-left: 42px;
}


.product-wrapper > .img-wrapper > a > img {
    height: 140px;
    width: 100%;
}


.product-wrapper > .right-content-wrapper {
    width: 62%;
    padding: 10px 42px 10px 28px;
    float: right;
    height: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
}


.product-wrapper > .right-content-wrapper > p {
    word-break: break-all;
}


.product-wrapper > .right-content-wrapper .view-more {
    padding-left: 20px;
    cursor: pointer;
    color: #666464;
}


.block-wrapper .block:hover .block-toolbar {
    display: block;
}


.sortable .img-selected {
    border: 3px dashed #000;
}


.sortable::-webkit-scrollbar {
    width: 8px;
}


.sortable::-webkit-scrollbar-thumb {
    background-color: rgba(0, 0, 0, 0.2);
    border-radius: 10px;
}


BlockRender.php
<?php
namespace App\Services\Editor;


use App\Contracts\Editor\Renderer;
use App\Models\User;
use App\Models\Product;
use App\Models\ProductAttribute;


class BlockRenderer implements Renderer
{
    const BLOCK_TYPE_IMAGE = 'image';
    const BLOCK_TYPE_VIDEO = 'video';
    const BLOCK_TYPE_EMBED = 'embed';
    const BLOCK_TYPE_TEXT = 'text';
    const BLOCK_TYPE_PRODUCT = 'product';


    private $context;


    public function __construct($context)
    {
        $this->context = $context;
    }


    public function render($content)
    {
        $blocks = @json_decode($content, true);


        if ($blocks === null) {
            return $content;
        }


        $blockSegments = [];
        foreach ($blocks as $block) {
            $blockSegments[] = $this->renderBlock($block);
        }


        return join('', $blockSegments);
    }


    protected function renderBlock($block)
    {
        $type = data_get($block, 'type');


        if (!$type) {
            return '';
        }


        $blockContent = '';
        $datas = [];
        switch ($type) {
            case self::BLOCK_TYPE_TEXT:
                $blockContent = $this->buildTextContent($block);
                // $datas['content'] = array_get($block, 'content', '');
                break;
            case self::BLOCK_TYPE_IMAGE:
                $blockContent = $this->buildImageContent($block);
                $datas['src'] = array_get($block, 'src', '');
                $datas['size'] = array_get($block, 'size', 'normal');
                $datas['caption'] = array_get($block, 'caption', '');
                $datas['media-id'] = array_get($block, 'mediaId', '');
                break;
            case self::BLOCK_TYPE_VIDEO:
                $blockContent = $this->buildVideoContent($block);
                $datas['src'] = array_get($block, 'src', '');
                $datas['size'] = array_get($block, 'size', 'normal');
                $datas['caption'] = array_get($block, 'caption', '');
                $datas['media-id'] = array_get($block, 'mediaId', '');
                break;
            case self::BLOCK_TYPE_EMBED:
                $blockContent = $this->buildEmbedContent($block);
                $datas['code'] = array_get($block, 'code', '');
                $datas['caption'] = array_get($block, 'caption', '');
                break;
            case self::BLOCK_TYPE_PRODUCT:
                $productId = data_get($block, 'relatedId');
                $product = Product::with('user')->published()->find($productId);
                if ($product) {
                    $blockContent = $this->buildProductContent($product);
                    $datas['related-id'] = array_get($block, 'relatedId', '');
                } else {
                    return '';
                }
                break;
            default:
                return '';
                break;
        }
        $datas['type'] = $type;
        $dataString = '';
        foreach ($datas as $key => $value) {
            $dataString .= e('data-' . $key) . '="' . e($value) . '" ';
        }


        $segments = [
            '<div class="block block-' . $type . ' ',
            e(data_get($block, 'size', 'normal')) . '" ',
            $dataString,
            '>',
            '<div class="block-content">',
            $blockContent,
            '</div>',
            '</div>',
        ];


        $blockHtml = join('', $segments);


        return $blockHtml;
    }


    protected function buildImageContent($data)
    {
        $src = data_get($data, 'src', '');
        if ($src === '') {
            return '';
        }


        $fullSrc = app('cdn')->getResourceUrl($src, ['mode' => 2, 'width' => 1440]);
        $segments = [
            '<figure class="image-container layzr-image ',
            e(data_get($data, 'size', 'normal')),
            '">',
            '<img class="img" data-layzr="',
            e($fullSrc),
            '"/><div class="loading"></div><figcaption class="caption-container"><p class="caption">',
            e(data_get($data, 'caption', '')),
            '</p></figcaption>',
            '</figure>',
        ];


        return join('', $segments);
    }


    protected function buildVideoContent($data)
    {
        $src = data_get($data, 'src', '');
        $fullSrc = app('cdn')->getResourceUrl($src);
        $segments = [
            '<div class="video-container ',
            e(data_get($data, 'size', 'normal')),
            '">',
            '<video controls src="',
            e($fullSrc),
            '"></video>',
            '<div class="caption-container"><p class="caption">',
            e(data_get($data, 'caption', '')),
            '</p></div>',
            '</div>',
        ];


        return join('', $segments);
    }


    protected function buildEmbedContent($data)
    {
        $content = data_get($data, 'code', '');
        $content = strip_tags($content, '<object><param><embed><video><iframe>');


        $segments = [
            $content,
            '<div class="caption-container"><p class="caption">',
            e(data_get($data, 'caption', '')),
            '</p></div>',
        ];


        return join('', $segments);;
    }


    protected function buildTextContent($data)
    {
        $allowedTags = [
            '<div><p><span><label>',
            '<h1><h2><h3><h4><h5><h6>',
            '<a><br>',
            '<small><strong><sub><sup><del><em><i><b>',
            '<blockquote><ins><code><output><pre>',
            '<dd><dl><dt><li><ol><ul>',
        ];
        $content = data_get($data, 'content', '');
        $content = strip_tags($content, join('', $allowedTags));


        return '<div class="text-content" contenteditable="false" placeholder="输入描述文本">' . $content . '</div>';
    }


    protected function buildProductContent($data)
    {
        $fullSrc = '';
        $product = $data;
        $media = $product->medias()->where('is_cover', '1')->first();


        $attributes = $product->attributes()->where('path', ProductAttribute::PRODUCT_CITY)->first();
        if (isset($media) && isset($media['cdn_path'])) {
            $fullSrc = app('cdn')->getResourceUrl($media['cdn_path'], ['width' => 200, 'height' => 120]);
        }
        $username = data_get($product, 'user.nickname', '');
        $location = $attributes['data'];


        $linkStr = '';
        if ($this->context == 'admin') {
            $productDetailUrl = '';
        } else {
            $productDetailUrl = e(route("product_detail", ["productId" => $product['product_id']]));
            $linkStr = '<a class="view-more" href="' . $productDetailUrl . '">查看全部</a>';
        }


        $segments = [
            '<div class="product-wrapper"><div class="img-wrapper"><a href="',
            $productDetailUrl,
            '"><img src="',
            e($fullSrc),
            '"></a></div><div class="right-content-wrapper"><h4>|<a href="',
            $productDetailUrl,
            '">',
            e(data_get($product, 'name', '')),
            '</a></h4><h5>|',
            e($location),
            '&nbsp;',
            e($username),
            '</h5><p>',
            e(data_get($product, 'short_description', '')),
            $linkStr,
            '</p></div></div>',
        ];


        return join('', $segments);
    }
}


多功能自定义的util.js
define(['jquery'], function ($) {
    // discuss at: http://phpjs.org/functions/number_format/
    // original by: Jonas Raoni Soares Silva (http://www.jsfromhell.com)
    function number_format(number, decimals, dec_point, thousands_sep) {
        number = (number + '').replace(/[^0-9+\-Ee.]/g, '');
        var n = !isFinite(+number) ? 0 : +number,
            prec = !isFinite(+decimals) ? 0 : Math.abs(decimals),
            sep = (typeof thousands_sep === 'undefined') ? ',' : thousands_sep,
            dec = (typeof dec_point === 'undefined') ? '.' : dec_point,
            s = '',
            toFixedFix = function (n, prec) {
                var k = Math.pow(10, prec);
                return '' + (Math.round(n * k) / k).toFixed(prec);
            };
        // Fix for IE parseFloat(0.55).toFixed(0) = 0;
        s = (prec ? toFixedFix(n, prec) : '' + Math.round(n)).split('.');
        if (s[0].length > 3) {
            s[0] = s[0].replace(/\B(?=(?:\d{3})+(?!\d))/g, sep);
        }
        if ((s[1] || '').length < prec) {
            s[1] = s[1] || '';
            s[1] += new Array(prec - s[1].length + 1).join('0');
        }
        return s.join(dec);
    }


    //  discuss at: http://phpjs.org/functions/strip_tags/
    function strip_tags(input, allowed) {
        allowed = (((allowed || '') + '')
            .toLowerCase()
            .match(/<[a-z][a-z0-9]*>/g) || [])
            .join(''); // making sure the allowed arg is a string containing only tags in lowercase (<a><b><c>)
        var tags = /<\/?([a-z][a-z0-9]*)\b[^>]*>/gi,
            commentsAndPhpTags = /<!--[\s\S]*?-->|<\?(?:php)?[\s\S]*?\?>/gi;


        return input.replace(commentsAndPhpTags, '')
            .replace(tags, function($0, $1) {
                return allowed.indexOf('<' + $1.toLowerCase() + '>') > -1 ? $0 : '';
            });
    }


    function getCdnResource(key, options) {
        if (!key) {
            return false;
        }


        key = encodeURI(key);


        if (!options) {
             return 'http://' + APP_CONFIG.cdn.domain + '/' + key;
        }


        var mode = options.mode || '1',
            w = options.w || '',
            h = options.h || '',
            q = options.q || '',
            format = options.format || '';


        if (!mode) {
            return false;
        }


        if (!w && !h) {
            return false;
        }


        var imageUrl = 'imageView2/' + mode;
        imageUrl += w ? '/w/' + w : '';
        imageUrl += h ? '/h/' + h : '';
        imageUrl += q ? '/q/' + q : '';
        imageUrl += format ? '/format/' + format : '';


        return 'http://' + APP_CONFIG.cdn.domain + '/' + key + '?' + imageUrl;
    }


    function getLocations(locationId, callback) {
        $.ajax({
            url: '/service/location/sublocation',
            data: {
                location_id: locationId
            }
        }).done(function(response) {
            if (response.status == 'SUCCESS') {
                callback(response.body.data);
            } else if (response.status == 'INVALID') {
                alert('参数错误');
            } else if (response.status == 'FAILED') {
                alert(response.body.error);
            } else {
                alert('系统错误,请稍后再试');
            }
        });
    }


    function getAudioDuration(file, callback) {
        if (!AudioContext || !FileReader || !File) {
            callback(false);
        }


        var context = new AudioContext(),
            reader = new FileReader();


        reader.onload = function (e) {
            context.decodeAudioData(e.target.result, function (buffer) {
                callback(buffer);
            });
        };


        reader.readAsArrayBuffer(file);
    }


    function initGotoTop() {
        var $window = $(window);
        function updateGoTop() {
            if ($window.scrollTop() >= 100) {
                $('.go-top').show();
            } else {
                $('.go-top').hide();
            }
        }


        updateGoTop();


        $window.on('scroll', updateGoTop);


        $('.go-top').on('click', function () {
            $('body,html').animate({
                scrollTop: 0
            });
        });
    }


    return {
        number_format: number_format,
        strip_tags: strip_tags,
        getCdnResource: getCdnResource,
        getLocations: getLocations,
        getAudioDuration: getAudioDuration,
        initGotoTop: initGotoTop
    };
});
0 0
原创粉丝点击