完成後台編輯文章架構
This commit is contained in:
parent
31286d4e85
commit
142d787436
42
app/Console/Commands/CreatePostType.php
Normal file
42
app/Console/Commands/CreatePostType.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class CreatePostType extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'command:name';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Command description';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
88
app/Console/Commands/GeneratePosts.php
Normal file
88
app/Console/Commands/GeneratePosts.php
Normal file
@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Post;
|
||||
use App\User;
|
||||
use App\Repositories\PostRepository;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class GeneratePosts extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'post:generate {postType} {amount?} {--user_id=}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Generate posts';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$user_id = $this->option('user_id');
|
||||
if($user_id) {
|
||||
$user = User::find($user_id);
|
||||
if(!$user) {
|
||||
throw new \Exception("User $user_id doesn't exist");
|
||||
}
|
||||
} else {
|
||||
$user = User::inRandomOrder()->first();
|
||||
if(!$user) {
|
||||
throw new \Exception("User $user_id doesn't exist");
|
||||
}
|
||||
}
|
||||
$amount = $this->argument('amount');
|
||||
if(!$amount) {
|
||||
$amount = 1;
|
||||
}
|
||||
$postType = $this->argument('postType');
|
||||
$postTypes = config('postTypes');
|
||||
if(array_key_exists($postType, $postTypes)) {
|
||||
/** @var Post $model */
|
||||
$postRepo = new PostRepository($postTypes[$postType]['model']);
|
||||
$faker = \Faker\Factory::create();
|
||||
for($i = 0; $i < $amount; $i++) {
|
||||
$post = $postRepo->createModel();
|
||||
$post->fill([
|
||||
'title' => $this->randomTextLength($faker, 10, 25),
|
||||
'excerpt' => $this->randomTextLength($faker, 10, 30),
|
||||
'introduction' => $faker->realText($faker->numberBetween(20, 400)),
|
||||
'user_id' => $user->id
|
||||
]);
|
||||
$post->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function randomTextLength($faker, $min, $max)
|
||||
{
|
||||
return mb_substr($this->rtrim($faker->realText($max)), 0, $faker->numberBetween($min, $max - 1));
|
||||
}
|
||||
|
||||
private function rtrim($text)
|
||||
{
|
||||
return mb_substr($text, 0, -1);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Menu\Articles;
|
||||
|
||||
use App\Http\Controllers\Admin\Menu\BaseMenuItemController;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ArticlesMenuItemController extends BaseMenuItemController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->name = 'adminMenu.items.articles.articles';
|
||||
|
||||
$this->slug = 'articles';
|
||||
|
||||
$this->permissions = ['admin manage post articles'];
|
||||
|
||||
$this->iconClasses = 'nav-icon icon-wrench';
|
||||
}
|
||||
|
||||
public function handle(Request $request)
|
||||
{
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Menu\Articles\Children;
|
||||
|
||||
use App\Http\Controllers\Admin\Menu\BaseMenuItemController;
|
||||
use App\Http\Controllers\Admin\Menu\PostCreateMenuItemController;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class CreateMenuItemController extends PostCreateMenuItemController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->name = 'adminMenu.items.articles.create';
|
||||
|
||||
$this->slug = 'create';
|
||||
|
||||
$this->iconClasses = 'nav-icon icon-wrench';
|
||||
|
||||
$this->postType = 'article';
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Menu\Articles\Children;
|
||||
|
||||
use App\Http\Controllers\Admin\Menu\PostListMenuItemController;
|
||||
|
||||
class ListMenuItemController extends PostListMenuItemController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->name = 'adminMenu.items.articles.list';
|
||||
|
||||
$this->slug = 'list';
|
||||
|
||||
$this->iconClasses = 'nav-icon icon-wrench';
|
||||
|
||||
$this->postType = 'article';
|
||||
|
||||
$this->pageHeader = trans('adminPageHeader.articles.articles');
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Menu\Portfolios\Children;
|
||||
|
||||
use App\Http\Controllers\Admin\Menu\PostCreateMenuItemController;
|
||||
|
||||
class CreateMenuItemController extends PostCreateMenuItemController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->name = 'adminMenu.items.portfolios.create';
|
||||
|
||||
$this->slug = 'create';
|
||||
|
||||
$this->iconClasses = 'nav-icon icon-wrench';
|
||||
|
||||
$this->postType = 'portfolio';
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Menu\Portfolios\Children;
|
||||
|
||||
use App\Http\Controllers\Admin\Menu\PostListMenuItemController;
|
||||
|
||||
class ListMenuItemController extends PostListMenuItemController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->name = 'adminMenu.items.portfolios.list';
|
||||
|
||||
$this->slug = 'list';
|
||||
|
||||
$this->iconClasses = 'nav-icon icon-wrench';
|
||||
|
||||
$this->postType = 'portfolio';
|
||||
|
||||
$this->pageHeader = trans('adminPageHeader.portfolios.portfolios');
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Menu\Portfolios;
|
||||
|
||||
use App\Http\Controllers\Admin\Menu\BaseMenuItemController;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class PortfoliosMenuItemController extends BaseMenuItemController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->name = 'adminMenu.items.portfolios.portfolios';
|
||||
|
||||
$this->slug = 'portfolios';
|
||||
|
||||
$this->permissions = ['admin manage post portfolio'];
|
||||
|
||||
$this->iconClasses = 'nav-icon icon-wrench';
|
||||
}
|
||||
|
||||
public function handle(Request $request)
|
||||
{
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Menu;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class PostCreateMenuItemController extends BaseMenuItemController
|
||||
{
|
||||
protected $postType;
|
||||
|
||||
public function handle(Request $request)
|
||||
{
|
||||
$languages = app('SiteState')->languageTranslations;
|
||||
return view('admin.menu.posts.edit', [
|
||||
'adminRouteNamePrefix' => config('admin.route_name_prefix'),
|
||||
'languages' => $languages,
|
||||
'post' => null,
|
||||
'resource' => $this->postType
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Menu;
|
||||
|
||||
use App\Repositories\PostRepository;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class PostListMenuItemController extends BaseMenuItemController
|
||||
{
|
||||
protected $postType;
|
||||
|
||||
protected $pageHeader;
|
||||
|
||||
public function handle(Request $request)
|
||||
{
|
||||
$postTypes = config('postTypes.' . $this->postType);
|
||||
$postRepo = new PostRepository($postTypes['model']);
|
||||
$posts = $postRepo->getPostsPager();
|
||||
return view('admin.menu.posts.list', [
|
||||
'adminRouteNamePrefix' => config('admin.route_name_prefix'),
|
||||
'resource' => $this->postType,
|
||||
'pageHeader' => $this->pageHeader,
|
||||
'posts' => $posts
|
||||
]);
|
||||
}
|
||||
}
|
||||
106
app/Http/Controllers/Admin/PostController.php
Normal file
106
app/Http/Controllers/Admin/PostController.php
Normal file
@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Traits\ModelAttributeTranslationsUpdattable;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Auth;
|
||||
|
||||
class PostController extends Controller
|
||||
{
|
||||
use ModelAttributeTranslationsUpdattable;
|
||||
|
||||
private function getModelClass($resource)
|
||||
{
|
||||
return config('postTypes.' . $resource)['model'];
|
||||
}
|
||||
|
||||
private function getResource(Request $request)
|
||||
{
|
||||
return explode('.', str_replace(config('admin.route_name_prefix'), '', $request->route()->getName()))[0];
|
||||
}
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public function edit(Request $request, $postId)
|
||||
{
|
||||
$resource = $this->getResource($request);
|
||||
$modelClass = $this->getModelClass($resource);
|
||||
$post = $modelClass::find($postId);
|
||||
$languages = app('SiteState')->languageTranslations;
|
||||
return view('admin.menu.posts.edit', [
|
||||
'adminRouteNamePrefix' => config('admin.route_name_prefix'),
|
||||
'resource' => $resource,
|
||||
'post' => $post,
|
||||
'languages' => $languages
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(Request $request)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
return $this->update($request, null);
|
||||
}
|
||||
|
||||
public function update(Request $request, $postId)
|
||||
{
|
||||
$defaultLocale = config('app.locale');
|
||||
$resource = $this->getResource($request);
|
||||
$modelClass = $this->getModelClass($resource);
|
||||
|
||||
/** @var \App\Post $post */
|
||||
if($postId) {
|
||||
$post = $modelClass::find($postId);
|
||||
} else {
|
||||
$post = app($modelClass);
|
||||
$translatableAttributes = $post->getTranslatableAttributes();
|
||||
$translatableAttributeValues = $request->all($translatableAttributes);
|
||||
$translatableAttributeValues = collect($translatableAttributeValues)->map(function($translatableAttributeValue) use ($defaultLocale){
|
||||
return $translatableAttributeValue[$defaultLocale];
|
||||
});
|
||||
$post = $modelClass::create(array_merge($translatableAttributeValues->toArray(), [
|
||||
'user_id' => Auth::id()
|
||||
]));
|
||||
}
|
||||
|
||||
// Post的可翻譯屬性
|
||||
$translatableAttributes = $post->getTranslatableAttributes();
|
||||
// 更新所有可翻譯的屬性
|
||||
foreach ($translatableAttributes as $translatableAttribute) {
|
||||
$translations = $request->get($translatableAttribute);
|
||||
if($translations) {
|
||||
$this->updateAttributeTranslations($post, $translatableAttribute, $translations);
|
||||
}
|
||||
}
|
||||
|
||||
$post->fill([
|
||||
'feature_image_id' => $request->get('feature_image_id')
|
||||
]);
|
||||
$post->save();
|
||||
|
||||
return redirect(route(config('admin.route_name_prefix') . $resource . '.edit', [$resource => $post->id]));
|
||||
}
|
||||
|
||||
public function show(Request $request)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public function destroy(Request $request, $postId)
|
||||
{
|
||||
$resource = $this->getResource($request);
|
||||
$modelClass = $this->getModelClass($resource);
|
||||
|
||||
$post = $modelClass::find($postId);
|
||||
$post->delete();
|
||||
return back();
|
||||
}
|
||||
}
|
||||
14
app/Post.php
14
app/Post.php
@ -9,4 +9,18 @@ abstract class Post extends TranslatableModel
|
||||
protected $fillable = [
|
||||
'title', 'content', 'excerpt', 'user_id', 'feature_image_id'
|
||||
];
|
||||
|
||||
protected $translatableAttributes = [
|
||||
'title', 'content', 'excerpt'
|
||||
];
|
||||
|
||||
public function author()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id');
|
||||
}
|
||||
|
||||
public function featureImage()
|
||||
{
|
||||
return $this->belongsTo(MediaFile::class, 'feature_image_id');
|
||||
}
|
||||
}
|
||||
|
||||
@ -60,7 +60,7 @@ class MainMenuItemPresenter
|
||||
case 'title':
|
||||
$html .= $htmlPresenter->li([
|
||||
'class' => 'nav-title',
|
||||
'html' => trans('menu.titles.' . $item['name'])
|
||||
'html' => trans('adminMenu.titles.' . $item['name'])
|
||||
]);
|
||||
break;
|
||||
case 'item':
|
||||
|
||||
60
app/Presenters/Admin/MediaSelectionFieldPresenter.php
Normal file
60
app/Presenters/Admin/MediaSelectionFieldPresenter.php
Normal file
@ -0,0 +1,60 @@
|
||||
<?php
|
||||
namespace App\Presenters\Admin;
|
||||
|
||||
use App\Repositories\MediaFileRepository;
|
||||
|
||||
class MediaSelectionFieldPresenter
|
||||
{
|
||||
public function render($mediaFileId = null, $inputHtmlArgs = [], $wrapperClasses = [])
|
||||
{
|
||||
$presenter = app('Html');
|
||||
$mediaFileRepo = app(MediaFileRepository::class);
|
||||
$mediaFile = $mediaFileRepo->findModel($mediaFileId);
|
||||
$previewImgHtml = '';
|
||||
$mediaFieldContentHtml = '';
|
||||
if($mediaFile) {
|
||||
$previewImgHtml = $presenter->img([
|
||||
'src' => $mediaFile->url
|
||||
]);
|
||||
}
|
||||
$mediaFieldContentHtml .= $presenter->input(array_merge([
|
||||
'type' => 'hidden',
|
||||
'class' => 'input',
|
||||
'value' => $mediaFileId
|
||||
], $inputHtmlArgs));
|
||||
$mediaFieldContentHtml .= $presenter->div([
|
||||
'class' => 'form-group row',
|
||||
'html' => $presenter->div([
|
||||
'class' => 'col-6',
|
||||
'html' => $presenter->div([
|
||||
'class' => 'preview',
|
||||
'html' => $previewImgHtml
|
||||
])
|
||||
])
|
||||
]);
|
||||
$mediaFieldContentHtml .= $presenter->div([
|
||||
'class' => 'form-group row',
|
||||
'html' => [
|
||||
$presenter->div([
|
||||
'class' => 'col-3',
|
||||
'html' => $presenter->button([
|
||||
'class' => ['btn', 'btn-success', 'select-media'],
|
||||
'html' => trans('form.buttons.select')
|
||||
])
|
||||
]),
|
||||
$presenter->div([
|
||||
'class' => 'col-3',
|
||||
'html' => $presenter->button([
|
||||
'class' => ['btn', 'btn-danger', 'clear-media'],
|
||||
'style' => !$mediaFile ? 'display:none;' : '',
|
||||
'html' => trans('form.buttons.remove')
|
||||
])
|
||||
])
|
||||
]
|
||||
]);
|
||||
return $presenter->div([
|
||||
'class' => array_merge($wrapperClasses, ['media-selection-field']),
|
||||
'html' => $mediaFieldContentHtml
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -12,7 +12,7 @@ class OptionFormFieldsPresenter
|
||||
$options = config('admin.options.' . $page);
|
||||
$presenter = app('Html');
|
||||
$optionRepo = app('Option');
|
||||
$mediaFileRepo = app(MediaFileRepository::class);
|
||||
$mediaSelectionFieldPresenter = app(MediaSelectionFieldPresenter::class);
|
||||
$html = '';
|
||||
if(!empty($options['fields'])) {
|
||||
foreach($options['fields'] as $key => $option) {
|
||||
@ -37,7 +37,7 @@ class OptionFormFieldsPresenter
|
||||
]),
|
||||
$presenter->div([
|
||||
'class' => 'col-12 col-md-6',
|
||||
'html' => function() use ($key, $type, $required, $presenter, $optionRepo, $mediaFileRepo) {
|
||||
'html' => function() use ($key, $type, $required, $presenter, $optionRepo, $mediaSelectionFieldPresenter) {
|
||||
$html = '';
|
||||
|
||||
$bastHtmlArgs = [
|
||||
@ -46,54 +46,8 @@ class OptionFormFieldsPresenter
|
||||
];
|
||||
switch ($type) {
|
||||
case 'media':
|
||||
$mediaFiledId = $optionRepo->$key;
|
||||
$mediaFile = $mediaFileRepo->findModel($mediaFiledId);
|
||||
$previewImgHtml = '';
|
||||
$mediaFieldContentHtml = '';
|
||||
if($mediaFile) {
|
||||
$previewImgHtml = $presenter->img([
|
||||
'src' => $mediaFile->url
|
||||
]);
|
||||
}
|
||||
$mediaFieldContentHtml .= $presenter->input(array_merge([
|
||||
'type' => 'hidden',
|
||||
'class' => 'input',
|
||||
'value' => $mediaFiledId
|
||||
], $bastHtmlArgs));
|
||||
$mediaFieldContentHtml .= $presenter->div([
|
||||
'class' => 'form-group row',
|
||||
'html' => $presenter->div([
|
||||
'class' => 'col-6',
|
||||
'html' => $presenter->div([
|
||||
'class' => 'preview',
|
||||
'html' => $previewImgHtml
|
||||
])
|
||||
])
|
||||
]);
|
||||
$mediaFieldContentHtml .= $presenter->div([
|
||||
'class' => 'form-group row',
|
||||
'html' => [
|
||||
$presenter->div([
|
||||
'class' => 'col-3',
|
||||
'html' => $presenter->button([
|
||||
'class' => ['btn', 'btn-success', 'select-media'],
|
||||
'html' => trans('form.buttons.select')
|
||||
])
|
||||
]),
|
||||
$presenter->div([
|
||||
'class' => 'col-3',
|
||||
'html' => $presenter->button([
|
||||
'class' => ['btn', 'btn-danger', 'clear-media'],
|
||||
'style' => !$mediaFile ? 'display:none;' : '',
|
||||
'html' => trans('form.buttons.remove')
|
||||
])
|
||||
])
|
||||
]
|
||||
]);
|
||||
$html .= $presenter->div([
|
||||
'class' => 'option-media-field',
|
||||
'html' => $mediaFieldContentHtml
|
||||
]);
|
||||
$mediaFileId = $optionRepo->$key;
|
||||
$html .= $mediaSelectionFieldPresenter->render($mediaFileId, $bastHtmlArgs, ['option-media-field']);
|
||||
break;
|
||||
case 'text':
|
||||
case 'password':
|
||||
|
||||
27
app/Repositories/PostRepository.php
Normal file
27
app/Repositories/PostRepository.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repositories;
|
||||
|
||||
use App\Post;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
|
||||
class PostRepository extends BaseRepository
|
||||
{
|
||||
public function __construct($model)
|
||||
{
|
||||
if(is_string($model)) {
|
||||
$this->setModel(app($model));
|
||||
} elseif($model instanceof Post) {
|
||||
$this->setModel($model);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $postsPerPage
|
||||
* @return LengthAwarePaginator
|
||||
*/
|
||||
public function getPostsPager($postsPerPage = 15)
|
||||
{
|
||||
return $this->getModelClass()::orderByDesc('created_at')->paginate($postsPerPage);
|
||||
}
|
||||
}
|
||||
26
app/Traits/ModelAttributeTranslationsUpdattable.php
Normal file
26
app/Traits/ModelAttributeTranslationsUpdattable.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Traits;
|
||||
|
||||
use App\Repositories\ModelTranslationRepository;
|
||||
|
||||
trait ModelAttributeTranslationsUpdattable
|
||||
{
|
||||
private function updateAttributeTranslations($model, $attribute, $translations)
|
||||
{
|
||||
$transRepo = app(ModelTranslationRepository::class);
|
||||
$transRepo->setTranslatedModel($model);
|
||||
$locales = app('SiteState')->languages;
|
||||
$defaultLocale = config('app.locale');
|
||||
|
||||
foreach ($translations as $locale => $translation) {
|
||||
if(in_array($locale, $locales)) {
|
||||
$transRepo->updateModelTranslation($model->id, $attribute, $locale, $translation);
|
||||
}
|
||||
}
|
||||
if(isset($translations[$defaultLocale])) {
|
||||
$model->$attribute = $translations[$defaultLocale];
|
||||
$model->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -8,6 +8,29 @@ return [
|
||||
'route_name_prefix' => 'admin.',
|
||||
// 後台選單項目
|
||||
'menuItems' => [
|
||||
[
|
||||
'type' => 'title',
|
||||
'name' => 'posts',
|
||||
],
|
||||
[
|
||||
'type' => 'item',
|
||||
'controller' => Menu\Articles\ArticlesMenuItemController::class,
|
||||
'children' => [
|
||||
Menu\Articles\Children\ListMenuItemController::class,
|
||||
Menu\Articles\Children\CreateMenuItemController::class,
|
||||
]
|
||||
],
|
||||
[
|
||||
'type' => 'item',
|
||||
'controller' => Menu\Portfolios\PortfoliosMenuItemController::class,
|
||||
'children' => [
|
||||
Menu\Portfolios\Children\ListMenuItemController::class,
|
||||
Menu\Portfolios\Children\CreateMenuItemController::class,
|
||||
]
|
||||
],
|
||||
[
|
||||
'type' => 'divider'
|
||||
],
|
||||
[
|
||||
'type' => 'item',
|
||||
'controller' => Menu\Options\OptionsMenuItemController::class,
|
||||
|
||||
9
config/postTypes.php
Normal file
9
config/postTypes.php
Normal file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
return [
|
||||
'article' => [
|
||||
'model' => \App\Article::class
|
||||
],
|
||||
'portfolio' => [
|
||||
'model' => \App\Portfolio::class
|
||||
],
|
||||
];
|
||||
@ -1,5 +0,0 @@
|
||||
<?php
|
||||
return [
|
||||
\App\Article::class,
|
||||
\App\Portfolio::class,
|
||||
];
|
||||
29
resources/js/admin/app.js
vendored
29
resources/js/admin/app.js
vendored
@ -106,4 +106,33 @@ $(() => {
|
||||
$('#app-header .medialibrary').on('click', function(){
|
||||
app.adminMediaLibrary = app.methods.media(false, null, false);
|
||||
});
|
||||
|
||||
$('.media-selection-field .select-media').on('click', e => {
|
||||
e.preventDefault()
|
||||
let $this = $(e.currentTarget),
|
||||
$wrapper = $this.closest('.media-selection-field'),
|
||||
$input = $wrapper.find('input'),
|
||||
$preview = $wrapper.find('.preview'),
|
||||
$clearButton = $wrapper.find('.clear-media')
|
||||
|
||||
app.methods.media(true, $input.get(0), true, (data) => {
|
||||
let media = data.medias[0]
|
||||
$preview.empty().append(
|
||||
$('<img>').attr('src', media.url)
|
||||
)
|
||||
$clearButton.show()
|
||||
})
|
||||
})
|
||||
|
||||
$('.media-selection-field .clear-media').on('click', e => {
|
||||
e.preventDefault()
|
||||
let $this = $(e.currentTarget),
|
||||
$wrapper = $this.closest('.media-selection-field'),
|
||||
$input = $wrapper.find('input'),
|
||||
$preview = $wrapper.find('.preview')
|
||||
|
||||
$input.val(null)
|
||||
$preview.empty()
|
||||
$this.hide()
|
||||
})
|
||||
})
|
||||
|
||||
28
resources/js/admin/page/options.js
vendored
28
resources/js/admin/page/options.js
vendored
@ -1,28 +0,0 @@
|
||||
$('.select-media').on('click', e => {
|
||||
e.preventDefault()
|
||||
let $this = $(e.currentTarget),
|
||||
$wrapper = $this.closest('.option-media-field'),
|
||||
$input = $wrapper.find('input'),
|
||||
$preview = $wrapper.find('.preview'),
|
||||
$clearButton = $wrapper.find('.clear-media')
|
||||
|
||||
app.methods.media(true, $input.get(0), true, (data) => {
|
||||
let media = data.medias[0]
|
||||
$preview.empty().append(
|
||||
$('<img>').attr('src', media.url)
|
||||
)
|
||||
$clearButton.show()
|
||||
})
|
||||
})
|
||||
|
||||
$('.clear-media').on('click', e => {
|
||||
e.preventDefault()
|
||||
let $this = $(e.currentTarget),
|
||||
$wrapper = $this.closest('.option-media-field'),
|
||||
$input = $wrapper.find('input'),
|
||||
$preview = $wrapper.find('.preview')
|
||||
|
||||
$input.val(null)
|
||||
$preview.empty()
|
||||
$this.hide()
|
||||
})
|
||||
@ -12,6 +12,19 @@ return array(
|
||||
'routes' => 'Route List',
|
||||
'system' => 'System',
|
||||
'gateAbilities' => 'Gate Abilities',
|
||||
],
|
||||
'articles' => [
|
||||
'articles' => 'Articles',
|
||||
'list' => 'List',
|
||||
'create' => 'Create',
|
||||
],
|
||||
'portfolios' => [
|
||||
'portfolios' => 'Portfolios',
|
||||
'list' => 'List',
|
||||
'create' => 'Create'
|
||||
]
|
||||
],
|
||||
'titles' => [
|
||||
'posts' => 'Posts'
|
||||
]
|
||||
);
|
||||
|
||||
@ -6,4 +6,10 @@ return [
|
||||
'development' => 'Development Settings',
|
||||
'platform' => 'Platform Settings',
|
||||
],
|
||||
'articles' => [
|
||||
'articles' => 'Articles'
|
||||
],
|
||||
'portfolios' => [
|
||||
'portfolios' => 'Portfolios'
|
||||
]
|
||||
];
|
||||
|
||||
@ -3,6 +3,19 @@ return [
|
||||
'buttons' => [
|
||||
'update' => 'Update',
|
||||
'select' => 'Select',
|
||||
'remove' => 'Remove'
|
||||
'remove' => 'Remove',
|
||||
'create' => 'Create',
|
||||
'delete' => 'Delete',
|
||||
'logout' => 'Logout',
|
||||
],
|
||||
'titles' => [
|
||||
'title' => 'Title',
|
||||
'author' => 'Author',
|
||||
'created' => 'Created',
|
||||
'updated' => 'Updated',
|
||||
'operations' => 'Operations',
|
||||
'excerpt' => 'Excerpt',
|
||||
'content' => 'Content',
|
||||
'featureImage' => 'Feature Image'
|
||||
]
|
||||
];
|
||||
|
||||
9
resources/lang/en/languages.php
Normal file
9
resources/lang/en/languages.php
Normal file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
return array (
|
||||
'en' => 'English',
|
||||
'zh-tw' => 'Chinese Traditional',
|
||||
'zh-cn' => 'Chinese Simplified',
|
||||
'ja' => 'Japanese',
|
||||
'ko' => 'Korean',
|
||||
);
|
||||
@ -1,5 +1,6 @@
|
||||
<?php
|
||||
return [
|
||||
'mediaLibrary' => 'Media Library',
|
||||
'tabCategory' => 'Category',
|
||||
'tabUpload' => 'Upload',
|
||||
'tabBrowse' => 'Browse',
|
||||
|
||||
30
resources/lang/zh-tw/adminMenu.php
Normal file
30
resources/lang/zh-tw/adminMenu.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
return array(
|
||||
'items' => [
|
||||
'options' => [
|
||||
'options' => '設定',
|
||||
'general' => '一般',
|
||||
'development' => '開發',
|
||||
'platform' => '平台',
|
||||
],
|
||||
'systemStatus' => [
|
||||
'systemStatus' => '系統狀態',
|
||||
'routes' => '路由列表',
|
||||
'system' => '系統資訊',
|
||||
'gateAbilities' => 'Gate能力列表',
|
||||
],
|
||||
'articles' => [
|
||||
'articles' => '文章',
|
||||
'list' => '列表',
|
||||
'create' => '建立',
|
||||
],
|
||||
'portfolios' => [
|
||||
'portfolios' => '作品集',
|
||||
'list' => '列表',
|
||||
'create' => '建立'
|
||||
]
|
||||
],
|
||||
'titles' => [
|
||||
'posts' => '文章內容'
|
||||
]
|
||||
);
|
||||
9
resources/lang/zh-tw/languages.php
Normal file
9
resources/lang/zh-tw/languages.php
Normal file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
return array (
|
||||
'en' => '英文',
|
||||
'zh-tw' => '繁體中文',
|
||||
'zh-cn' => '簡體中文',
|
||||
'ja' => '日文',
|
||||
'ko' => '韓文',
|
||||
);
|
||||
@ -1,5 +1,6 @@
|
||||
<?php
|
||||
return [
|
||||
'mediaLibrary' => '媒體庫',
|
||||
'category' => '分類',
|
||||
'date' => '日期',
|
||||
'delete' => '刪除',
|
||||
|
||||
18
resources/sass/admin/app.scss
vendored
18
resources/sass/admin/app.scss
vendored
@ -5,3 +5,21 @@
|
||||
@import "components/blockui";
|
||||
@import "../components/media-library";
|
||||
@import "../app-common";
|
||||
//媒體庫圖片預覽
|
||||
.media-selection-field {
|
||||
.preview {
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
//左側主選單子項目
|
||||
.sidebar-nav {
|
||||
ul.submenu {
|
||||
li.nav-item {
|
||||
a.nav-link {
|
||||
padding: 0.3rem 0.6rem 0.3rem 1.4rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
5
resources/sass/admin/page/post-list.scss
vendored
Normal file
5
resources/sass/admin/page/post-list.scss
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
td.feature-image {
|
||||
img {
|
||||
max-width: 50px;
|
||||
}
|
||||
}
|
||||
@ -34,7 +34,7 @@
|
||||
@include('components.navBrand')
|
||||
</li>
|
||||
<li class="nav-item px-3">
|
||||
<a href="#" class="nav-link medialibrary">Media Library</a>
|
||||
<a href="#" class="nav-link medialibrary">{{ trans('mediaLibrary.mediaLibrary') }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="nav navbar-nav ml-auto">
|
||||
@ -49,7 +49,7 @@
|
||||
</button>
|
||||
<form action="{{ route('logout') }}" method="post">
|
||||
@csrf
|
||||
<button class="btn btn-outline-danger btn-sm">登出</button>
|
||||
<button class="btn btn-outline-danger btn-sm">{{ trans('form.buttons.logout') }}</button>
|
||||
</form>
|
||||
</header>
|
||||
<div id="app-body" class="app-body">
|
||||
|
||||
56
resources/views/admin/menu/posts/edit.blade.php
Normal file
56
resources/views/admin/menu/posts/edit.blade.php
Normal file
@ -0,0 +1,56 @@
|
||||
@extends('admin.layouts.app')
|
||||
|
||||
@section('admin-page-content')
|
||||
<form action="{{ $post ? route($adminRouteNamePrefix . $resource . '.update', [$post->id]) : route($adminRouteNamePrefix . $resource . '.store') }}" method="POST">
|
||||
@if($post)
|
||||
@method('PUT')
|
||||
@endif
|
||||
@csrf
|
||||
<div class="row">
|
||||
<div class="col-8">
|
||||
<ul class="nav nav-tabs" id="language-tab" role="tablist">
|
||||
@foreach ($languages as $languageCode => $name)
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{ $languageCode == config('app.locale') ? 'active' : ''}}" id="{{ $languageCode }}-tab" data-toggle="tab" href="#{{ $languageCode }}-content" role="tab" aria-controls="{{ $languageCode }}-content" aria-selected="true">@lang('languages.' . $languageCode)</a>
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
<div class="tab-content">
|
||||
<?php foreach ($languages as $languageCode => $name): ?>
|
||||
<div class="tab-pane {{ $languageCode == config('app.locale') ? 'active' : ''}}" id="{{$languageCode}}-content" role="tabpanel" aria-labelledby="{{ $languageCode }}-tab">
|
||||
<div class="form-group">
|
||||
<label>{{ trans('form.titles.title') }}</label>
|
||||
<input type="text" name="title[{{ $languageCode }}]" class="form-control" value="{{ $post ? $post->trans('title', $languageCode) : '' }}" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>{{ trans('form.titles.excerpt') }}</label>
|
||||
<input type="text" name="excerpt[{{ $languageCode }}]" class="form-control" value="{{ $post ? $post->trans('excerpt', $languageCode) : '' }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>{{ trans('form.titles.content') }}</label>
|
||||
<textarea name="content[{{ $languageCode }}]" class="form-control" rows="10">{{ $post ? $post->trans('content', $languageCode) : ''}}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="col-4">
|
||||
@if($post)
|
||||
<div class="form-group">
|
||||
<label>{{ trans('form.titles.author') }}</label>
|
||||
<div>{{ $post->author->email}}</div>
|
||||
</div>
|
||||
@endif
|
||||
@inject('mediaSelectionFieldPresenter', 'App\Presenters\Admin\MediaSelectionFieldPresenter')
|
||||
<div class="form-group">
|
||||
<label>{{ trans('form.titles.featureImage') }}</label>
|
||||
{!! $mediaSelectionFieldPresenter->render($post ? $post->feature_image_id : null, ['name' => 'feature_image_id']) !!}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button class="btn btn-success">{{ $post ? trans('form.buttons.update') : trans('form.buttons.create') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@endsection
|
||||
46
resources/views/admin/menu/posts/list.blade.php
Normal file
46
resources/views/admin/menu/posts/list.blade.php
Normal file
@ -0,0 +1,46 @@
|
||||
@extends('admin.layouts.app')
|
||||
|
||||
@push('admin-app-styles')
|
||||
<link rel="stylesheet" href="{{ asset('css/admin/page/post-list.css') }}">
|
||||
@endpush
|
||||
|
||||
@section('admin-page-content')
|
||||
<h2>{{ $pageHeader }}</h2>
|
||||
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>{{ trans('form.titles.title') }}</th>
|
||||
<th>{{ trans('form.titles.author') }}</th>
|
||||
<th>{{ trans('form.titles.created') }}</th>
|
||||
<th>{{ trans('form.titles.updated') }}</th>
|
||||
<th>{{ trans('form.titles.operations') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($posts as $post)
|
||||
<tr>
|
||||
<td class="feature-image">
|
||||
@if($post->featureImage)
|
||||
<img src="{{ $post->featureImage->url }}" alt="{{ $post->featureImage->description }}">
|
||||
@endif
|
||||
</td>
|
||||
<td><a href="{{ route($adminRouteNamePrefix . $resource . '.edit', [$resource => $post->id]) }}">{{ $post->title }}</a></td>
|
||||
<td>{{ $post->author->email }}</td>
|
||||
<td>{{ $post->created_at }}</td>
|
||||
<td>{{ $post->updated_at }}</td>
|
||||
<td>
|
||||
<form action="{{ route($adminRouteNamePrefix . $resource . '.destroy', [$resource => $post->id]) }}" method="POST">
|
||||
@method('delete')
|
||||
@csrf
|
||||
<button class="btn btn-danger">{{ trans('form.buttons.delete') }}</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{!! $posts->links() !!}
|
||||
@endsection
|
||||
@ -28,6 +28,10 @@ Route::get('/robots.txt', 'PageController@robotstxt');
|
||||
*/
|
||||
Route::group(['prefix' => config('admin.route'), 'middleware' => ['admin.area'], 'as' => config('admin.route_name_prefix')], function() {
|
||||
Route::get('/', 'AdminPageController@index')->name('index');
|
||||
|
||||
foreach (config('postTypes') as $resource => $postType) {
|
||||
Route::resource($resource, 'Admin\PostController');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
1
webpack.mix.js
vendored
1
webpack.mix.js
vendored
@ -28,3 +28,4 @@ mix.sass('resources/sass/admin/lib.scss', publicAdminCssDir)
|
||||
.sass('resources/sass/admin/app.scss', publicAdminCssDir)
|
||||
.sass('resources/sass/admin/page/system-status.scss', publicAdminCssDir + '/page')
|
||||
.sass('resources/sass/admin/page/options.scss', publicAdminCssDir + '/page')
|
||||
.sass('resources/sass/admin/page/post-list.scss', publicAdminCssDir + '/page')
|
||||
|
||||
Loading…
Reference in New Issue
Block a user