发表文章:
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">×</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(' ', ' ', 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),
' ',
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
};
});
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">×</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(' ', ' ', 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),
' ',
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
};
});