加入後台媒體庫功能
This commit is contained in:
parent
179e369f0b
commit
d896c085a3
33
app/Disk.php
Normal file
33
app/Disk.php
Normal file
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* App\Disk
|
||||
*
|
||||
* @property int $id
|
||||
* @property string $name 名稱
|
||||
* @property-read mixed $path
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection|\App\MediaFile[] $mediaFiles
|
||||
* @property-read int|null $media_files_count
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\Disk newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\Disk newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\Disk query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\Disk whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\Disk whereName($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class Disk extends Model
|
||||
{
|
||||
public function getPathAttribute()
|
||||
{
|
||||
return config('filesystems.disks.' . $this->name)['root'];
|
||||
}
|
||||
|
||||
public function mediaFiles()
|
||||
{
|
||||
return $this->hasMany(MediaFile::class);
|
||||
}
|
||||
}
|
||||
39
app/Events/MediaCategoryDeletingEvent.php
Normal file
39
app/Events/MediaCategoryDeletingEvent.php
Normal file
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use App\MediaCategory;
|
||||
use Illuminate\Broadcasting\Channel;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Broadcasting\PrivateChannel;
|
||||
use Illuminate\Broadcasting\PresenceChannel;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||
|
||||
class MediaCategoryDeletingEvent
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
public $mediaCategory;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(MediaCategory $mediaCategory)
|
||||
{
|
||||
$this->mediaCategory = $mediaCategory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the channels the event should broadcast on.
|
||||
*
|
||||
* @return \Illuminate\Broadcasting\Channel|array
|
||||
*/
|
||||
public function broadcastOn()
|
||||
{
|
||||
return new PrivateChannel('channel-name');
|
||||
}
|
||||
}
|
||||
39
app/Events/MediaFileDeletedEvent.php
Normal file
39
app/Events/MediaFileDeletedEvent.php
Normal file
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use App\MediaFile;
|
||||
use Illuminate\Broadcasting\Channel;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Broadcasting\PrivateChannel;
|
||||
use Illuminate\Broadcasting\PresenceChannel;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||
|
||||
class MediaFileDeletedEvent
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
public $model;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(MediaFile $model)
|
||||
{
|
||||
$this->model = $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the channels the event should broadcast on.
|
||||
*
|
||||
* @return \Illuminate\Broadcasting\Channel|array
|
||||
*/
|
||||
public function broadcastOn()
|
||||
{
|
||||
return new PrivateChannel('channel-name');
|
||||
}
|
||||
}
|
||||
300
app/Http/Controllers/MediaCategoryController.php
Normal file
300
app/Http/Controllers/MediaCategoryController.php
Normal file
@ -0,0 +1,300 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Repositories\MediaCategoriesRepository;
|
||||
use App\Repositories\MediaFileRepository;
|
||||
use App\Traits\PureAjaxMethodProtectable;
|
||||
use Illuminate\Http\Request;
|
||||
use Gate;
|
||||
use Auth;
|
||||
use Validator;
|
||||
|
||||
/**
|
||||
* 媒體庫分類的Controller
|
||||
*
|
||||
* Class MediaCategoryController
|
||||
* @package App\Http\Controllers
|
||||
*/
|
||||
class MediaCategoryController extends Controller
|
||||
{
|
||||
use PureAjaxMethodProtectable;
|
||||
|
||||
private $mediaCategoriesRepository;
|
||||
|
||||
private $mediaFileRepository;
|
||||
|
||||
public function __construct(MediaCategoriesRepository $mediaCategoriesRepository, MediaFileRepository $mediaFileRepository)
|
||||
{
|
||||
$this->mediaCategoriesRepository = $mediaCategoriesRepository;
|
||||
$this->mediaFileRepository = $mediaFileRepository;
|
||||
}
|
||||
|
||||
public function hasAppMediaCategoryPermission()
|
||||
{
|
||||
return Gate::allows('permission:manage-app-media-categories');
|
||||
}
|
||||
|
||||
/**
|
||||
* 取得所有媒體庫分類
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$this->protectFromNoneAjaxRequest($request);
|
||||
|
||||
$responseData = [
|
||||
'mediaCategories' => []
|
||||
];
|
||||
if(Auth::check()) {
|
||||
$userId = Auth::id();
|
||||
$categories = [];
|
||||
$hasAppCategoryPermission = $this->hasAppMediaCategoryPermission();
|
||||
if($hasAppCategoryPermission) {
|
||||
$categories = $this->mediaCategoriesRepository->getAllAppMediaCategories();
|
||||
} else {
|
||||
$categories = $this->mediaCategoriesRepository->getAllUserCategories($userId);
|
||||
}
|
||||
foreach ($categories as $category) {
|
||||
$category->count = $this->mediaCategoriesRepository->getMediaFilesCount($category->id);
|
||||
}
|
||||
$categories = array_merge([
|
||||
[
|
||||
'id' => 'all',
|
||||
'name' => trans('mediaLibrary.all'),
|
||||
'count' => $hasAppCategoryPermission
|
||||
? $this->mediaFileRepository->getAppMediasCount()
|
||||
: $this->mediaFileRepository->getUserMediasCount($userId)
|
||||
],
|
||||
[
|
||||
'id' => 'uncategorized',
|
||||
'name' => trans('mediaLibrary.uncategorized'),
|
||||
'count' => $hasAppCategoryPermission
|
||||
? $this->mediaFileRepository->getAppUncategorizedMediasCount()
|
||||
: $this->mediaFileRepository->getUserUncategorizedMediasCount($userId)
|
||||
]
|
||||
] , $categories->toArray());
|
||||
$responseData['mediaCategories'] = $categories;
|
||||
return response()->json($responseData);
|
||||
} else {
|
||||
return response()->json($responseData, 401);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增媒體庫分類
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$this->protectFromNoneAjaxRequest($request);
|
||||
|
||||
Validator::make($request->all(), [
|
||||
'name' => 'required|min:1'
|
||||
])->validate();
|
||||
|
||||
$status = 200;
|
||||
if(Auth::check()) {
|
||||
$userId = Auth::id();
|
||||
$hasAppMediaCategoryPermission = $this->hasAppMediaCategoryPermission();
|
||||
$category = $this->mediaCategoriesRepository->addCategory($request->get('name'), $userId, $hasAppMediaCategoryPermission);
|
||||
if(!$category) {
|
||||
$status = 500;
|
||||
}
|
||||
} else {
|
||||
$status = 401;
|
||||
}
|
||||
if($status == 200) {
|
||||
return response()->json([
|
||||
'mediaCategory' => $category,
|
||||
'message' => trans('message.categoryNameHasBeenAdded', ['name' => $category->name])
|
||||
]);
|
||||
} else {
|
||||
return response()->json([
|
||||
'message' => trans('message.failToAddCategory')
|
||||
], $status);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新媒體庫分類
|
||||
*
|
||||
* @param Request $request
|
||||
* @param $categoryId
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function update(Request $request, $categoryId)
|
||||
{
|
||||
$this->protectFromNoneAjaxRequest($request);
|
||||
|
||||
Validator::make($request->all(), [
|
||||
'name' => 'required|min:1'
|
||||
])->validate();
|
||||
|
||||
$category = $this->mediaCategoriesRepository->getCategory($categoryId);
|
||||
|
||||
$status = 200;
|
||||
if(Auth::check()) {
|
||||
if($category) {
|
||||
$permissionCheck = true;
|
||||
if($this->hasAppMediaCategoryPermission()) {
|
||||
if(!$category->is_app_media_category) {
|
||||
$permissionCheck = false;
|
||||
}
|
||||
} else {
|
||||
if($category->user_id != Auth::id()) {
|
||||
$permissionCheck = false;
|
||||
}
|
||||
}
|
||||
|
||||
if($permissionCheck) {
|
||||
$category->name = $request->get('name');
|
||||
$saved = $category->save();
|
||||
if(!$saved) {
|
||||
$status = 500;
|
||||
}
|
||||
} else {
|
||||
$status = 403;
|
||||
}
|
||||
} else {
|
||||
$status = 400;
|
||||
}
|
||||
} else {
|
||||
$status = 401;
|
||||
}
|
||||
|
||||
if($status == 200) {
|
||||
return response()->json([
|
||||
'id' => $categoryId,
|
||||
'name' => $request->get('name'),
|
||||
'message' => trans('message.categoryNameHasBeenUpdated', ['name' => $request->get('name')])
|
||||
]);
|
||||
} else {
|
||||
return response()->json([
|
||||
'message' => trans('message.failToUpdateCategoryName')
|
||||
], $status);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刪除媒體庫分類
|
||||
*
|
||||
* @param Request $request
|
||||
* @param $categoryId
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function destroy(Request $request, $categoryId)
|
||||
{
|
||||
$this->protectFromNoneAjaxRequest($request);
|
||||
$status = 200;
|
||||
if(Auth::check()) {
|
||||
$category = $this->mediaCategoriesRepository->getCategory($categoryId);
|
||||
if($category) {
|
||||
$permissionCheck = true;
|
||||
if($this->hasAppMediaCategoryPermission()) {
|
||||
if(!$category->is_app_media_category) {
|
||||
$permissionCheck = false;
|
||||
}
|
||||
} else {
|
||||
if($category->user_id != Auth::id()) {
|
||||
$permissionCheck = false;
|
||||
}
|
||||
}
|
||||
if($permissionCheck) {
|
||||
$mediaFilesCount = $category->mediaFiles->count();
|
||||
$category->delete();
|
||||
} else {
|
||||
$status = 403;
|
||||
}
|
||||
} else {
|
||||
$status = 400;
|
||||
}
|
||||
} else {
|
||||
$status = 401;
|
||||
}
|
||||
|
||||
if($status == 200) {
|
||||
return response()->json([
|
||||
'id' => $category->id,
|
||||
'count' => $mediaFilesCount,
|
||||
'message' => trans('message.categoryNameHasBeenDeleted', ['name' => $category->name])
|
||||
]);
|
||||
} else {
|
||||
return response()->json([
|
||||
'message' => trans('message.failToDeleteCategory')
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新分類順序
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function updateOrder(Request $request)
|
||||
{
|
||||
$this->protectFromNoneAjaxRequest($request);
|
||||
|
||||
$this->validate($request, [
|
||||
'ids' => 'required|min:1'
|
||||
]);
|
||||
|
||||
$status = 200;
|
||||
|
||||
$mediaCategories = [];
|
||||
$ids = explode(',', $request->get('ids'));
|
||||
foreach ($ids as $id) {
|
||||
$mediaCategories[] = $this->mediaCategoriesRepository->getCategory($id);
|
||||
}
|
||||
|
||||
if(Auth::check()) {
|
||||
$existenceCheck = true;
|
||||
|
||||
foreach ($mediaCategories as $mediaCategory) {
|
||||
$existenceCheck = $existenceCheck && $mediaCategory;
|
||||
}
|
||||
|
||||
if($existenceCheck) {
|
||||
$permissionCheck = true;
|
||||
|
||||
if($this->hasAppMediaCategoryPermission()) {
|
||||
foreach ($mediaCategories as $mediaCategory) {
|
||||
$permissionCheck = $permissionCheck && $mediaCategory->is_app_media_category;
|
||||
}
|
||||
} else {
|
||||
foreach ($mediaCategories as $mediaCategory) {
|
||||
$permissionCheck = $permissionCheck && ($mediaCategory->user_id == Auth::id());
|
||||
}
|
||||
}
|
||||
|
||||
if($permissionCheck) {
|
||||
foreach ($mediaCategories as $index => $mediaCategory) {
|
||||
$mediaCategory->seq = $index;
|
||||
$mediaCategory->save();
|
||||
}
|
||||
} else {
|
||||
$status = 403;
|
||||
}
|
||||
} else {
|
||||
$status = 400;
|
||||
}
|
||||
} else {
|
||||
$status = 401;
|
||||
}
|
||||
|
||||
if($status == 200) {
|
||||
return response()->json([]);
|
||||
} else {
|
||||
return response()->json([
|
||||
'message' => trans('message.failToReOrderCategory', $status)
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
374
app/Http/Controllers/MediaLibraryController.php
Normal file
374
app/Http/Controllers/MediaLibraryController.php
Normal file
@ -0,0 +1,374 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Repositories\MediaCategoriesRepository;
|
||||
use App\Traits\PureAjaxMethodProtectable;
|
||||
use App\Traits\UploadedFileProccessable;
|
||||
use App\MediaFile;
|
||||
use App\Repositories\MediaFileRepository;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Http\Request;
|
||||
use Storage;
|
||||
use Str;
|
||||
use Auth;
|
||||
use Gate;
|
||||
use Validator;
|
||||
|
||||
/**
|
||||
* 媒體檔案
|
||||
*
|
||||
* Class MediaLibraryController
|
||||
* @package App\Http\Controllers
|
||||
*/
|
||||
class MediaLibraryController extends Controller
|
||||
{
|
||||
use PureAjaxMethodProtectable;
|
||||
|
||||
use UploadedFileProccessable;
|
||||
|
||||
private $mediaFileRepository;
|
||||
|
||||
private $mediaCategoriesRepository;
|
||||
|
||||
public function __construct(MediaFileRepository $mediaFileRepository, MediaCategoriesRepository $mediaCategoriesRepository)
|
||||
{
|
||||
$this->mediaFileRepository = $mediaFileRepository;
|
||||
$this->mediaCategoriesRepository = $mediaCategoriesRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* 建立response資料
|
||||
*
|
||||
* @param MediaFile $mediaFile
|
||||
* @return array
|
||||
*/
|
||||
private function createResponseMediaFile(MediaFile $mediaFile)
|
||||
{
|
||||
$mediaCategory = $mediaFile->mediaCategory;
|
||||
return [
|
||||
'id' => $mediaFile->id,
|
||||
'fileName' => $mediaFile->file_name,
|
||||
'url' => $mediaFile->url,
|
||||
'date' => $mediaFile->created_at->format('Y-m-d H:i:s'),
|
||||
'mimeType' => $mediaFile->mime_type,
|
||||
'description' => $mediaFile->description,
|
||||
'sizeInBytes' => $mediaFile->size,
|
||||
'width' => $mediaFile->width,
|
||||
'height' => $mediaFile->height,
|
||||
'category' => [
|
||||
'id' => $mediaCategory ? $mediaCategory->id : 'uncategorized',
|
||||
'name' => $mediaCategory ? $mediaCategory->name : ''
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
private function hasAppMediaPermission()
|
||||
{
|
||||
return Gate::allows("permission:manage-app-medias");
|
||||
}
|
||||
|
||||
public function hasAppMediaCategoryPermission()
|
||||
{
|
||||
return Gate::allows('permission:manage-app-media-categories');
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增媒體檔案
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function fileUpload(Request $request)
|
||||
{
|
||||
$this->protectFromNoneAjaxRequest($request);
|
||||
|
||||
if(Auth::check()) {
|
||||
/** @var \Symfony\Component\HttpFoundation\File\UploadedFile $file */
|
||||
$file = $request->files->get('media_file');
|
||||
if($file) {
|
||||
$userId = Auth::id();
|
||||
if($this->hasAppMediaPermission()) {
|
||||
$uploadedMediaFile = $this->uploadMediaFile($file, 1, $userId, null, true);
|
||||
} else {
|
||||
$uploadedMediaFile = $this->uploadMediaFile($file, 2, $userId, $userId);
|
||||
}
|
||||
|
||||
$success = false;
|
||||
|
||||
if($uploadedMediaFile) {
|
||||
$category = $request->get('category');
|
||||
if($this->mediaCategoriesRepository->hasCategory($category)) {
|
||||
$uploadedMediaFile->media_category_id = $category;
|
||||
$uploadedMediaFile->save();
|
||||
}
|
||||
$success = true;
|
||||
}
|
||||
|
||||
if($success) {
|
||||
return response()->json([
|
||||
'media' => $this->createResponseMediaFile($uploadedMediaFile),
|
||||
'category' => $category
|
||||
]);
|
||||
} else {
|
||||
return response()->json([], 400);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return response()->json([], 401);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取得媒體檔案
|
||||
*
|
||||
* @param Request $request
|
||||
* @param null $page
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
*/
|
||||
public function getMedias(Request $request, $page = null)
|
||||
{
|
||||
$this->protectFromNoneAjaxRequest($request);
|
||||
|
||||
$this->validate($request, [
|
||||
'category_id' => 'required'
|
||||
]);
|
||||
|
||||
$responseData = [
|
||||
'medias' => []
|
||||
];
|
||||
|
||||
$lastQueryId = $request->get('last_fetched_media_id');
|
||||
$category = $request->get('category_id');
|
||||
$limit = $request->get('limit');
|
||||
$mediaFiles = [];
|
||||
if(Auth::check()) {
|
||||
$userId = Auth::id();
|
||||
if($this->hasAppMediaPermission()) {
|
||||
if($lastQueryId) {
|
||||
$mediaFiles = $this->mediaFileRepository->getAppMediasWithLastQueryIdAndCategory($lastQueryId, $category, $limit ? $limit : 14);
|
||||
} else {
|
||||
$mediaFiles = $this->mediaFileRepository->getAppMediasWithCategory($category, $limit ? $limit : 35);
|
||||
}
|
||||
} else {
|
||||
if($lastQueryId) {
|
||||
$mediaFiles = $this->mediaFileRepository->getUserMediasWithLastQueryIdAndCategory($userId, $category, $lastQueryId, $limit ? $limit : 14);
|
||||
} else {
|
||||
$mediaFiles = $this->mediaFileRepository->getUserMediasWithCategory($userId, $category, $limit ? $limit : 35);
|
||||
}
|
||||
}
|
||||
foreach ($mediaFiles as $mediaFile) {
|
||||
$responseData['medias'][] = $this->createResponseMediaFile($mediaFile);
|
||||
}
|
||||
return response()->json($responseData);
|
||||
} else {
|
||||
return response()->json($responseData, 401);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新媒體檔案
|
||||
*
|
||||
* @param Request $request
|
||||
* @param $mediaId
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function updateMedia(Request $request, $mediaId)
|
||||
{
|
||||
$this->protectFromNoneAjaxRequest($request);
|
||||
|
||||
$status = 200;
|
||||
if(Auth::check()) {
|
||||
$media = $this->mediaFileRepository->getMedia($mediaId);
|
||||
if($media) {
|
||||
if($this->hasAppMediaPermission()) {
|
||||
if(!$media->is_app_media) {
|
||||
$status = 403;
|
||||
}
|
||||
} else {
|
||||
if(!$media->user->id == Auth::id()) {
|
||||
$status = 403;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$status = 404;
|
||||
}
|
||||
} else {
|
||||
$status = 401;
|
||||
}
|
||||
|
||||
if($status == 200) {
|
||||
$media->description = $request->get('description');
|
||||
$mediaCategoryId = $request->get('category_id');
|
||||
$mediaCategoryId = $mediaCategoryId == 'uncategorized' ? null : $mediaCategoryId;
|
||||
$media->media_category_id = $mediaCategoryId;
|
||||
$saved = $media->save();
|
||||
if($saved) {
|
||||
return response()->json([
|
||||
'message' => trans('message.descriptionHasBeenUpdated')
|
||||
]);
|
||||
} else {
|
||||
return response()->json([
|
||||
'message' => trans('message.failToUpdateDescription')
|
||||
], 400);
|
||||
}
|
||||
} else {
|
||||
return response()->json([
|
||||
'message' => trans('message.failToUpdateDescription')
|
||||
], $status);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刪除媒體檔案
|
||||
*
|
||||
* @param Request $request
|
||||
* @param $mediaId
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function deleteMedia(Request $request, $mediaId)
|
||||
{
|
||||
$this->protectFromNoneAjaxRequest($request);
|
||||
|
||||
$status = 200;
|
||||
if(Auth::check()) {
|
||||
$media = $this->mediaFileRepository->getMedia($mediaId);
|
||||
if($media) {
|
||||
if($this->hasAppMediaPermission()) {
|
||||
if(!$media->is_app_media) {
|
||||
$status = 403;
|
||||
}
|
||||
} else {
|
||||
if(!$media->user->id == Auth::id()) {
|
||||
$status = 403;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$status = 404;
|
||||
}
|
||||
} else {
|
||||
$status = 401;
|
||||
}
|
||||
|
||||
if($status == 200) {
|
||||
$deleted = $media->delete();
|
||||
if($deleted) {
|
||||
return response()->json([
|
||||
'id' => $media->id,
|
||||
'categoryId' => $media->media_category_id ? $media->media_category_id : 'uncategorized',
|
||||
'message' => trans('message.mediaHasBeenDeleted')
|
||||
]);
|
||||
} else {
|
||||
return response()->json([
|
||||
'message' => trans('message.failToDeleteMedia')
|
||||
], 400);
|
||||
}
|
||||
} else {
|
||||
return response()->json([
|
||||
'message' => trans('message.failToDeleteMedia')
|
||||
], $status);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新媒體檔案的分類
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
*/
|
||||
public function updateCategory(Request $request)
|
||||
{
|
||||
$this->protectFromNoneAjaxRequest($request);
|
||||
|
||||
$this->validate($request, [
|
||||
'category_id' => 'required',
|
||||
'media_ids' => 'required'
|
||||
]);
|
||||
|
||||
$status = 200;
|
||||
if(Auth::check()) {
|
||||
$userId = Auth::id();
|
||||
$categoryId = $request->get('category_id');
|
||||
$mediaIds = $request->get('media_ids');
|
||||
$mediaIds = is_array($mediaIds) ? $mediaIds : explode(',', $request->get('media_ids'));
|
||||
$mediaFiles = collect($mediaIds)->map(function($mediaId){
|
||||
return $this->mediaFileRepository->getMedia($mediaId);
|
||||
});
|
||||
$mediaCategory = $this->mediaCategoriesRepository->getCategory($categoryId);
|
||||
|
||||
$existenceCheck = true;
|
||||
if($categoryId != 'uncategorized') {
|
||||
$existenceCheck = $existenceCheck && $mediaCategory;
|
||||
}
|
||||
|
||||
foreach ($mediaFiles as $mediaFile) {
|
||||
$existenceCheck = $existenceCheck && $mediaFile;
|
||||
}
|
||||
|
||||
if($existenceCheck) {
|
||||
$permissionCheck = true;
|
||||
$hasAppMediaCategoryPermission = $this->hasAppMediaCategoryPermission();
|
||||
$hasAppMediaPermisssion = $this->hasAppMediaPermission();
|
||||
foreach ($mediaFiles as $mediaFile) {
|
||||
if($hasAppMediaPermisssion) {
|
||||
$permissionCheck = $permissionCheck && $mediaFile->is_app_media;
|
||||
} else {
|
||||
$permissionCheck = $permissionCheck && ($mediaFile->user_id == $userId);
|
||||
}
|
||||
}
|
||||
if($mediaCategory) {
|
||||
if($hasAppMediaCategoryPermission) {
|
||||
$permissionCheck = $permissionCheck && $mediaCategory->is_app_media_category;
|
||||
} else {
|
||||
$permissionCheck = $permissionCheck && ($mediaCategory->user_id == $userId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if($permissionCheck) {
|
||||
$categorySources = [];
|
||||
$mediaCategoryId = $mediaCategory ? $mediaCategory->id : null;
|
||||
foreach ($mediaFiles as $mediaFile) {
|
||||
$oldCategoryId = $mediaFile->media_category_id ? $mediaFile->media_category_id : 'uncategorized';
|
||||
if(!isset($categorySources[$oldCategoryId])) {
|
||||
$categorySources[$oldCategoryId] = 0;
|
||||
}
|
||||
$categorySources[$oldCategoryId]++;
|
||||
|
||||
$mediaFile->media_category_id = $mediaCategoryId;
|
||||
$mediaFile->save();
|
||||
}
|
||||
} else {
|
||||
$status = 403;
|
||||
}
|
||||
|
||||
} else {
|
||||
$status = 403;
|
||||
}
|
||||
|
||||
} else {
|
||||
$status = 401;
|
||||
}
|
||||
|
||||
if($status == 200) {
|
||||
$message = $mediaCategory
|
||||
? trans('message.mediaHasBeenMoveToCategory', ['name' => $mediaCategory->name])
|
||||
: trans('message.mediaHasBeenSetToUncategorized');
|
||||
return response()->json([
|
||||
'mediaIds' => $mediaIds,
|
||||
'categoryId' => $categoryId,
|
||||
'categorySources' => $categorySources,
|
||||
'message' => $message
|
||||
]);
|
||||
} else {
|
||||
return response()->json([
|
||||
'message' => trans('message.failToMoveMediaToCategory')
|
||||
], $status);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -23,8 +23,9 @@ class SetSiteStates
|
||||
public function handle($request, Closure $next)
|
||||
{
|
||||
$siteState = app(SiteStateService::class);
|
||||
$adminRoute = config('admin.route');
|
||||
//是否網站後台,判斷route prefix
|
||||
if($request->is(config('admin.route') . '/*')) {
|
||||
if($request->is($adminRoute . '/*', $adminRoute)) {
|
||||
$siteState->isAdminArea = true;
|
||||
}
|
||||
|
||||
|
||||
37
app/Listeners/MediaCategoryDeletingListener.php
Normal file
37
app/Listeners/MediaCategoryDeletingListener.php
Normal file
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Listeners;
|
||||
|
||||
use App\Events\MediaCategoryDeletingEvent;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
|
||||
class MediaCategoryDeletingListener
|
||||
{
|
||||
/**
|
||||
* Create the event listener.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the event.
|
||||
*
|
||||
* @param object $event
|
||||
* @return void
|
||||
*/
|
||||
public function handle(MediaCategoryDeletingEvent $event)
|
||||
{
|
||||
$mediaCategory = $event->mediaCategory;
|
||||
|
||||
$mediaFiles = $mediaCategory->mediaFiles;
|
||||
$mediaFiles->each(function($mediaFile){
|
||||
$mediaFile->media_category_id = null;
|
||||
$mediaFile->save();
|
||||
});
|
||||
}
|
||||
}
|
||||
46
app/Listeners/MediaFileDeletedListener.php
Normal file
46
app/Listeners/MediaFileDeletedListener.php
Normal file
@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Listeners;
|
||||
|
||||
use App\Events\MediaFileDeletedEvent;
|
||||
use App\MediaFile;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Storage;
|
||||
|
||||
class MediaFileDeletedListener
|
||||
{
|
||||
/**
|
||||
* Create the event listener.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the event.
|
||||
*
|
||||
* @param object $event
|
||||
* @return void
|
||||
*/
|
||||
public function handle(MediaFileDeletedEvent $event)
|
||||
{
|
||||
/**
|
||||
* 刪除檔案
|
||||
*/
|
||||
/** @var MediaFile $mediaFile */
|
||||
$mediaFile = $event->model;
|
||||
if($mediaFile->disk) {
|
||||
$disk = Storage::disk($mediaFile->disk->name);
|
||||
$disk->delete($mediaFile->fileNameWithPath);
|
||||
if($mediaFile->path) {
|
||||
if(empty($disk->allFiles($mediaFile->path)) && empty($disk->allDirectories($mediaFile->path))) {
|
||||
$disk->deleteDirectory($mediaFile->path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
57
app/MediaCategory.php
Normal file
57
app/MediaCategory.php
Normal file
@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* App\MediaCategory
|
||||
*
|
||||
* @property int $id
|
||||
* @property string $name 名稱
|
||||
* @property int $is_app_media_category 是否為網站媒體庫分類
|
||||
* @property int|null $seq 排序
|
||||
* @property int|null $user_id 所屬使用者
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection|\App\MediaFile[] $mediaFiles
|
||||
* @property-read int|null $media_files_count
|
||||
* @property-read \App\User|null $user
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaCategory newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaCategory newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaCategory query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaCategory whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaCategory whereIsAppMediaCategory($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaCategory whereName($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaCategory whereSeq($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaCategory whereUserId($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class MediaCategory extends Model
|
||||
{
|
||||
public $timestamps = false;
|
||||
|
||||
protected $dispatchesEvents = [
|
||||
'deleting' => \App\Events\MediaCategoryDeletingEvent::class
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'is_app_media_category',
|
||||
'user_id'
|
||||
];
|
||||
|
||||
protected $hidden = [
|
||||
'is_app_media_category',
|
||||
'user_id',
|
||||
'seq'
|
||||
];
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function mediaFiles()
|
||||
{
|
||||
return $this->hasMany(MediaFile::class);
|
||||
}
|
||||
}
|
||||
174
app/MediaFile.php
Normal file
174
app/MediaFile.php
Normal file
@ -0,0 +1,174 @@
|
||||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Storage;
|
||||
use File;
|
||||
|
||||
/**
|
||||
* App\MediaFile
|
||||
*
|
||||
* @property int $id
|
||||
* @property string|null $path 子資料夾路徑
|
||||
* @property string $file_name 檔名
|
||||
* @property string|null $ext 副檔名
|
||||
* @property int|null $disk_id disk名稱
|
||||
* @property int|null $media_source_id 外部來源id
|
||||
* @property int|null $size 檔案大小
|
||||
* @property int|null $width 圖片寬度
|
||||
* @property int|null $height 圖片高度
|
||||
* @property string|null $mime_type 檔案的Mime Type
|
||||
* @property int $is_app_media 是否為網站的媒體檔案
|
||||
* @property string|null $description 描述
|
||||
* @property int|null $media_category_id 媒體分類
|
||||
* @property int|null $user_id 所屬使用者
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \App\Disk|null $disk
|
||||
* @property-read mixed $file_name_with_path
|
||||
* @property-read mixed $file_path
|
||||
* @property-read mixed $url
|
||||
* @property-read mixed $youtube_embed_url
|
||||
* @property-read \App\MediaCategory|null $mediaCategory
|
||||
* @property-read \App\MediaSource|null $mediaSource
|
||||
* @property-read \App\User|null $user
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaFile category($categoryId)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaFile isAppMedia()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaFile newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaFile newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaFile query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaFile uncategorized()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaFile userMedia($userId)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaFile whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaFile whereDescription($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaFile whereDiskId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaFile whereExt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaFile whereFileName($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaFile whereHeight($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaFile whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaFile whereIsAppMedia($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaFile whereMediaCategoryId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaFile whereMediaSourceId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaFile whereMimeType($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaFile wherePath($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaFile whereSize($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaFile whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaFile whereUserId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaFile whereWidth($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class MediaFile extends Model
|
||||
{
|
||||
protected $dispatchesEvents = [
|
||||
'deleted' => \App\Events\MediaFileDeletedEvent::class
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'path',
|
||||
'file_name',
|
||||
'ext',
|
||||
'disk_id',
|
||||
'media_source_id',
|
||||
'size',
|
||||
'width',
|
||||
'height',
|
||||
'is_app_media',
|
||||
'user_id',
|
||||
'mime_type',
|
||||
'description'
|
||||
];
|
||||
|
||||
protected $hidden = [
|
||||
'path',
|
||||
'ext',
|
||||
'disk_id',
|
||||
'disk',
|
||||
'is_app_media',
|
||||
'user_id'
|
||||
];
|
||||
|
||||
public function getUrlAttribute()
|
||||
{
|
||||
return Storage::disk($this->disk->name)->url(trim($this->path, '/'). '/' . trim($this->file_name, '/'));
|
||||
}
|
||||
|
||||
public function getFilePathAttribute()
|
||||
{
|
||||
return Storage::disk($this->disk->name)->path(trim($this->path, '/'). '/' . trim($this->file_name, '/'));
|
||||
}
|
||||
|
||||
public function getFileNameWithPathAttribute()
|
||||
{
|
||||
return trim(trim($this->path, '/') . '/' . $this->file_name, '/');
|
||||
}
|
||||
|
||||
public function getYoutubeEmbedUrlAttribute()
|
||||
{
|
||||
$mediaSource = $this->mediaSource;
|
||||
if($mediaSource && 'youtube' == $mediaSource->name) {
|
||||
return "https://www.youtube.com/embed/$this->file_name";
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Illuminate\Database\Query\Builder $query
|
||||
*
|
||||
* @return \Illuminate\Database\Query\Builder
|
||||
*/
|
||||
public function scopeUncategorized($query)
|
||||
{
|
||||
return $query->where('media_category_id', null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Illuminate\Database\Query\Builder $query
|
||||
*
|
||||
* @return \Illuminate\Database\Query\Builder
|
||||
*/
|
||||
public function scopeCategory($query, $categoryId)
|
||||
{
|
||||
return $query->where('media_category_id', $categoryId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Illuminate\Database\Query\Builder $query
|
||||
*
|
||||
* @return \Illuminate\Database\Query\Builder
|
||||
*/
|
||||
public function scopeIsAppMedia($query)
|
||||
{
|
||||
return $query->where('is_app_media', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Illuminate\Database\Query\Builder $query
|
||||
*
|
||||
* @return \Illuminate\Database\Query\Builder
|
||||
*/
|
||||
public function scopeUserMedia($query, $userId)
|
||||
{
|
||||
return $query->where('is_app_media', false)->where('user_id', $userId);
|
||||
}
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function disk()
|
||||
{
|
||||
return $this->belongsTo(Disk::class);
|
||||
}
|
||||
|
||||
public function mediaCategory()
|
||||
{
|
||||
return $this->belongsTo(MediaCategory::class);
|
||||
}
|
||||
|
||||
public function mediaSource()
|
||||
{
|
||||
return $this->belongsTo(MediaSource::class);
|
||||
}
|
||||
}
|
||||
27
app/MediaSource.php
Normal file
27
app/MediaSource.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* App\MediaSource
|
||||
*
|
||||
* @property int $id
|
||||
* @property string $name 外部來源名稱
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection|\App\MediaFile[] $mediaFiles
|
||||
* @property-read int|null $media_files_count
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaSource newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaSource newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaSource query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaSource whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaSource whereName($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class MediaSource extends Model
|
||||
{
|
||||
public function mediaFiles()
|
||||
{
|
||||
return $this->hasMany(MediaFile::class);
|
||||
}
|
||||
}
|
||||
@ -4,6 +4,21 @@ namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* App\Option
|
||||
*
|
||||
* @property int $id
|
||||
* @property string $key 選項名稱
|
||||
* @property string $value 選項值
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\Option key($key)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\Option newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\Option newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\Option query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\Option whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\Option whereKey($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\Option whereValue($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class Option extends Model
|
||||
{
|
||||
protected $fillable = ['key', 'value'];
|
||||
|
||||
@ -18,6 +18,14 @@ class EventServiceProvider extends ServiceProvider
|
||||
Registered::class => [
|
||||
SendEmailVerificationNotification::class,
|
||||
],
|
||||
// MediaCategory刪除前的Event
|
||||
\App\Events\MediaCategoryDeletingEvent::class => [
|
||||
\App\Listeners\MediaCategoryDeletingListener::class
|
||||
],
|
||||
// MediaFile刪除後的Event
|
||||
\App\Events\MediaFileDeletedEvent::class => [
|
||||
\App\Listeners\MediaFileDeletedListener::class
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
82
app/Reposotories/MediaCategoriesRepository.php
Normal file
82
app/Reposotories/MediaCategoriesRepository.php
Normal file
@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repositories;
|
||||
|
||||
use App\MediaCategory;
|
||||
|
||||
/**
|
||||
* Class MediaCategoriesRepository
|
||||
* @package App\Repositories
|
||||
* @method MediaCategory createModel()
|
||||
* @method MediaCategory getModel()
|
||||
* @method MediaCategory findModel($id)
|
||||
*/
|
||||
class MediaCategoriesRepository extends BaseRepository
|
||||
{
|
||||
public function __construct(MediaCategory $mediaCategory)
|
||||
{
|
||||
$this->setModel($mediaCategory);
|
||||
}
|
||||
|
||||
public function getAllAppMediaCategories()
|
||||
{
|
||||
return $this->getModel()
|
||||
->where('is_app_media_category', true)
|
||||
->orderBy('seq')
|
||||
->orderBy('id', 'desc')
|
||||
->get();
|
||||
}
|
||||
|
||||
public function getAllUserCategories($userId)
|
||||
{
|
||||
return $this->getModel()
|
||||
->where('is_app_media_category', false)
|
||||
->where('user_id', $userId)
|
||||
->orderBy('seq')
|
||||
->orderBy('id', 'desc')
|
||||
->get();
|
||||
}
|
||||
|
||||
public function getMediaFilesCount($categoryId)
|
||||
{
|
||||
return $this->findModel($categoryId)->mediaFiles->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $name
|
||||
* @return \App\MediaCategory
|
||||
*/
|
||||
public function addCategory($name, $userId, $isAppMediaCategory)
|
||||
{
|
||||
$category = $this->createModel();
|
||||
$category->fill([
|
||||
'name' => $name,
|
||||
'user_id' => $userId,
|
||||
'is_app_media_category' => $isAppMediaCategory
|
||||
]);
|
||||
$saved = $category->save();
|
||||
if($saved) {
|
||||
return $category;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $id
|
||||
* @return \App\MediaCategory
|
||||
*/
|
||||
public function getCategory($id)
|
||||
{
|
||||
return $this->findModel($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $id
|
||||
* @return bool
|
||||
*/
|
||||
public function hasCategory($id)
|
||||
{
|
||||
return $this->hasModel($id);
|
||||
}
|
||||
}
|
||||
242
app/Reposotories/MediaFileRepository.php
Normal file
242
app/Reposotories/MediaFileRepository.php
Normal file
@ -0,0 +1,242 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repositories;
|
||||
|
||||
use App\MediaFile;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
/**
|
||||
* Class MediaFileRepository
|
||||
* @package App\Repositories
|
||||
* @method MediaFile createModel()
|
||||
* @method MediaFile getModel()
|
||||
* @method MediaFile findModel($id)
|
||||
*/
|
||||
class MediaFileRepository extends BaseRepository
|
||||
{
|
||||
public function __construct(MediaFile $mediaFile)
|
||||
{
|
||||
$this->setModel($mediaFile);
|
||||
}
|
||||
|
||||
private function queryBuilderGetLimit(Builder $queryBuilder, $limit = null)
|
||||
{
|
||||
if($limit) {
|
||||
return $queryBuilder->limit($limit)->get();
|
||||
}
|
||||
return $queryBuilder->get();
|
||||
}
|
||||
|
||||
private function queryBuilderWhereCategory(Builder $queryBuilder, $category = '')
|
||||
{
|
||||
if($category == 'uncategorized') {
|
||||
$queryBuilder = $queryBuilder->uncategorized();
|
||||
} else if(is_numeric($category)) {
|
||||
$queryBuilder = $queryBuilder->category($category);
|
||||
}
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $id
|
||||
* @return \App\MediaFile|null
|
||||
*/
|
||||
public function getMedia($id)
|
||||
{
|
||||
return $this->findModel($id);
|
||||
}
|
||||
|
||||
public function hasMedia($id)
|
||||
{
|
||||
return $this->hasModel($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $fileName
|
||||
* @param $ext
|
||||
* @param $diskId
|
||||
* @param $mediaSourceId
|
||||
* @param $mimeType
|
||||
* @param null $userId
|
||||
* @param null $width
|
||||
* @param null $height
|
||||
* @param bool $isAppMedia
|
||||
* @param $size
|
||||
* @return MediaFile|\Illuminate\Database\Eloquent\Model|boolean
|
||||
*/
|
||||
public function addMedia($fileName, $ext, $diskId, $mediaSourceId, $size, $mimeType, $userId, $path = null, $width = null, $height = null, $isAppMedia = false)
|
||||
{
|
||||
$mediaFile = $this->createModel();
|
||||
$mediaFile->fill([
|
||||
'path' => $path,
|
||||
'file_name' => $fileName,
|
||||
'ext' => $ext,
|
||||
'disk_id' => $diskId,
|
||||
'media_source_id' => $mediaSourceId,
|
||||
'size' => $size,
|
||||
'mime_type' => $mimeType,
|
||||
'width' => $width,
|
||||
'height' => $height,
|
||||
'is_app_media' => $isAppMedia,
|
||||
'user_id' => $userId,
|
||||
]);
|
||||
$saved = $mediaFile->save();
|
||||
return $saved ? $mediaFile : $saved;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $fileName
|
||||
* @param $ext
|
||||
* @param $size
|
||||
* @param $mimeType
|
||||
* @param $userId
|
||||
* @param null $width
|
||||
* @param null $height
|
||||
* @return MediaFile|\Illuminate\Database\Eloquent\Model
|
||||
*/
|
||||
public function addAppMedia($fileName, $ext, $size, $mimeType, $userId, $path = null, $width = null, $height = null)
|
||||
{
|
||||
return $this->addMedia($fileName, $ext, 1, null, $size, $mimeType, $userId, $path, $width, $height, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $fileName
|
||||
* @param $ext
|
||||
* @param $size
|
||||
* @param $mimeType
|
||||
* @param $userId
|
||||
* @param null $width
|
||||
* @param null $height
|
||||
* @return MediaFile|\Illuminate\Database\Eloquent\Model
|
||||
*/
|
||||
public function addUserMedia($fileName, $ext, $size, $mimeType, $userId, $path = null, $width = null, $height = null)
|
||||
{
|
||||
return $this->addMedia($fileName, $ext, 2, null, $size, $mimeType, $userId, $path, $width, $height);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param null $limit
|
||||
* @return MediaFile[]|\Illuminate\Database\Eloquent\Builder[]|\Illuminate\Database\Eloquent\Collection|\Illuminate\Database\Query\Builder[]|\Illuminate\Support\Collection
|
||||
*/
|
||||
public function getAppMedias($limit = null)
|
||||
{
|
||||
$queryBuilder = $this->getModel()
|
||||
->isAppMedia()
|
||||
->orderByDesc('id');
|
||||
return $this->queryBuilderGetLimit($queryBuilder, $limit);
|
||||
}
|
||||
|
||||
public function getAppMediasWithCategory($category, $limit = null)
|
||||
{
|
||||
$queryBuilder = $this->getModel()
|
||||
->isAppMedia()
|
||||
->orderByDesc('id');
|
||||
|
||||
$queryBuilder = $this->queryBuilderWhereCategory($queryBuilder, $category);
|
||||
|
||||
return $this->queryBuilderGetLimit($queryBuilder, $limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $lastQueryId
|
||||
* @param null $limit
|
||||
* @return MediaFile[]|\Illuminate\Database\Eloquent\Builder[]|\Illuminate\Database\Eloquent\Collection|\Illuminate\Database\Query\Builder[]|\Illuminate\Support\Collection
|
||||
*/
|
||||
public function getAppMediasWithLastQueryId($lastQueryId, $limit = null)
|
||||
{
|
||||
$queryBuilder = $this->getModel()
|
||||
->isAppMedia()
|
||||
->where('id', '<', $lastQueryId)
|
||||
->orderByDesc('id');
|
||||
return $this->queryBuilderGetLimit($queryBuilder, $limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $lastQueryId
|
||||
* @param null $limit
|
||||
* @return MediaFile[]|\Illuminate\Database\Eloquent\Builder[]|\Illuminate\Database\Eloquent\Collection|\Illuminate\Database\Query\Builder[]|\Illuminate\Support\Collection
|
||||
*/
|
||||
public function getAppMediasWithLastQueryIdAndCategory($lastQueryId, $category, $limit = null)
|
||||
{
|
||||
$queryBuilder = $this->getModel()
|
||||
->where('is_app_media', true)
|
||||
->where('id', '<', $lastQueryId)
|
||||
->orderByDesc('id');
|
||||
|
||||
$queryBuilder = $this->queryBuilderWhereCategory($queryBuilder, $category);
|
||||
|
||||
return $this->queryBuilderGetLimit($queryBuilder, $limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $userId
|
||||
* @param null $limit
|
||||
* @return Builder[]|\Illuminate\Database\Eloquent\Collection
|
||||
*/
|
||||
public function getUserMedias($userId, $limit = null)
|
||||
{
|
||||
$queryBuilder = $this->getModel()
|
||||
->userMedia($userId)
|
||||
->orderByDesc('id');
|
||||
return $this->queryBuilderGetLimit($queryBuilder, $limit);
|
||||
}
|
||||
|
||||
public function getUserMediasWithCategory($userId, $category, $limit = null)
|
||||
{
|
||||
$queryBuilder = $this->getModel()
|
||||
->userMedia($userId)
|
||||
->orderByDesc('id');
|
||||
|
||||
$queryBuilder = $this->queryBuilderWhereCategory($queryBuilder, $category);
|
||||
|
||||
return $this->queryBuilderGetLimit($queryBuilder, $limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $userId
|
||||
* @param $lastQueryId
|
||||
* @param null $limit
|
||||
* @return MediaFile[]|\Illuminate\Database\Eloquent\Builder[]|\Illuminate\Database\Eloquent\Collection|\Illuminate\Database\Query\Builder[]|\Illuminate\Support\Collection
|
||||
*/
|
||||
public function getUserMediasWithLastQueryId($userId, $lastQueryId, $limit = null)
|
||||
{
|
||||
$queryBuilder = $this->getModel()
|
||||
->userMedia($userId)
|
||||
->where('id', '<', $lastQueryId)
|
||||
->orderByDesc('id');
|
||||
|
||||
return $this->queryBuilderGetLimit($queryBuilder, $limit);
|
||||
}
|
||||
|
||||
public function getUserMediasWithLastQueryIdAndCategory($userId, $lastQueryId, $category, $limit = null)
|
||||
{
|
||||
$queryBuilder = $this->getModel()
|
||||
->userMedia($userId)
|
||||
->where('id', '<', $lastQueryId)
|
||||
->orderByDesc('id');
|
||||
|
||||
$queryBuilder = $this->queryBuilderWhereCategory($queryBuilder, $category);
|
||||
|
||||
return $this->queryBuilderGetLimit($queryBuilder, $limit);
|
||||
}
|
||||
|
||||
public function getAppUncategorizedMediasCount()
|
||||
{
|
||||
return $this->getAppMediasWithCategory('uncategorized')->count();
|
||||
}
|
||||
|
||||
public function getUserUncategorizedMediasCount($userId)
|
||||
{
|
||||
return $this->getUserMediasWithCategory($userId, 'uncategorized')->count();
|
||||
}
|
||||
|
||||
public function getAppMediasCount()
|
||||
{
|
||||
return $this->getModel()->isAppMedia()->count();
|
||||
}
|
||||
|
||||
public function getUserMediasCount($userId)
|
||||
{
|
||||
return $this->getModel()->userMedia($userId)->count();
|
||||
}
|
||||
}
|
||||
@ -40,8 +40,25 @@ class AppJsObjectService
|
||||
}
|
||||
|
||||
if($siteState->isAdminArea) {
|
||||
$obj['admin']['ajax']['resource'] = [
|
||||
'mediaFile' => [
|
||||
'upload' => route('media.upload'),
|
||||
'get' => route('media.get'),
|
||||
'update' => route('media.update', [':id']),
|
||||
'destroy' => route('media.destroy', [':id']),
|
||||
'updateCategory' => route('media.updateCategory')
|
||||
],
|
||||
'mediaCategory' => [
|
||||
'add' => route('media-category.store'),
|
||||
'index' => route('media-category.index'),
|
||||
'update' => route('media-category.update', [':id']),
|
||||
'destroy' => route('media-category.destroy', [':id']),
|
||||
'updateOrder' => route('media-category.updateOrder')
|
||||
],
|
||||
];
|
||||
$obj['admin']['translations'] = [
|
||||
'dataTables' => trans('datatables'),
|
||||
'mediaLibrary' => trans('mediaLibrary')
|
||||
];
|
||||
}
|
||||
return $obj;
|
||||
|
||||
21
app/Traits/PureAjaxMethodProtectable.php
Normal file
21
app/Traits/PureAjaxMethodProtectable.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Traits;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
/**
|
||||
* 使得Controller可以過濾非Ajax的請求
|
||||
*
|
||||
* Trait PureAjaxMethodProtectable
|
||||
* @package App\Traits
|
||||
*/
|
||||
trait PureAjaxMethodProtectable
|
||||
{
|
||||
// 非ajax請求則回應404
|
||||
protected function protectFromNoneAjaxRequest(Request $request)
|
||||
{
|
||||
if(!$request->ajax()) abort(404);
|
||||
return;
|
||||
}
|
||||
}
|
||||
94
app/Traits/UploadedFileProccessable.php
Normal file
94
app/Traits/UploadedFileProccessable.php
Normal file
@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace App\Traits;
|
||||
|
||||
use App\Repositories\MediaFileRepository;
|
||||
use App\Disk;
|
||||
use Auth;
|
||||
use Storage;
|
||||
use Str;
|
||||
|
||||
/**
|
||||
* 使得Controller可以上傳媒體檔案
|
||||
*
|
||||
* Trait UploadedFileProccessable
|
||||
* @package App\Traits
|
||||
*/
|
||||
trait UploadedFileProccessable
|
||||
{
|
||||
/**
|
||||
* 取得上傳的媒體檔案資訊
|
||||
*
|
||||
* @param \Symfony\Component\HttpFoundation\File\UploadedFile $file
|
||||
* @return array
|
||||
*/
|
||||
private function getUpadloedFileInfo(\Symfony\Component\HttpFoundation\File\UploadedFile $file)
|
||||
{
|
||||
$ext = $file->getClientOriginalExtension();
|
||||
$baseName = str_replace(".$ext", '', $file->getClientOriginalName());
|
||||
$ext = strtolower($ext);
|
||||
$alteredBaseName = Str::random(26) . time() . ".$ext";
|
||||
$imageDim = getimagesize($file);
|
||||
if($imageDim) {
|
||||
$width = $imageDim[0];
|
||||
$height = $imageDim[1];
|
||||
} else {
|
||||
$width = null;
|
||||
$height = null;
|
||||
}
|
||||
$fileSize = $file->getSize();
|
||||
$mimeType = $file->getMimeType();
|
||||
|
||||
return [
|
||||
'ext' => $ext,
|
||||
'fileName' => $alteredBaseName,
|
||||
'fileSize' => $fileSize,
|
||||
'mimeType' => $mimeType,
|
||||
'width' => $width,
|
||||
'height' => $height,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 上傳媒體檔案
|
||||
*
|
||||
* @param \Symfony\Component\HttpFoundation\File\UploadedFile $file
|
||||
* @param $diskId
|
||||
* @param $userId
|
||||
* @param string $path
|
||||
* @param bool $isAppMedia
|
||||
* @return \App\MediaFile|null
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function uploadMediaFile(\Symfony\Component\HttpFoundation\File\UploadedFile $file, $diskId, $userId, $path = null, $isAppMedia = false)
|
||||
{
|
||||
$fileInfo = $this->getUpadloedFileInfo($file);
|
||||
|
||||
$ext = $fileInfo['ext'];
|
||||
$fileName = $fileInfo['fileName'];
|
||||
$fileSize = $fileInfo['fileSize'];
|
||||
$mimeType = $fileInfo['mimeType'];
|
||||
$width = $fileInfo['width'];
|
||||
$height = $fileInfo['height'];
|
||||
|
||||
$mediaFileRepo = app(MediaFileRepository::class);
|
||||
|
||||
$disk = Disk::find($diskId);
|
||||
if($disk) {
|
||||
$mediaFile = $mediaFileRepo->addMedia($fileName, $ext, $diskId, null, $fileSize, $mimeType, $userId, $path, $width, $height, $isAppMedia);
|
||||
if($mediaFile) {
|
||||
$putted = Storage::disk($disk->name)->putFileAs($path, $file, $fileName);
|
||||
if($putted !== false) {
|
||||
return $mediaFile;
|
||||
} else {
|
||||
$mediaFile->delete();
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
throw new \Exception('Disk not found with id: ' . $diskId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -30,6 +30,12 @@ use Spatie\Permission\Traits\HasRoles;
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\User whereRememberToken($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\User whereUpdatedAt($value)
|
||||
* @mixin \Eloquent
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection|\Spatie\Permission\Models\Permission[] $permissions
|
||||
* @property-read int|null $permissions_count
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection|\Spatie\Permission\Models\Role[] $roles
|
||||
* @property-read int|null $roles_count
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\User permission($permissions)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\User role($roles, $guard = null)
|
||||
*/
|
||||
class User extends Authenticatable
|
||||
{
|
||||
|
||||
@ -1,5 +1,14 @@
|
||||
<?php
|
||||
return [
|
||||
// 自訂的disk,關聯到config/fielsystems.php下定義的disks
|
||||
'disks' => [
|
||||
1 => 'uploadedAppMedias',
|
||||
2 => 'uploadedUserMedias',
|
||||
],
|
||||
// 外連的media來源
|
||||
'mediaSources' => [
|
||||
1 => 'youtube'
|
||||
],
|
||||
'roles' => [
|
||||
['name' => 'administrator', 'displayName' => 'administrator'],
|
||||
['name' => 'editor', 'displayName' => 'editor'],
|
||||
@ -43,6 +52,22 @@ return [
|
||||
'displayName' => 'adminManageSystemStatus',
|
||||
'assignTo' => []
|
||||
],
|
||||
[
|
||||
//管理媒體庫
|
||||
'name' => 'manage app medias',
|
||||
'displayName' => 'manageAppMedias',
|
||||
'assignTo' => [
|
||||
'editor'
|
||||
]
|
||||
],
|
||||
[
|
||||
//管理媒體庫分類
|
||||
'name' => 'manage app media categories',
|
||||
'displayName' => 'manageAppMediaCategories',
|
||||
'assignTo' => [
|
||||
'editor'
|
||||
]
|
||||
],
|
||||
],
|
||||
// 預設的設定值
|
||||
'options' => [
|
||||
|
||||
@ -66,7 +66,7 @@ return [
|
||||
| Optionally, you can also send ServerTiming headers on ajax requests for the Chrome DevTools.
|
||||
*/
|
||||
|
||||
'capture_ajax' => true,
|
||||
'capture_ajax' => false,
|
||||
'add_ajax_timing' => false,
|
||||
|
||||
/*
|
||||
|
||||
@ -63,7 +63,19 @@ return [
|
||||
'bucket' => env('AWS_BUCKET'),
|
||||
'url' => env('AWS_URL'),
|
||||
],
|
||||
'uploadedAppMedias' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path('app/public/uploads/app'),
|
||||
'url' => env('APP_URL').'/storage/uploads/app',
|
||||
'visibility' => 'public',
|
||||
],
|
||||
|
||||
'uploadedUserMedias' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path('app/public/uploads/user'),
|
||||
'url' => env('APP_URL').'/storage/uploads/user',
|
||||
'visibility' => 'public',
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
33
database/migrations/2020_02_22_135647_create_disks_table.php
Normal file
33
database/migrations/2020_02_22_135647_create_disks_table.php
Normal file
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateDisksTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('disks', function (Blueprint $table) {
|
||||
$table->unsignedTinyInteger('id')->primary();
|
||||
$table->string('name', 40)->unique()->comment('名稱');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
DB::statement('SET FOREIGN_KEY_CHECKS = 0');
|
||||
Schema::dropIfExists('disks');
|
||||
DB::statement('SET FOREIGN_KEY_CHECKS = 1');
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateMediaCategoriesTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('media_categories', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->string('name', 100)->comment('名稱');
|
||||
$table->boolean('is_app_media_category')->default(false)->comment('是否為網站媒體庫分類');
|
||||
$table->unsignedInteger('seq')->nullable()->default(0)->comment('排序');
|
||||
$table->unsignedBigInteger('user_id')->nullable()->comment('所屬使用者');
|
||||
|
||||
$table->foreign('user_id')->references('id')->on('users');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('media_categories');
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateMediaFilesTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('media_files', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->text('path')->nullable()->comment('子資料夾路徑');
|
||||
$table->string('file_name', 50)->unique()->comment('檔名');
|
||||
$table->string('ext', 10)->nullable()->comment('副檔名');
|
||||
$table->unsignedTinyInteger('disk_id')->nullable()->comment('disk名稱');
|
||||
$table->unsignedInteger('size')->nullable()->comment('檔案大小');
|
||||
$table->unsignedMediumInteger('width')->nullable()->default(null)->comment('圖片寬度');
|
||||
$table->unsignedMediumInteger('height')->nullable()->default(null)->comment('圖片高度');
|
||||
$table->string('mime_type', 40)->nullable()->comment('檔案的Mime Type');
|
||||
$table->boolean('is_app_media')->default(false)->comment('是否為網站的媒體檔案');
|
||||
$table->text('description')->nullable()->comment('描述');
|
||||
$table->unsignedBigInteger('media_category_id')->nullable()->default(null)->comment('媒體分類');
|
||||
$table->bigInteger('user_id')->unsigned()->nullable()->comment('所屬使用者');
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreign('user_id')->references('id')->on('users');
|
||||
$table->foreign('disk_id')->references('id')->on('disks');
|
||||
$table->foreign('media_category_id')->references('id')->on('media_categories');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('media_files');
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateMediaSourcesTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('media_sources', function (Blueprint $table) {
|
||||
$table->unsignedTinyInteger('id')->primary();
|
||||
$table->string('name')->comment('外部來源名稱');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('media_sources');
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AddMediaSourceColumnToMediaFilesTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('media_files', function (Blueprint $table) {
|
||||
$table->unsignedTinyInteger('media_source_id')->nullable()->default(null)->after('disk_id')->comment('外部來源id');
|
||||
$table->foreign('media_source_id', 'source')->references('id')->on('media_sources');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('media_files', function (Blueprint $table) {
|
||||
DB::statement('SET FOREIGN_KEY_CHECKS = 0');
|
||||
$table->dropForeign('source'); // Drop foreign key 'user_id' from 'posts' table
|
||||
$table->dropColumn('media_source_id');
|
||||
DB::statement('SET FOREIGN_KEY_CHECKS = 1');
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -13,5 +13,7 @@ class DatabaseSeeder extends Seeder
|
||||
{
|
||||
$this->call(RolesAndPermissionsSeeder::class);
|
||||
$this->call(OptionsSeeder::class);
|
||||
$this->call(DisksSeeder::class);
|
||||
$this->call(MediaSourcesSeeder::class);
|
||||
}
|
||||
}
|
||||
|
||||
25
database/seeds/Preset/DisksSeeder.php
Normal file
25
database/seeds/Preset/DisksSeeder.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class DisksSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function run()
|
||||
{
|
||||
$disks = config('data-presets.disks');
|
||||
|
||||
$diskModel = app(\App\Disk::class);
|
||||
|
||||
foreach ($disks as $id => $disk) {
|
||||
$diskModel->insert([
|
||||
'id' => $id,
|
||||
'name' => $disk
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
25
database/seeds/Preset/MediaSourcesSeeder.php
Normal file
25
database/seeds/Preset/MediaSourcesSeeder.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class MediaSourcesSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function run()
|
||||
{
|
||||
$mediaSources = config('data-presets.mediaSources');
|
||||
|
||||
$mediaSourceModel = app(\App\MediaSource::class);
|
||||
|
||||
foreach ($mediaSources as $id => $mediaSource) {
|
||||
$mediaSourceModel->insert([
|
||||
'id' => $id,
|
||||
'name' => $mediaSource
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
6
package-lock.json
generated
6
package-lock.json
generated
@ -3281,6 +3281,12 @@
|
||||
"integrity": "sha1-3vHxyl1gWdJKdm5YeULCEQbOEnU=",
|
||||
"dev": true
|
||||
},
|
||||
"dropzone": {
|
||||
"version": "5.7.0",
|
||||
"resolved": "https://registry.npmjs.org/dropzone/-/dropzone-5.7.0.tgz",
|
||||
"integrity": "sha512-kOltiZXH5cO/72I22JjE+w6BoT6uaVLfWdFMsi1PMKFkU6BZWpqRwjnsRm0o6ANGTBuZar5Piu7m/CbKqRPiYg==",
|
||||
"dev": true
|
||||
},
|
||||
"duplexify": {
|
||||
"version": "3.7.1",
|
||||
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz",
|
||||
|
||||
@ -20,6 +20,7 @@
|
||||
"cross-env": "^7.0.0",
|
||||
"datatables.net-bs4": "^1.10.20",
|
||||
"datatables.net-responsive-bs4": "^2.2.3",
|
||||
"dropzone": "^5.7.0",
|
||||
"flag-icon-css": "^3.4.6",
|
||||
"font-awesome": "^4.7.0",
|
||||
"jquery": "^3.2",
|
||||
|
||||
107
resources/js/admin/app.js
vendored
107
resources/js/admin/app.js
vendored
@ -1,2 +1,109 @@
|
||||
import './lib';
|
||||
import '../app-common'
|
||||
import {block, unblock} from '../utils/blockui'
|
||||
import mediaLibrary from "../utils/media-library";
|
||||
|
||||
/**
|
||||
* 開啟媒體庫
|
||||
*
|
||||
* @param single 是否為單選模式
|
||||
* @param inputElement 自動會傳值進input元素
|
||||
* @param selectMode 是否出現選擇按鈕
|
||||
*/
|
||||
app.methods.media = function(single = true, inputElement = '', selectMode = true, callback = null) {
|
||||
let media = mediaLibrary({
|
||||
url: app.admin.ajax.resource.mediaFile.upload,
|
||||
translations: app.admin.translations.mediaLibrary,
|
||||
csrfToken: app.csrfToken,
|
||||
single: single,
|
||||
selectMode: selectMode,
|
||||
})
|
||||
|
||||
let resizeMediaLibraryWindow = () => {
|
||||
let windowHeight = $(window).height(),
|
||||
$mediaLib = $('#media-library');
|
||||
let totalHeight = windowHeight * .98,
|
||||
rowHeadHeight = 50,
|
||||
rowFooterHeight = 40,
|
||||
padding = 15 * 2,
|
||||
rowBodyHeight = totalHeight - padding - rowHeadHeight - rowFooterHeight;
|
||||
$mediaLib.height(totalHeight);
|
||||
$mediaLib.find('.row-head').height(rowHeadHeight);
|
||||
$mediaLib.find('.row-footer').height(rowFooterHeight);
|
||||
$mediaLib.find('.row-body').height(rowBodyHeight);
|
||||
$mediaLib.find('.tab-page, .category-list, .media-info-wrapper').height(rowBodyHeight);
|
||||
$mediaLib.find('.col-media-info').css('marginTop', this.matchMediaInPx(768) ? 0 : rowHeadHeight);
|
||||
media.$emit('resize', windowHeight, $(window).width())
|
||||
}
|
||||
|
||||
$(window).on('resize', resizeMediaLibraryWindow);
|
||||
resizeMediaLibraryWindow();
|
||||
|
||||
let closeMediaLibrary = () => {
|
||||
$('#media-library').remove();
|
||||
unblock();
|
||||
$(window).off('resize', resizeMediaLibraryWindow);
|
||||
}
|
||||
|
||||
if(callback) {
|
||||
media.$on('media_selected', function(data){
|
||||
callback(data);
|
||||
})
|
||||
}
|
||||
|
||||
if(inputElement) {
|
||||
media.$on('media_selected', function(data){
|
||||
if(typeof inputElement == 'string') {
|
||||
document.querySelectorAll(inputElement).forEach(function(element, index){
|
||||
element.value = data.idString;
|
||||
});
|
||||
} else if(inputElement instanceof HTMLElement) {
|
||||
inputElement.value = data.idString;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
media.$on('close', closeMediaLibrary);
|
||||
|
||||
block(media.$el, {
|
||||
blockMsgClass: 'media-library',
|
||||
onUnblock: function(){
|
||||
closeMediaLibrary()
|
||||
}
|
||||
})
|
||||
|
||||
let $dropzone = $(media.$children[0].$refs.uploadZone).dropzone({
|
||||
paramName: 'media_file',
|
||||
url: app.admin.ajax.resource.mediaFile.upload,
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': app.csrfToken
|
||||
},
|
||||
sending: function(file, xhr, formData){
|
||||
formData.append('category', media.d.currentCategory);
|
||||
},
|
||||
success:function(response){
|
||||
let responseBody = JSON.parse(response.xhr.response);
|
||||
if(responseBody.category == media.d.currentCategory) {
|
||||
media.d.medias.unshift(responseBody.media)
|
||||
}
|
||||
if(responseBody.category == 'all') {
|
||||
media.$children[0].addCategoryCount('uncategorized', 1);
|
||||
} else {
|
||||
media.$children[0].addCategoryCount(responseBody.category, 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
media.$on('remove_all_uploaded_files', function(){
|
||||
$dropzone.get(0).dropzone.removeAllFiles();
|
||||
});
|
||||
|
||||
return media;
|
||||
}
|
||||
|
||||
|
||||
$(() => {
|
||||
$('#app-header .medialibrary').on('click', function(){
|
||||
app.adminMediaLibrary = app.methods.media(false, null, false);
|
||||
});
|
||||
})
|
||||
|
||||
1
resources/js/admin/lib.js
vendored
1
resources/js/admin/lib.js
vendored
@ -25,6 +25,7 @@ import 'datatables.net-bs4';
|
||||
import 'datatables.net-responsive-bs4';
|
||||
import 'block-ui';
|
||||
|
||||
window.Dropzone = require('dropzone');
|
||||
try {
|
||||
|
||||
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
||||
|
||||
86
resources/js/apis/admin/media-category.js
vendored
Normal file
86
resources/js/apis/admin/media-category.js
vendored
Normal file
@ -0,0 +1,86 @@
|
||||
/**
|
||||
* 取得媒體分類
|
||||
*
|
||||
* 參數:
|
||||
* category_id: 分類ID
|
||||
* media_ids: 媒體的ID字串(使用逗號分隔)
|
||||
*
|
||||
* Response:
|
||||
* mediaCategories: 分類資料
|
||||
*/
|
||||
export const index = (data = {}) => {
|
||||
return axios.get(app.admin.ajax.resource.mediaCategory.index, {params: data});
|
||||
}
|
||||
/**
|
||||
* 新增媒體分類
|
||||
*
|
||||
* 參數:
|
||||
* name: 名稱
|
||||
*
|
||||
* Success Response:
|
||||
* mediaCategory: 被新增的分類
|
||||
* message: 訊息
|
||||
*
|
||||
* Error Response:
|
||||
* message: 訊息
|
||||
*/
|
||||
export const add = (name, data = {}) => {
|
||||
data = Object.assign({
|
||||
name: name
|
||||
}, data);
|
||||
return axios.post(app.admin.ajax.resource.mediaCategory.add, data);
|
||||
}
|
||||
/**
|
||||
* 更新媒體分類名稱
|
||||
*
|
||||
* 參數:
|
||||
* id: 分類ID
|
||||
* name: 名稱
|
||||
*
|
||||
* Success Response:
|
||||
* id: 分類ID
|
||||
* name: 分類名稱
|
||||
* message: 訊息
|
||||
*
|
||||
* Error Response:
|
||||
* message: 訊息
|
||||
*/
|
||||
export const update = (id, name, data = {}) => {
|
||||
data = Object.assign({
|
||||
name: name
|
||||
}, data);
|
||||
return axios.put(app.admin.ajax.resource.mediaCategory.update.replace(':id', id), data);
|
||||
}
|
||||
/**
|
||||
* 更新媒體分類排序
|
||||
*
|
||||
* 參數:
|
||||
* ids: 分類id(以逗號分隔)
|
||||
*
|
||||
*
|
||||
* Error Response:
|
||||
* message: 訊息
|
||||
*/
|
||||
export const updateOrder = (ids, data = {}) => {
|
||||
data = Object.assign({
|
||||
ids: ids
|
||||
}, data);
|
||||
return axios.put(app.admin.ajax.resource.mediaCategory.updateOrder, data);
|
||||
}
|
||||
/**
|
||||
* 刪除媒體分類
|
||||
*
|
||||
* 參數:
|
||||
* id: 分類ID
|
||||
*
|
||||
* Success Response:
|
||||
* id: 分類ID
|
||||
* count: 該媒體分類所有的媒體數量
|
||||
* message: 訊息
|
||||
*
|
||||
* Error Response:
|
||||
* message: 訊息
|
||||
*/
|
||||
export const destroy = (id, data = {}) => {
|
||||
return axios.delete(app.admin.ajax.resource.mediaCategory.destroy.replace(':id', id), data);
|
||||
}
|
||||
74
resources/js/apis/admin/media-file.js
vendored
Normal file
74
resources/js/apis/admin/media-file.js
vendored
Normal file
@ -0,0 +1,74 @@
|
||||
/**
|
||||
* 取得媒體
|
||||
*
|
||||
* 參數:
|
||||
* category_id: 分類ID
|
||||
* last_fetched_media_id: 最後一個取得的媒體media
|
||||
* limit: 取得的數量
|
||||
*
|
||||
* Response:
|
||||
* medias: 媒體資訊
|
||||
*/
|
||||
export const get = (categoryId, lastFetchedMediaId = null, limit = null, data = {}) => {
|
||||
data = Object.assign({
|
||||
category_id: categoryId,
|
||||
last_fetched_media_id: lastFetchedMediaId,
|
||||
limit: limit
|
||||
}, data)
|
||||
return axios.get(app.admin.ajax.resource.mediaFile.get, {params: data});
|
||||
}
|
||||
/**
|
||||
* 更新媒體描述
|
||||
*
|
||||
* 參數:
|
||||
* id: 媒體ID
|
||||
* description: 描述
|
||||
*
|
||||
* Response:
|
||||
* message: 訊息
|
||||
*/
|
||||
export const update = (id, description = '', data = {}) => {
|
||||
data = Object.assign({
|
||||
description: description
|
||||
}, data);
|
||||
return axios.put(app.admin.ajax.resource.mediaFile.update.replace(':id', id), data);
|
||||
}
|
||||
/**
|
||||
* 刪除媒體
|
||||
*
|
||||
* 參數:
|
||||
* id: 媒體ID
|
||||
*
|
||||
* Success Response:
|
||||
* id: 媒體ID
|
||||
* message: 訊息
|
||||
*
|
||||
* Error Response:
|
||||
* message: 訊息
|
||||
*/
|
||||
export const destroy = (id, data = {}) => {
|
||||
return axios.delete(app.admin.ajax.resource.mediaFile.destroy.replace(':id', id), data);
|
||||
}
|
||||
/**
|
||||
* 更新媒體的分類
|
||||
*
|
||||
* 參數:
|
||||
* category_id: 分類ID
|
||||
* media_ids: 媒體的ID字串(使用逗號分隔)
|
||||
*
|
||||
* Success Response:
|
||||
* mediaIds: 被移動的媒體ID
|
||||
* categoryId: 被移動至的分類ID
|
||||
* categorySources: 被移動的媒體的舊分類ID與數量
|
||||
* message: 訊息
|
||||
*
|
||||
* Error Response:
|
||||
* message: 訊息
|
||||
*/
|
||||
export const updateCategory = (mediaIds, categoryId, data = {}) => {
|
||||
data = Object.assign({
|
||||
media_ids: mediaIds,
|
||||
category_id: categoryId
|
||||
}, data)
|
||||
return axios.put(app.admin.ajax.resource.mediaFile.updateCategory, data);
|
||||
}
|
||||
18
resources/js/app-common.js
vendored
18
resources/js/app-common.js
vendored
@ -20,3 +20,21 @@ app.utils = {
|
||||
},
|
||||
select2: initSelect2,
|
||||
}
|
||||
|
||||
app.methods = {
|
||||
/**
|
||||
* 檢查是否符合給定的Media Query
|
||||
* 預設為max-width
|
||||
* */
|
||||
matchMedia: function(value, directive = 'max-width') {
|
||||
return window.matchMedia('(' + directive + ': ' + value + ')').matches;
|
||||
},
|
||||
|
||||
/**
|
||||
* 檢查是否符合給定的Media Query,單位為px
|
||||
* 預設為max-width
|
||||
* */
|
||||
matchMediaInPx(value, directive = 'max-width') {
|
||||
return this.matchMedia(value + 'px', directive);
|
||||
},
|
||||
}
|
||||
|
||||
717
resources/js/components/MediaLibrary.vue
Normal file
717
resources/js/components/MediaLibrary.vue
Normal file
@ -0,0 +1,717 @@
|
||||
<template>
|
||||
<div id="media-library">
|
||||
<div class="container-fluid">
|
||||
<div class="row no-gutters">
|
||||
<div class="close-window" @click="onCloseWindow"><i class="fa fa-close"></i></div>
|
||||
<div class="col-md-2 col-categories" v-show="!isMobile || _d.onlyShowCategory">
|
||||
<div class="row row-head no-gutters">
|
||||
<div class="col">
|
||||
<div class="controls">
|
||||
<div v-if="_d.addingNewCategory">
|
||||
<input type="text" class="form-control" v-model="_d.currentEditingCategoryText" @click.prevent.stop @keypress.enter="onAddCategory">
|
||||
<button class="btn btn-sm btn-success" @click.prevent.stop="onAddCategory"><i class="fa fa-check"></i></button>
|
||||
<button class="btn btn-sm btn-outline-secondary" @click.prevent.stop="onCancelCategoryAdding"><i class="fa fa-close"></i></button>
|
||||
</div>
|
||||
<div v-if="!_d.addingNewCategory">
|
||||
<button class="btn btn-success btn-sm" @click="_d.addingNewCategory=true"><i class="fa fa-plus"></i></button>
|
||||
<button class="btn btn-primary btn-sm" @click="onEditCategory(_d.currentCategory)"><i class="fa fa-edit"></i></button>
|
||||
<button class="btn btn-danger btn-sm" @click="onDeleteCategory(_d.currentCategory)"><i class="fa fa-close"></i></button>
|
||||
<button class="btn btn-outline-secondary btn-sm return" v-if="isMobile" @click="_d.onlyShowCategory=false"><i class="fa fa-arrow-right"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row row-body no-gutters">
|
||||
<div class="col">
|
||||
<div class="category-list-wrapper">
|
||||
<div class="message">
|
||||
<div class="alert alert-success" v-if="_d.categorySuccessMessage">{{ _d.categorySuccessMessage }}</div>
|
||||
<div class="alert alert-danger" v-if="_d.categoryErrorMessage">
|
||||
{{ _d.categoryErrorMessage }}
|
||||
<button type="button" class="close" aria-label="Close" @click="_d.categoryErrorMessage=null">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="spinner-border" role="status" v-if="_d.categoryFetching">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
<nav class="nav flex-column category-list" :class="{dragging: this._d.currentDraggingCategoryId}" ref="categoryList">
|
||||
<a class="nav-link category"
|
||||
href="#"
|
||||
:draggable="!isUneditableCategory(category.id)"
|
||||
:data-id="category.id"
|
||||
:class="{current: _d.currentCategory == category.id, editing: isCategoryEditing(category.id)}"
|
||||
@dragenter.capture.prevent.stop="onDragEnterCategory($event, category.id)"
|
||||
@dragleave.capture.prevent.stop="onDragLeaveCategory($event)"
|
||||
@drop="onDropOnCategory($event, category.id)"
|
||||
@dragstart.stop="onDragCategory(category.id)"
|
||||
@dragover.prevent.stop
|
||||
@dragend.capture.prevent.stop="onDropCategory"
|
||||
@click.prevent="switchCategory(category.id)"
|
||||
v-for="category in _d.categories">
|
||||
<span v-if="!isCategoryEditing(category.id)">
|
||||
<span class="name">{{ category.name }}</span>
|
||||
<span class="badge badge-secondary">{{ category.count }}</span>
|
||||
<button class="btn btn-sm edit-category" v-if="!isUneditableCategory(category.id)" @click.prevent.stop="onEditCategory(category.id)">
|
||||
<i class="fa fa-edit"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm remove-category" v-if="!isUneditableCategory(category.id)" @click.prevent.stop="onDeleteCategory(category.id)">
|
||||
<i class="fa fa-close"></i>
|
||||
</button>
|
||||
</span>
|
||||
<span v-if="isCategoryEditing(category.id)">
|
||||
<input type="text" class="form-control" v-model="_d.currentEditingCategoryText" @click.prevent.stop>
|
||||
<button class="btn btn-sm btn-primary" @click.prevent.stop="onUpdateCategoryName()"><i class="fa fa-check"></i></button>
|
||||
<button class="btn btn-sm btn-outline-secondary" @click.prevent.stop="cancelCategoryEditing"><i class="fa fa-close"></i></button>
|
||||
</span>
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row row-footer no-gutters">
|
||||
<div class="col"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col col-images" v-show="!isMobile || (isMobile && !_d.onlyShowCategory && !_d.onlyShowMediaInfo)">
|
||||
<div class="row row-head no-gutters">
|
||||
<div class="col">
|
||||
<ul class="nav nav-tabs tabs">
|
||||
<li class="nav-item" v-if="isMobile">
|
||||
<a href="#" class="nav-link" @click="_d.onlyShowCategory=true">
|
||||
{{ _d.text.tabCategory }}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#" @drag.prevent @click="_d.isUploadingPage=true"
|
||||
v-bind:class="{active: _d.isUploadingPage}">{{ _d.text.tabUpload }}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#" @drag.prevent @click="_d.isUploadingPage=false"
|
||||
v-bind:class="{active: !_d.isUploadingPage}">{{ _d.text.tabBrowse }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row row-body no-gutters">
|
||||
<div class="col">
|
||||
<div class="alert alert-success image-message" v-if="_d.imageSuccessMessage">
|
||||
{{ _d.imageSuccessMessage }}
|
||||
</div>
|
||||
<div class="alert alert-danger image-message" v-if="_d.imageErrorMessage">
|
||||
{{ _d.imageErrorMessage }}
|
||||
<button type="button" class="close" aria-label="Close" @click="_d.imageErrorMessage=null">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="tab-page upload" v-show="_d.isUploadingPage">
|
||||
<form class="upload-zone" v-bind:action="_d.formAction" ref="uploadZone">
|
||||
<input type="hidden" name="_token" v-bind:value="_d.csrfToken">
|
||||
<div class="fallback">
|
||||
<input name="media_file" type="file" multiple />
|
||||
</div>
|
||||
<div class="upload-tip"> {{ _d.text.dragOrClickToUpload }} </div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="tab-page browse" v-bind:class="{dragging : !isCurrentDraggingMediaIdsEmpty }" v-show="!_d.isUploadingPage" @scroll="onScrollBrowsing($event)">
|
||||
<div v-for="media in _d.medias"
|
||||
:key="media.id"
|
||||
@dragend="onDropMedia"
|
||||
@dragstart="onDragMedia(media.id)"
|
||||
draggable="true"
|
||||
class="img-wrapper"
|
||||
v-bind:class="{ selected: isMediaSelected(media)}"
|
||||
@click="onClickMedia($event, media)"
|
||||
@click.ctrl="onCtrlClickMedia($event, media)">
|
||||
<div class="img" :style="{ 'background-image': 'url(' + media.url + ')' }">
|
||||
</div>
|
||||
</div>
|
||||
<div class="spinner-border" role="status" v-if="_d.fetchingLock">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row row-footer no-gutters">
|
||||
<div class="col"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2 col-media-info" v-show="!isMobile || _d.onlyShowMediaInfo">
|
||||
<div class="row row-head" v-if="isMobile">
|
||||
<div class="col">
|
||||
<button class="btn btn-outline-secondary btn-sm return" @click="_d.onlyShowMediaInfo=false"><i class="fa fa-arrow-left"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row row-body no-gutters">
|
||||
<div class="col">
|
||||
<div class="media-info-wrapper" :class="{'select-mode': _d.selectMode}">
|
||||
<div class="message">
|
||||
<div class="alert alert-danger" v-if="_d.infoErrorMessage">
|
||||
{{ _d.infoErrorMessage }}
|
||||
<button type="button" class="close" aria-label="Close" @click="_d.infoErrorMessage=null">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="alert alert-success" v-if="_d.infoSuccessMessage">{{ _d.infoSuccessMessage }}</div>
|
||||
</div>
|
||||
<div class="info" :class="{hidden: !_d.currentSelectedMedia}">
|
||||
<div class="url">
|
||||
<span class="label font-weight-bold">{{ _d.text.url }} :</span>
|
||||
<div class="content">
|
||||
<a v-bind:href="_d.currentSelectedMedia.url" target="_blank">{{ _d.currentSelectedMedia.url }}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="size">
|
||||
<span class="label font-weight-bold">{{ _d.text.fileSize }}:</span>
|
||||
<span class="content">{{ fileSize }}</span>
|
||||
</div>
|
||||
<div class="date">
|
||||
<span class="label font-weight-bold">{{ _d.text.date }}:</span>
|
||||
<span class="content">{{ _d.currentSelectedMedia.date }}</span>
|
||||
</div>
|
||||
<div class="dimension">
|
||||
<span class="label font-weight-bold">{{ _d.text.size }}:</span>
|
||||
<span class="content">{{ dimension }}</span>
|
||||
</div>
|
||||
<div class="category">
|
||||
<span class="label font-weight-bold">{{ _d.text.category }}:</span>
|
||||
<select name="" id="" v-model="_d.currentSelectedMedia.category.id" v-if="_d.currentSelectedMedia.category">
|
||||
<option :value="category.id" v-for="category in this.mediaSettableCategories">{{ category.name }}</option>
|
||||
</select>
|
||||
<button class="btn btn-success btn-sm" @click.prevent="onUpdateMediaCategory()" v-if="_d.currentSelectedMedia">{{ _d.text.update }}</button>
|
||||
</div>
|
||||
<div class="description">
|
||||
<span class="label font-weight-bold">{{ _d.text.description }}</span>
|
||||
<div class="content">
|
||||
<textarea class="form-control" v-model="_d.currentSelectedMedia.description"></textarea>
|
||||
</div>
|
||||
<button class="btn btn-success btn-sm" @click.prevent="onUpdateMediaDescription($event)">{{ _d.text.update }}</button>
|
||||
<button class="btn btn-outline-danger btn-sm" @click.prevent="onDeleteMedia($event)">{{ _d.text.delete }}</button>
|
||||
</div>
|
||||
<div class="img">
|
||||
<img v-bind:src="_d.currentSelectedMedia.url" alt="">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row row-footer no-gutters">
|
||||
<div class="col">
|
||||
<div class="select-medias" v-if="_d.selectMode">
|
||||
<button class="btn btn-primary" @click="onDoneSelectedMedias" :disabled="isSelectedMediasEmpty">{{ _d.text.select }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "media-library",
|
||||
props: ['_d'],
|
||||
computed: {
|
||||
isMobile() {
|
||||
let isMobile = this._d.windowWidth < 768;
|
||||
return isMobile;
|
||||
},
|
||||
mediaSettableCategories() {
|
||||
return this._d.categories.filter(category => !this.isAllCategory(category.id));
|
||||
},
|
||||
// 是否為單選模式
|
||||
isSingleSelectionMode() {
|
||||
return this._d.single;
|
||||
},
|
||||
isSelectedMediasEmpty() {
|
||||
return this._d.selectedMedias.length ? false : true;
|
||||
},
|
||||
selectedMediaIds() {
|
||||
return this._d.selectedMedias.map(_media => _media.id);
|
||||
},
|
||||
isCurrentDraggingMediaIdsEmpty() {
|
||||
return this._d.currentDraggingMediaIds ? false : true;
|
||||
},
|
||||
dimension() {
|
||||
if(this._d.currentSelectedMedia.width !== null && this._d.currentSelectedMedia.height !== null) {
|
||||
return this._d.currentSelectedMedia.width + 'x' + this._d.currentSelectedMedia.height;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
fileSize() {
|
||||
if(this._d.currentSelectedMedia.sizeInBytes) {
|
||||
let size = this._d.currentSelectedMedia.sizeInBytes;
|
||||
|
||||
if(size < 1024)
|
||||
return size + ' Bytes';
|
||||
|
||||
let unit = 0;
|
||||
while(size >= 1024) {
|
||||
unit++;
|
||||
size /= 1024;
|
||||
}
|
||||
size = parseFloat(size).toFixed(2)
|
||||
|
||||
switch(unit) {
|
||||
case 1:
|
||||
size += ' KB';
|
||||
break;
|
||||
case 2:
|
||||
size += ' MB';
|
||||
break;
|
||||
case 3:
|
||||
size += ' GB';
|
||||
break;
|
||||
}
|
||||
return size;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 設定已選擇的媒體
|
||||
setSelectedMedias(selectedMedias) {
|
||||
this._d.selectedMedias = selectedMedias;
|
||||
},
|
||||
// 清空被選擇的媒體
|
||||
clearSelectedMedia() {
|
||||
this.setSelectedMedias([]);
|
||||
},
|
||||
// 加入媒體至被選擇的媒體中
|
||||
addToSelectedMedia(media) {
|
||||
this._d.selectedMedias.push(media)
|
||||
},
|
||||
// 媒體是否已被選擇(by ID)
|
||||
isMediaIdSelected(id) {
|
||||
let foundMedias = this._d.selectedMedias.find(_media => _media.id == id);
|
||||
return foundMedias ? true : false;
|
||||
},
|
||||
// 媒體是否已被選擇
|
||||
isMediaSelected(media) {
|
||||
return this.isMediaIdSelected(media.id);
|
||||
},
|
||||
// 從被選擇的媒體中刪除媒體
|
||||
deleteFromSelectedMedia(media) {
|
||||
this.setSelectedMedias(this._d.selectedMedias.filter(_media => _media.id != media.id));
|
||||
},
|
||||
// 從被選擇的媒體中刪除或新增媒體
|
||||
addOrDeleteSelectedMedia(media) {
|
||||
if(this.isMediaSelected(media)) {
|
||||
this.unselectMedia(media);
|
||||
} else {
|
||||
this.addToSelectedMedia(media);
|
||||
}
|
||||
},
|
||||
// 設定單一個媒體至被選擇的媒體
|
||||
setSingleSelectedMedia(media) {
|
||||
this.clearSelectedMedia();
|
||||
this.addToSelectedMedia(media);
|
||||
this.setCurrentSelectedMedia(media);
|
||||
},
|
||||
// 反選擇媒體
|
||||
unselectMedia(media) {
|
||||
this.deleteFromSelectedMedia(media);
|
||||
this.clearCurrentSelectedMedia();
|
||||
},
|
||||
// 清除目前點選的媒體
|
||||
clearCurrentSelectedMedia() {
|
||||
this._d.currentSelectedMedia = false
|
||||
},
|
||||
// 設定目前點選的媒體
|
||||
setCurrentSelectedMedia(media) {
|
||||
this._d.currentSelectedMedia = media;
|
||||
if(this.isMobile) {
|
||||
this._d.onlyShowMediaInfo = true;
|
||||
}
|
||||
},
|
||||
// 取得媒體資料
|
||||
findMedia(id) {
|
||||
return this._d.medias.find(media => media.id == id);
|
||||
},
|
||||
// 分類ID為"所有"
|
||||
isAllCategory(id) {
|
||||
return id == 'all';
|
||||
},
|
||||
// 分類ID為"未分類"
|
||||
isUncategorizedCategory(id) {
|
||||
return id == 'uncategorized';
|
||||
},
|
||||
// 無法編輯的分類
|
||||
isUneditableCategory(id) {
|
||||
return this.isUncategorizedCategory(id) || this.isAllCategory(id)
|
||||
},
|
||||
// 特定ID的分類正在編輯
|
||||
isCategoryEditing(id) {
|
||||
return this._d.currentEditingCategory == id;
|
||||
},
|
||||
// 取得分類資料
|
||||
findCategory(id) {
|
||||
return this._d.categories.find(category => category.id == id);
|
||||
},
|
||||
// 取得分類名稱
|
||||
getCategoryName(id) {
|
||||
let category = this.findCategory(id);
|
||||
return category ? category.name : null;
|
||||
},
|
||||
// 刪除媒體
|
||||
removeMediaFromList(mediaId) {
|
||||
this._d.medias = this._d.medias.filter(media => media.id != mediaId);
|
||||
if(this._d.currentSelectedMedia.id == mediaId) {
|
||||
this.clearCurrentSelectedMedia();
|
||||
}
|
||||
},
|
||||
// click事件,選擇媒體
|
||||
onClickMedia(e, media, fromMulti = false) {
|
||||
if(e.ctrlKey && !fromMulti) {
|
||||
return;
|
||||
}
|
||||
if(
|
||||
(this.isSingleSelectionMode && this.isMediaSelected(media)) ||
|
||||
(!this.isSingleSelectionMode && this.isMediaSelected(media) && this._d.selectedMedias.length == 1)
|
||||
) {
|
||||
this.unselectMedia(media);
|
||||
} else {
|
||||
this.setSingleSelectedMedia(media);
|
||||
}
|
||||
},
|
||||
// click事件,多選媒體
|
||||
onCtrlClickMedia(e, media) {
|
||||
this.setCurrentSelectedMedia(media);
|
||||
if(this.isSingleSelectionMode) {
|
||||
this.onClickMedia(e, media, true);
|
||||
} else {
|
||||
this.addOrDeleteSelectedMedia(media);
|
||||
}
|
||||
},
|
||||
// click事件,回傳選擇的媒體
|
||||
onDoneSelectedMedias() {
|
||||
let data = [], idString = '';
|
||||
this._d.selectedMedias.forEach(_media => {
|
||||
data.push({
|
||||
id: _media.id,
|
||||
url: _media.url
|
||||
})
|
||||
})
|
||||
idString = data.map(media => media.id).join(',')
|
||||
|
||||
this.$root.$emit('media_selected', {
|
||||
idString: idString,
|
||||
medias: data
|
||||
})
|
||||
this.onCloseWindow();
|
||||
},
|
||||
// click事件,關閉視窗
|
||||
onCloseWindow() {
|
||||
this.$root.$emit('close');
|
||||
},
|
||||
onUpdateMediaCategory() {
|
||||
this.$root.$emit('update_category_for_medias', this._d.currentSelectedMedia.category.id, this._d.currentSelectedMedia.id, false);
|
||||
},
|
||||
// click事件,更新媒體描述
|
||||
onUpdateMediaDescription(e) {
|
||||
this.$root.$emit('update_media_description', e, this._d.currentSelectedMedia)
|
||||
},
|
||||
// click事件,刪除媒體
|
||||
onDeleteMedia(e) {
|
||||
this.$root.$emit('delete_media', e, this._d.currentSelectedMedia)
|
||||
},
|
||||
// 更新媒體的分類名稱
|
||||
updateCategoryForMedia(mediaId, categoryId) {
|
||||
let media = this._d.medias.find(media => media.id == mediaId);
|
||||
if(media) {
|
||||
let category = this.findCategory(categoryId);
|
||||
if(category) {
|
||||
media.category.name = category.name;
|
||||
media.category.id = categoryId;
|
||||
}
|
||||
}
|
||||
},
|
||||
// 設定目前正在拖曳的媒體ID
|
||||
setCurrentDraggingMediaIds(ids) {
|
||||
this._d.currentDraggingMediaIds = ids;
|
||||
},
|
||||
// 清除目前正在拖曳的媒體ID
|
||||
clearCurrentDraggingMediaIds() {
|
||||
this._d.currentDraggingMediaIds = null;
|
||||
},
|
||||
// click事件,刪除所有選擇的媒體
|
||||
deleteSelectedMedias() {
|
||||
//TODO 批次刪除媒體
|
||||
},
|
||||
// scroll事件,載入更多媒體
|
||||
onScrollBrowsing(e) {
|
||||
if(this._d.fetchingEnd) return;
|
||||
let ele = e.target;
|
||||
if(ele.scrollTop >= ele.scrollHeight - ele.clientHeight - 10) {
|
||||
if(!this._d.fetchingLock) {
|
||||
this.$root.$emit('fetch_medias', 14);
|
||||
}
|
||||
}
|
||||
},
|
||||
clearInfoMessages() {
|
||||
this._d.infoSuccessMessage = '';
|
||||
this._d.infoErrorMessage = '';
|
||||
},
|
||||
showInfoSuccessMessage(message) {
|
||||
this._d.infoSuccessMessage = message;
|
||||
setTimeout(() => {
|
||||
this._d.infoSuccessMessage = '';
|
||||
}, 5000);
|
||||
},
|
||||
showInfoErrorMessage(message) {
|
||||
this._d.infoErrorMessage = message;
|
||||
},
|
||||
clearCategoryMessages() {
|
||||
this._d.categorySuccessMessage = '';
|
||||
this._d.categoryErrorMessage = '';
|
||||
},
|
||||
showCategorySuccessMessage(message) {
|
||||
this._d.categorySuccessMessage = message;
|
||||
setTimeout(() => {
|
||||
this._d.categorySuccessMessage = '';
|
||||
}, 1000);
|
||||
},
|
||||
showCategoryErrorMessage(message) {
|
||||
this._d.categoryErrorMessage = message;
|
||||
},
|
||||
clearImageMessages() {
|
||||
this._d.imageSuccessMessage = '';
|
||||
this._d.imageErrorMessage = '';
|
||||
},
|
||||
showImageSuccessMessage(message) {
|
||||
this._d.imageSuccessMessage = message;
|
||||
setTimeout(() => {
|
||||
this._d.imageSuccessMessage = '';
|
||||
}, 1000);
|
||||
},
|
||||
showImageErrorMessage(message) {
|
||||
this._d.imageErrorMessage = message;
|
||||
},
|
||||
// drag事件,拖曳媒體
|
||||
onDragMedia(id) {
|
||||
if(!this.isSelectedMediasEmpty && this.isMediaIdSelected(id)) {
|
||||
this.setCurrentDraggingMediaIds(this.selectedMediaIds);
|
||||
} else {
|
||||
this.setCurrentDraggingMediaIds(id);
|
||||
}
|
||||
},
|
||||
// dragenter事件,拖曳媒體或分類至分類上
|
||||
onDragEnterCategory(e, id) {
|
||||
if(this._d.currentDraggingCategoryId) {
|
||||
if(this.isUneditableCategory(id)) return;
|
||||
if(!this.isCurrentDraggingMediaIdsEmpty) return;
|
||||
|
||||
let element = this.findCategoryElementById(id);
|
||||
this._d.currentDraggingOverCategoryId = id;
|
||||
if(this.isCategoryDragSourceBefore(this._d.currentDraggingCategoryId, id)) {
|
||||
this.insertAfter(this._d.categoryDraggingIndicator, element)
|
||||
} else {
|
||||
this.insertBefore(this._d.categoryDraggingIndicator, element)
|
||||
}
|
||||
} else if(!this.isCurrentDraggingMediaIdsEmpty) {
|
||||
if(!this.isAllCategory(id) && this._d.currentCategory != id) {
|
||||
e.target.classList.add('on-drag');
|
||||
}
|
||||
}
|
||||
},
|
||||
// dragleave事件,拖曳媒體出分類
|
||||
onDragLeaveCategory(e) {
|
||||
e.target.classList.remove('on-drag');
|
||||
},
|
||||
// dragend事件,放開拖曳媒體
|
||||
onDropMedia() {
|
||||
this.clearCurrentDraggingMediaIds();
|
||||
},
|
||||
// drop事件,拖曳媒體並drop至分類上
|
||||
onDropOnCategory(e, id) {
|
||||
e.target.classList.remove('on-drag');
|
||||
if(!this.isAllCategory(id) && this._d.currentCategory != id && !this.isCurrentDraggingMediaIdsEmpty) {
|
||||
this.$root.$emit('update_category_for_medias', id, this._d.currentDraggingMediaIds);
|
||||
}
|
||||
},
|
||||
// 切換分類
|
||||
switchCategory(id) {
|
||||
if(this._d.currentEditingCategory) {
|
||||
this.cancelCategoryEditing();
|
||||
}
|
||||
|
||||
if(!this._d.fetchingLock && !this._d.currentEditingCategory) {
|
||||
this.clearSelectedMedia();
|
||||
this.clearCurrentSelectedMedia();
|
||||
this._d.medias = [];
|
||||
this._d.currentCategory = id;
|
||||
this._d.lastFetchedMediaId = null;
|
||||
this._d.fetchingEnd = false;
|
||||
this.$root.$emit('fetch_medias', 35);
|
||||
this.$root.$emit('remove_all_uploaded_files');
|
||||
}
|
||||
},
|
||||
// 減少分類的媒體數量
|
||||
substractCategoryCount(id, number, substractFromAll = true) {
|
||||
let category = this.findCategory(id);
|
||||
if(category) {
|
||||
category.count -= number;
|
||||
}
|
||||
if(substractFromAll) {
|
||||
let category = this.findCategory('all');
|
||||
if(category) {
|
||||
category.count -= number;
|
||||
}
|
||||
}
|
||||
},
|
||||
// 增加分類的媒體數量
|
||||
addCategoryCount(id, number, addToAll = true) {
|
||||
let category = this.findCategory(id);
|
||||
if(category) {
|
||||
category.count += number;
|
||||
}
|
||||
if(addToAll) {
|
||||
let category = this.findCategory('all');
|
||||
if(category) {
|
||||
category.count += number;
|
||||
}
|
||||
}
|
||||
},
|
||||
// click事件,編輯分類
|
||||
onEditCategory(id) {
|
||||
if(!this.isUneditableCategory(id)) {
|
||||
this._d.currentEditingCategory = id;
|
||||
this._d.currentEditingCategoryText = this.getCategoryName(id);
|
||||
}
|
||||
},
|
||||
// 從列表刪除分類
|
||||
deleteCategoryFromList(id) {
|
||||
this._d.categories = this._d.categories.filter(category => category.id != id);
|
||||
if(id == this._d.currentCategory) {
|
||||
this._d.medias = [];
|
||||
this._d.currentCategory = null;
|
||||
}
|
||||
},
|
||||
// click事件,刪除分類
|
||||
onDeleteCategory(id) {
|
||||
if(!this.isUneditableCategory(id) && id) {
|
||||
if(confirm(this._d.text.deleteConfirmation)) {
|
||||
this.$root.$emit('delete_category', id);
|
||||
}
|
||||
}
|
||||
},
|
||||
// 取消分類編輯
|
||||
cancelCategoryEditing() {
|
||||
this._d.currentEditingCategory = null;
|
||||
this._d.currentEditingCategoryText = null;
|
||||
},
|
||||
// click事件,更新分類名稱
|
||||
onUpdateCategoryName() {
|
||||
this.$root.$emit('update_category_name', this._d.currentEditingCategory, this._d.currentEditingCategoryText)
|
||||
},
|
||||
// 更新分類名稱
|
||||
updateListCategoryName(id, name) {
|
||||
let category = this.findCategory(id);
|
||||
if(category) {
|
||||
category.name = name;
|
||||
}
|
||||
},
|
||||
// click事件,取消新增分類
|
||||
onCancelCategoryAdding() {
|
||||
this._d.addingNewCategory = false;
|
||||
},
|
||||
// click事件,新增分類
|
||||
onAddCategory() {
|
||||
this.$root.$emit('add_category', this._d.currentEditingCategoryText);
|
||||
},
|
||||
// 新增分類至列表開頭
|
||||
addCategoryToList(id, name) {
|
||||
let _categories = [];
|
||||
_categories = _categories.concat(this._d.categories.filter(category => this.isUneditableCategory(category.id)))
|
||||
_categories.push({
|
||||
id: id,
|
||||
name: name,
|
||||
count: 0,
|
||||
})
|
||||
_categories = _categories.concat(this._d.categories.filter(category => !this.isUneditableCategory(category.id)))
|
||||
this._d.categories = _categories;
|
||||
},
|
||||
// 從分類列表中找尋分類的element
|
||||
findCategoryElementById(id) {
|
||||
let element = document.querySelector('.category[data-id="' + id + '"]');
|
||||
return element;
|
||||
},
|
||||
// insert after
|
||||
insertAfter(newNode, referenceNode) {
|
||||
referenceNode.parentNode.appendChild(newNode);
|
||||
referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling);
|
||||
},
|
||||
// insert before
|
||||
insertBefore(newNode, referenceNode) {
|
||||
referenceNode.parentNode.appendChild(newNode);
|
||||
referenceNode.parentNode.insertBefore(newNode, referenceNode);
|
||||
},
|
||||
// 拖曳的分類比目的分類還前面
|
||||
isCategoryDragSourceBefore(sourceId, targetId) {
|
||||
let dragSourceIndex = 0, dragTargetIndex = 0;
|
||||
this._d.categories.forEach((category, index) => {
|
||||
if(category.id == sourceId) {
|
||||
dragSourceIndex = index;
|
||||
}
|
||||
if(category.id == targetId) {
|
||||
dragTargetIndex = index;
|
||||
}
|
||||
});
|
||||
return dragSourceIndex < dragTargetIndex;
|
||||
},
|
||||
// dragstart事件,拖曳分類
|
||||
onDragCategory(id) {
|
||||
this._d.currentDraggingCategoryId = id;
|
||||
|
||||
let indicatorElement = document.createElement('span')
|
||||
indicatorElement.className = 'dragging-indicator';
|
||||
this._d.categoryDraggingIndicator = indicatorElement;
|
||||
},
|
||||
// 交換分類的順序
|
||||
switchCategoryOrder(draggingSourceId, draggingTargetId) {
|
||||
if(draggingSourceId == draggingTargetId) return;
|
||||
let _categories = [];
|
||||
let category = this.findCategory(draggingSourceId);
|
||||
let insertBefore = this.isCategoryDragSourceBefore(draggingSourceId, draggingTargetId);
|
||||
|
||||
if(!insertBefore) {
|
||||
this._d.categories.reverse();
|
||||
}
|
||||
|
||||
this._d.categories.forEach((_category, index) => {
|
||||
if(_category.id != category.id) {
|
||||
_categories.push(_category)
|
||||
}
|
||||
if(_category.id == draggingTargetId) {
|
||||
_categories.push(category);
|
||||
}
|
||||
})
|
||||
|
||||
if(!insertBefore) {
|
||||
_categories.reverse();
|
||||
}
|
||||
this._d.categories = _categories
|
||||
this.$root.$emit('update_category_order',
|
||||
_categories
|
||||
.filter(category => !this.isUneditableCategory(category.id))
|
||||
.map(category => category.id)
|
||||
.join(',')
|
||||
);
|
||||
},
|
||||
// dragend事件,拖曳分類並放置分類上
|
||||
onDropCategory() {
|
||||
let indicator = document.querySelector('.dragging-indicator')
|
||||
indicator.parentNode.removeChild(indicator);
|
||||
this.switchCategoryOrder(this._d.currentDraggingCategoryId, this._d.currentDraggingOverCategoryId);
|
||||
this._d.currentDraggingOverCategoryId = null;
|
||||
this._d.currentDraggingCategoryId = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
335
resources/js/utils/media-library.js
vendored
Normal file
335
resources/js/utils/media-library.js
vendored
Normal file
@ -0,0 +1,335 @@
|
||||
import mediaLibrary from '../components/MediaLibrary';
|
||||
import {
|
||||
get as fetchMedia,
|
||||
update as updateMediaDescription,
|
||||
destroy as deleteMedia,
|
||||
updateCategory as updateCategoryForMedia
|
||||
} from '../apis/admin/media-file';
|
||||
import {
|
||||
index as fetchMediaCategories,
|
||||
destroy as deleteMediaCategory,
|
||||
add as addMediaCategory,
|
||||
update as updateMediaCategory,
|
||||
updateOrder as updateMediaCategoriesOrder
|
||||
} from '../apis/admin/media-category';
|
||||
export default function(param) {
|
||||
// 如果有存在的媒體庫則先刪除
|
||||
let previousElement = document.getElementById('media-library');
|
||||
if (previousElement) {
|
||||
previousElement.remove();
|
||||
}
|
||||
// new一個新的媒體庫結構
|
||||
let mediaLibraryElement = document.createElement('media-library');
|
||||
// 設定vue的子組件屬性映射
|
||||
mediaLibraryElement.setAttribute(':_d', 'd')
|
||||
// 媒體庫元件append進body
|
||||
document.body.appendChild(mediaLibraryElement);
|
||||
// 初始化Vue
|
||||
let vue = new Vue({
|
||||
el: mediaLibraryElement,
|
||||
data: {
|
||||
d: {
|
||||
// 文字翻譯
|
||||
text: param.translations,
|
||||
// 上傳檔案的Url
|
||||
formAction: param.url,
|
||||
// csrf token
|
||||
csrfToken: param.csrfToken,
|
||||
// 是否為單選模式
|
||||
single: param.single,
|
||||
// 是否顯示選擇按鈕
|
||||
selectMode: param.selectMode,
|
||||
// 是否為上傳的頁面
|
||||
isUploadingPage: false,
|
||||
// 媒體列表
|
||||
medias: [],
|
||||
// 目前點選的媒體
|
||||
currentSelectedMedia: false,
|
||||
// 選中的媒體列表
|
||||
selectedMedias: [],
|
||||
// 最後一個取得的媒體ID
|
||||
lastFetchedMediaId: null,
|
||||
// 取得媒體請求的lock
|
||||
fetchingLock: false,
|
||||
// 已取的最後一個媒體
|
||||
fetchingEnd: false,
|
||||
// 正在取得分類
|
||||
categoryFetching: false,
|
||||
// 媒體資訊更新失敗訊息
|
||||
infoErrorMessage: '',
|
||||
// 媒體資訊更新成功訊息
|
||||
infoSuccessMessage: '',
|
||||
// 媒體分類
|
||||
categories: [],
|
||||
// 目前正在拖曳的媒體ID
|
||||
currentDraggingMediaIds: null,
|
||||
// 目前所在的媒體分類
|
||||
currentCategory: 'uncategorized',
|
||||
// 目前正在編輯的分類ID
|
||||
currentEditingCategory: null,
|
||||
// 目前正在編輯的分類輸入框文字
|
||||
currentEditingCategoryText: '',
|
||||
// 目前正在新增分類
|
||||
addingNewCategory: false,
|
||||
// 目前正在拖曳的媒體分類ID
|
||||
currentDraggingCategoryId: null,
|
||||
// 目前拖曳媒體分類的指示線
|
||||
categoryDraggingIndicator: null,
|
||||
// 目前拖曳媒體分類過的媒體分類
|
||||
currentDraggingOverCategoryId: null,
|
||||
// 分類更新成功訊息
|
||||
categorySuccessMessage: '',
|
||||
// 分類更新失敗訊息
|
||||
categoryErrorMessage: '',
|
||||
// 圖片更新成功訊息
|
||||
imageSuccessMessage: '',
|
||||
// 圖片更新失敗訊息
|
||||
imageErrorMessage: '',
|
||||
// 視窗高度
|
||||
windowHeight: null,
|
||||
// 視窗寬度
|
||||
windowWidth: null,
|
||||
// 手機板下用,只顯示分類
|
||||
onlyShowCategory: false,
|
||||
// 手機板下用,只顯示媒體資訊
|
||||
onlyShowMediaInfo: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
component() {
|
||||
return this.$children[0];
|
||||
}
|
||||
},
|
||||
// Vue instance建立完後呼叫
|
||||
created() {
|
||||
this.$on('resize', (height, width) => {
|
||||
this.d.windowHeight = height;
|
||||
this.d.windowWidth = width;
|
||||
});
|
||||
// 預設先載入媒體資料
|
||||
this.fetchMedia(35);
|
||||
// 監聽fetch_medias事件,載入媒體資料
|
||||
this.$on('fetch_medias', (limit = null) => {
|
||||
this.fetchMedia(limit);
|
||||
});
|
||||
// 媒體庫子組件觸發的事件,更新拖曳的媒體的分類
|
||||
this.$on('update_category_for_medias', (mediaCategoryId, mediaIds, isUpdatingDraggingMedias = true) => {
|
||||
// ajax更新媒體的分類
|
||||
updateCategoryForMedia(mediaIds, mediaCategoryId)
|
||||
.then(response => {
|
||||
let responseBody = response.data;
|
||||
// 被移動的媒體數量
|
||||
let mediasMovedCount = responseBody.mediaIds.length;
|
||||
// 如果不是在all的分類下
|
||||
if(this.d.currentCategory != 'all' && responseBody.categoryId != this.d.currentCategory) {
|
||||
// 將media從現有顯示的列表移除
|
||||
responseBody.mediaIds.forEach((id, i) => {
|
||||
this.component.removeMediaFromList(id);
|
||||
});
|
||||
} else {
|
||||
responseBody.mediaIds.forEach((id, i) => {
|
||||
this.component.updateCategoryForMedia(id, responseBody.categoryId);
|
||||
});
|
||||
}
|
||||
for(let categoryId in responseBody.categorySources) {
|
||||
// 從原本分類的數量減去
|
||||
this.component.substractCategoryCount(categoryId, responseBody.categorySources[categoryId], false);
|
||||
}
|
||||
// 加上數量至目標分類
|
||||
this.component.addCategoryCount(responseBody.categoryId, mediasMovedCount, false);
|
||||
// 顯示訊息
|
||||
if(isUpdatingDraggingMedias) {
|
||||
this.showImageMessage(responseBody);
|
||||
} else {
|
||||
this.showInfoMessage(responseBody);
|
||||
}
|
||||
}).catch(error => {
|
||||
// 顯示訊息
|
||||
if(isUpdatingDraggingMedias) {
|
||||
this.showImageErrorMessage(error);
|
||||
} else {
|
||||
this.showInfoErrorMessage(error)
|
||||
}
|
||||
});
|
||||
})
|
||||
// 媒體庫子組件觸發的click事件,更新媒體的描述
|
||||
this.$on('update_media_description', (e, media) => {
|
||||
// disable按鈕
|
||||
e.target.disabled = true;
|
||||
// ajax更新媒體描述
|
||||
updateMediaDescription(media.id, media.description)
|
||||
.then(response => {
|
||||
this.showInfoMessage(response.data);
|
||||
})
|
||||
.catch(error => {
|
||||
this.showInfoErrorMessage(error);
|
||||
})
|
||||
.finally(() => {
|
||||
// endable按鈕
|
||||
e.target.disabled = false;
|
||||
})
|
||||
});
|
||||
// 媒體庫子組件觸發的click事件,刪除媒體
|
||||
this.$on('delete_media', (e, media) => {
|
||||
if(confirm(this.d.text.deleteConfirmation)) {
|
||||
// disable按鈕
|
||||
e.target.disabled = true;
|
||||
// ajax刪除媒體
|
||||
deleteMedia(media.id)
|
||||
.then(response => {
|
||||
this.showInfoMessage(response.data);
|
||||
// 將媒體從列表移除
|
||||
this.component.removeMediaFromList(response.data.id);
|
||||
this.component.substractCategoryCount(response.data.categoryId, 1);
|
||||
})
|
||||
.catch(error => {
|
||||
this.showInfoErrorMessage(error);
|
||||
})
|
||||
.finally(() => {
|
||||
// endable按鈕
|
||||
e.target.disabled = false;
|
||||
})
|
||||
}
|
||||
});
|
||||
// ajax載入媒體分類
|
||||
this.d.categoryFetching = true;
|
||||
fetchMediaCategories()
|
||||
.then(response => {
|
||||
this.d.categories = response.data.mediaCategories;
|
||||
})
|
||||
.finally(() => {
|
||||
this.d.categoryFetching = false;
|
||||
});
|
||||
// ajax更新媒體分類名稱
|
||||
this.$on('update_category_name', (categoryId, name) => {
|
||||
if(name) {
|
||||
updateMediaCategory(categoryId, name)
|
||||
.then(response => {
|
||||
this.showCategoryMessage(response.data);
|
||||
// 更新列表分類名稱
|
||||
this.component.updateListCategoryName(response.data.id, response.data.name);
|
||||
this.component.cancelCategoryEditing();
|
||||
})
|
||||
.catch(error => {
|
||||
this.showCategoryErrorMessage(error);
|
||||
})
|
||||
}
|
||||
})
|
||||
// ajax新增媒體分類
|
||||
this.$on('add_category', name => {
|
||||
if(name) {
|
||||
addMediaCategory(name)
|
||||
.then(response => {
|
||||
this.showCategoryMessage(response.data);
|
||||
this.component.addCategoryToList(response.data.mediaCategory.id, response.data.mediaCategory.name);
|
||||
this.component.cancelCategoryEditing();
|
||||
this.component.onCancelCategoryAdding();
|
||||
})
|
||||
.catch(error => {
|
||||
this.showCategoryErrorMessage(error);
|
||||
})
|
||||
}
|
||||
})
|
||||
// ajax刪除媒體分類
|
||||
this.$on('delete_category', id => {
|
||||
deleteMediaCategory(id)
|
||||
.then(response => {
|
||||
this.showCategoryMessage(response.data);
|
||||
this.component.addCategoryCount('uncategorized', response.data.count, false);
|
||||
this.component.deleteCategoryFromList(response.data.id);
|
||||
})
|
||||
.catch(error => {
|
||||
this.showCategoryErrorMessage(error)
|
||||
})
|
||||
});
|
||||
// ajax更新媒體分類順序
|
||||
this.$on('update_category_order', ids => {
|
||||
updateMediaCategoriesOrder(ids)
|
||||
.catch(error => {
|
||||
this.showCategoryErrorMessage(error);
|
||||
})
|
||||
})
|
||||
},
|
||||
components: {
|
||||
mediaLibrary
|
||||
},
|
||||
methods: {
|
||||
// 載入媒體資料
|
||||
fetchMedia(limit = null) {
|
||||
// 載入動作的lock,避免重複呼叫
|
||||
this.d.fetchingLock = true;
|
||||
// ajax載入媒體資料
|
||||
fetchMedia(this.d.currentCategory, this.d.lastFetchedMediaId, limit)
|
||||
.then(response => {
|
||||
let medias = response.data.medias;
|
||||
// 加入至原有列表
|
||||
this.d.medias = this.d.medias.concat(medias);
|
||||
if(medias.length) {
|
||||
// 紀錄最後一筆媒體的id,供下次使用
|
||||
this.d.lastFetchedMediaId = medias[medias.length - 1].id;
|
||||
} else {
|
||||
// 已載入至最後的資料
|
||||
this.d.fetchingEnd = true;
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
// 釋放載入動作的lock
|
||||
this.d.fetchingLock = false;
|
||||
});
|
||||
},
|
||||
// 根據response body顯示訊息
|
||||
showInfoMessage(responseBody, success = true) {
|
||||
if(responseBody.hasOwnProperty('message')) {
|
||||
this.component.clearInfoMessages();
|
||||
if(success) {
|
||||
this.component.showInfoSuccessMessage(responseBody.message);
|
||||
} else {
|
||||
this.component.showInfoErrorMessage(responseBody.message);
|
||||
}
|
||||
}
|
||||
},
|
||||
// 根據error顯示訊息,只顯示http error
|
||||
showInfoErrorMessage(error) {
|
||||
if(error.hasOwnProperty('response')) {
|
||||
this.showInfoMessage(error.response.data, false);
|
||||
}
|
||||
},
|
||||
// 根據response body顯示分類操作訊息
|
||||
showCategoryMessage(responseBody, success = true) {
|
||||
if(responseBody.hasOwnProperty('message')) {
|
||||
this.component.clearCategoryMessages();
|
||||
if(success) {
|
||||
this.component.showCategorySuccessMessage(responseBody.message);
|
||||
} else {
|
||||
this.component.showCategoryErrorMessage(responseBody.message);
|
||||
}
|
||||
}
|
||||
},
|
||||
// 根據error顯示分類操作訊息,只顯示http error
|
||||
showCategoryErrorMessage(error) {
|
||||
if(error.hasOwnProperty('response')) {
|
||||
this.showCategoryMessage(error.response.data, false);
|
||||
}
|
||||
},
|
||||
// 根據response body顯示圖片操作訊息
|
||||
showImageMessage(responseBody, success = true) {
|
||||
if(responseBody.hasOwnProperty('message')) {
|
||||
this.component.clearImageMessages();
|
||||
if(success) {
|
||||
this.component.showImageSuccessMessage(responseBody.message);
|
||||
} else {
|
||||
this.component.showImageErrorMessage(responseBody.message);
|
||||
}
|
||||
}
|
||||
},
|
||||
// 根據error顯示圖片操作訊息,只顯示http error
|
||||
showImageErrorMessage(error) {
|
||||
if(error.hasOwnProperty('response')) {
|
||||
this.showImageMessage(error.response.data, false);
|
||||
}
|
||||
},
|
||||
|
||||
}
|
||||
});
|
||||
return vue;
|
||||
}
|
||||
20
resources/lang/en/mediaLibrary.php
Normal file
20
resources/lang/en/mediaLibrary.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
return [
|
||||
'tabCategory' => 'Category',
|
||||
'tabUpload' => 'Upload',
|
||||
'tabBrowse' => 'Browse',
|
||||
'dragOrClickToUpload' => 'Drop files here or click to upload.',
|
||||
'url' => 'Url',
|
||||
'fileSize' => 'File Size',
|
||||
'date' => 'Date',
|
||||
'size' => 'Size',
|
||||
'description' => 'Description',
|
||||
'mimeType' => 'MIME type',
|
||||
'select' => 'Select',
|
||||
'update' => 'Update',
|
||||
'delete' => 'Delete',
|
||||
'deleteConfirmation' => 'Are you sure to delete this?',
|
||||
'category' => 'Category',
|
||||
'all' => 'All',
|
||||
'uncategorized' => 'Uncategorized',
|
||||
];
|
||||
@ -1,4 +1,19 @@
|
||||
<?php
|
||||
return [
|
||||
'categoryNameHasBeenAdded' => 'Category :name has been added.',
|
||||
'categoryNameHasBeenUpdated' => 'Category :name has been updated.',
|
||||
'categoryNameHasBeenDeleted' => 'Category :name has been deleted.',
|
||||
'descriptionHasBeenUpdated' => 'The description has been updated.',
|
||||
'failToUpdateDescription' => 'Fail to update description.',
|
||||
'failToUpdateCategoryName' => 'Fail to update category name.',
|
||||
'failToAddCategory' => 'Fail to add category.',
|
||||
'failToDeleteMedia' => 'Fail to delete media.',
|
||||
'failToDeleteCategory' => 'Fail to delete category.',
|
||||
'failToMoveMediaToCategory' => 'Fail to move media to category.',
|
||||
'failToReOrderCategory' => 'Fail to reorder category.',
|
||||
'givenCategoryIdDoesNotExist' => 'Given category id does not exist.',
|
||||
'mediaHasBeenDeleted' => 'The media has been deleted.',
|
||||
'mediaHasBeenMoveToCategory' => 'Media has been move to category :name.',
|
||||
'mediaHasBeenSetToUncategorized' => 'Media has been set to uncategorized.',
|
||||
'success' => 'Success'
|
||||
];
|
||||
|
||||
20
resources/lang/zh-tw/mediaLibrary.php
Normal file
20
resources/lang/zh-tw/mediaLibrary.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
return [
|
||||
'category' => '分類',
|
||||
'date' => '日期',
|
||||
'delete' => '刪除',
|
||||
'deleteConfirmation' => '確定要刪除嗎?',
|
||||
'description' => '描述',
|
||||
'dragOrClickToUpload' => '拖曳檔案至此或點擊上傳',
|
||||
'fileSize' => '檔案大小',
|
||||
'mimeType' => '檔案類型',
|
||||
'select' => '選擇',
|
||||
'size' => '尺寸',
|
||||
'tabBrowse' => '瀏覽',
|
||||
'tabCategory' => '分類',
|
||||
'tabUpload' => '上傳',
|
||||
'update' => '更新',
|
||||
'url' => '網址',
|
||||
'all' => '全部',
|
||||
'uncategorized' => '未分類',
|
||||
];
|
||||
1
resources/sass/admin/app.scss
vendored
1
resources/sass/admin/app.scss
vendored
@ -3,6 +3,7 @@
|
||||
//元件
|
||||
@import "components/datatables";
|
||||
@import "components/blockui";
|
||||
@import "../components/media-library";
|
||||
|
||||
|
||||
|
||||
|
||||
770
resources/sass/components/_media-library.scss
vendored
Normal file
770
resources/sass/components/_media-library.scss
vendored
Normal file
@ -0,0 +1,770 @@
|
||||
.blockUI.media-library {
|
||||
z-index: 1099 !important;
|
||||
width: 98% !important;
|
||||
transform: translate(-50%, -50%);
|
||||
top: 50% !important;
|
||||
left: 50% !important;
|
||||
cursor: default !important;
|
||||
}
|
||||
|
||||
#media-library {
|
||||
[draggable="true"] {
|
||||
user-select: none;
|
||||
}
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
.container-fluid {
|
||||
padding-top: 15px;
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
.close-window {
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 8px;
|
||||
}
|
||||
ul.tabs {
|
||||
.nav-link {
|
||||
&.active {
|
||||
color: white;
|
||||
background: #343a40;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.image-message {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.tab-page {
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
border: 1px solid #eee;
|
||||
&.browse {
|
||||
&.dragging {
|
||||
opacity: .5;
|
||||
}
|
||||
.img-wrapper {
|
||||
width: calc(14.285% - 40px);
|
||||
margin: 20px;
|
||||
float: left;
|
||||
@include media-breakpoint-down(lg) {
|
||||
width: calc(20% - 30px);
|
||||
margin: 15px;
|
||||
}
|
||||
@include media-breakpoint-down(md) {
|
||||
width: calc(25% - 30px);
|
||||
}
|
||||
@include media-breakpoint-down(sm) {
|
||||
width: calc(33.3% - 20px);
|
||||
margin: 10px;
|
||||
}
|
||||
@include media-breakpoint-down(xs) {
|
||||
width: calc(50% - 30px);
|
||||
margin: 15px;
|
||||
}
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 0 1px 2px lightgrey;
|
||||
|
||||
&.selected {
|
||||
box-shadow: 0px 0px 0 2px white, 0px 0px 0px 8px black;
|
||||
}
|
||||
.img {
|
||||
width: 100%;
|
||||
padding-top: 100%;
|
||||
background-size: cover;
|
||||
background-position: center center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.col-categories {
|
||||
.controls {
|
||||
text-align: left;
|
||||
input {
|
||||
width: auto;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
.category-list-wrapper {
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
.message {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 75%;
|
||||
text-align: left;
|
||||
.alert {
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
}
|
||||
.category-list {
|
||||
flex-wrap: nowrap;
|
||||
&.dragging {
|
||||
.category {
|
||||
opacity: .5;
|
||||
}
|
||||
}
|
||||
.dragging-indicator {
|
||||
width: 100%;
|
||||
border-top: 2px dotted;
|
||||
}
|
||||
.category {
|
||||
padding: 4px;
|
||||
margin: 1px 0;
|
||||
text-align: left;
|
||||
color: black;
|
||||
&.on-drag {
|
||||
border: 2px solid black;
|
||||
}
|
||||
&.current {
|
||||
background: gray;
|
||||
color: white;
|
||||
&.editing {
|
||||
background: none;
|
||||
border: 1px solid gray;
|
||||
}
|
||||
}
|
||||
.btn {
|
||||
&.edit-category,
|
||||
&.remove-category {
|
||||
display: none;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
.edit-category {
|
||||
|
||||
}
|
||||
&:hover {
|
||||
.btn {
|
||||
&.edit-category,
|
||||
&.remove-category {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.col-media-info {
|
||||
.message {
|
||||
.alert {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.media-info-wrapper {
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
text-align: left;
|
||||
.info {
|
||||
&.hidden {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
section {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.img {
|
||||
margin-top: 5px;
|
||||
img {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.url {
|
||||
a {
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
.description {
|
||||
.content {
|
||||
textarea {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
.select-medias {
|
||||
height: 40px;
|
||||
margin-top: 5px;
|
||||
text-align: right;
|
||||
}
|
||||
.upload-zone {
|
||||
position: relative;
|
||||
min-height: 100%;
|
||||
padding: 20px 20px;
|
||||
width: 100%;
|
||||
border: 1px dotted #aaa;
|
||||
border-radius: 2px;
|
||||
box-sizing: border-box;
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.dz-preview {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 120px;
|
||||
margin: 0.5em;
|
||||
}
|
||||
|
||||
.dz-preview .dz-progress {
|
||||
display: block;
|
||||
height: 15px;
|
||||
border: 1px solid #aaa;
|
||||
}
|
||||
|
||||
.dz-preview .dz-progress .dz-upload {
|
||||
display: block;
|
||||
height: 100%;
|
||||
width: 0;
|
||||
background: green;
|
||||
}
|
||||
|
||||
.dz-preview .dz-error-message {
|
||||
color: red;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dz-preview.dz-error .dz-error-message, .dz-preview.dz-error .dz-error-mark {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dz-preview.dz-success .dz-success-mark {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dz-preview .dz-error-mark, .dz-preview .dz-success-mark {
|
||||
position: absolute;
|
||||
display: none;
|
||||
left: 30px;
|
||||
top: 30px;
|
||||
width: 54px;
|
||||
height: 58px;
|
||||
left: 50%;
|
||||
margin-left: -27px;
|
||||
}
|
||||
|
||||
/*
|
||||
* The MIT License
|
||||
* Copyright (c) 2012 Matias Meno <m@tias.me>
|
||||
*/
|
||||
@-webkit-keyframes passing-through {
|
||||
0% {
|
||||
opacity: 0;
|
||||
-webkit-transform: translateY(40px);
|
||||
-moz-transform: translateY(40px);
|
||||
-ms-transform: translateY(40px);
|
||||
-o-transform: translateY(40px);
|
||||
transform: translateY(40px);
|
||||
}
|
||||
30%, 70% {
|
||||
opacity: 1;
|
||||
-webkit-transform: translateY(0px);
|
||||
-moz-transform: translateY(0px);
|
||||
-ms-transform: translateY(0px);
|
||||
-o-transform: translateY(0px);
|
||||
transform: translateY(0px);
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
-webkit-transform: translateY(-40px);
|
||||
-moz-transform: translateY(-40px);
|
||||
-ms-transform: translateY(-40px);
|
||||
-o-transform: translateY(-40px);
|
||||
transform: translateY(-40px);
|
||||
}
|
||||
}
|
||||
|
||||
@-moz-keyframes passing-through {
|
||||
0% {
|
||||
opacity: 0;
|
||||
-webkit-transform: translateY(40px);
|
||||
-moz-transform: translateY(40px);
|
||||
-ms-transform: translateY(40px);
|
||||
-o-transform: translateY(40px);
|
||||
transform: translateY(40px);
|
||||
}
|
||||
30%, 70% {
|
||||
opacity: 1;
|
||||
-webkit-transform: translateY(0px);
|
||||
-moz-transform: translateY(0px);
|
||||
-ms-transform: translateY(0px);
|
||||
-o-transform: translateY(0px);
|
||||
transform: translateY(0px);
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
-webkit-transform: translateY(-40px);
|
||||
-moz-transform: translateY(-40px);
|
||||
-ms-transform: translateY(-40px);
|
||||
-o-transform: translateY(-40px);
|
||||
transform: translateY(-40px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes passing-through {
|
||||
0% {
|
||||
opacity: 0;
|
||||
-webkit-transform: translateY(40px);
|
||||
-moz-transform: translateY(40px);
|
||||
-ms-transform: translateY(40px);
|
||||
-o-transform: translateY(40px);
|
||||
transform: translateY(40px);
|
||||
}
|
||||
30%, 70% {
|
||||
opacity: 1;
|
||||
-webkit-transform: translateY(0px);
|
||||
-moz-transform: translateY(0px);
|
||||
-ms-transform: translateY(0px);
|
||||
-o-transform: translateY(0px);
|
||||
transform: translateY(0px);
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
-webkit-transform: translateY(-40px);
|
||||
-moz-transform: translateY(-40px);
|
||||
-ms-transform: translateY(-40px);
|
||||
-o-transform: translateY(-40px);
|
||||
transform: translateY(-40px);
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes slide-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
-webkit-transform: translateY(40px);
|
||||
-moz-transform: translateY(40px);
|
||||
-ms-transform: translateY(40px);
|
||||
-o-transform: translateY(40px);
|
||||
transform: translateY(40px);
|
||||
}
|
||||
30% {
|
||||
opacity: 1;
|
||||
-webkit-transform: translateY(0px);
|
||||
-moz-transform: translateY(0px);
|
||||
-ms-transform: translateY(0px);
|
||||
-o-transform: translateY(0px);
|
||||
transform: translateY(0px);
|
||||
}
|
||||
}
|
||||
|
||||
@-moz-keyframes slide-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
-webkit-transform: translateY(40px);
|
||||
-moz-transform: translateY(40px);
|
||||
-ms-transform: translateY(40px);
|
||||
-o-transform: translateY(40px);
|
||||
transform: translateY(40px);
|
||||
}
|
||||
30% {
|
||||
opacity: 1;
|
||||
-webkit-transform: translateY(0px);
|
||||
-moz-transform: translateY(0px);
|
||||
-ms-transform: translateY(0px);
|
||||
-o-transform: translateY(0px);
|
||||
transform: translateY(0px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slide-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
-webkit-transform: translateY(40px);
|
||||
-moz-transform: translateY(40px);
|
||||
-ms-transform: translateY(40px);
|
||||
-o-transform: translateY(40px);
|
||||
transform: translateY(40px);
|
||||
}
|
||||
30% {
|
||||
opacity: 1;
|
||||
-webkit-transform: translateY(0px);
|
||||
-moz-transform: translateY(0px);
|
||||
-ms-transform: translateY(0px);
|
||||
-o-transform: translateY(0px);
|
||||
transform: translateY(0px);
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes pulse {
|
||||
0% {
|
||||
-webkit-transform: scale(1);
|
||||
-moz-transform: scale(1);
|
||||
-ms-transform: scale(1);
|
||||
-o-transform: scale(1);
|
||||
transform: scale(1);
|
||||
}
|
||||
10% {
|
||||
-webkit-transform: scale(1.1);
|
||||
-moz-transform: scale(1.1);
|
||||
-ms-transform: scale(1.1);
|
||||
-o-transform: scale(1.1);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
20% {
|
||||
-webkit-transform: scale(1);
|
||||
-moz-transform: scale(1);
|
||||
-ms-transform: scale(1);
|
||||
-o-transform: scale(1);
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@-moz-keyframes pulse {
|
||||
0% {
|
||||
-webkit-transform: scale(1);
|
||||
-moz-transform: scale(1);
|
||||
-ms-transform: scale(1);
|
||||
-o-transform: scale(1);
|
||||
transform: scale(1);
|
||||
}
|
||||
10% {
|
||||
-webkit-transform: scale(1.1);
|
||||
-moz-transform: scale(1.1);
|
||||
-ms-transform: scale(1.1);
|
||||
-o-transform: scale(1.1);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
20% {
|
||||
-webkit-transform: scale(1);
|
||||
-moz-transform: scale(1);
|
||||
-ms-transform: scale(1);
|
||||
-o-transform: scale(1);
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
-webkit-transform: scale(1);
|
||||
-moz-transform: scale(1);
|
||||
-ms-transform: scale(1);
|
||||
-o-transform: scale(1);
|
||||
transform: scale(1);
|
||||
}
|
||||
10% {
|
||||
-webkit-transform: scale(1.1);
|
||||
-moz-transform: scale(1.1);
|
||||
-ms-transform: scale(1.1);
|
||||
-o-transform: scale(1.1);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
20% {
|
||||
-webkit-transform: scale(1);
|
||||
-moz-transform: scale(1);
|
||||
-ms-transform: scale(1);
|
||||
-o-transform: scale(1);
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.dz-clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dz-clickable * {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.dz-clickable .dz-message, .dz-clickable .dz-message * {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dz-started .dz-message {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dz-drag-hover {
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
.dz-drag-hover .dz-message {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.dz-message {
|
||||
text-align: center;
|
||||
margin: 2em 0;
|
||||
}
|
||||
|
||||
.dz-preview {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin: 16px;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.dz-preview:hover {
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.dz-preview:hover .dz-details {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.dz-preview.dz-file-preview .dz-image {
|
||||
border-radius: 20px;
|
||||
background: #999;
|
||||
background: linear-gradient(to bottom, #eee, #ddd);
|
||||
}
|
||||
|
||||
.dz-preview.dz-file-preview .dz-details {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.dz-preview.dz-image-preview {
|
||||
background: white;
|
||||
}
|
||||
|
||||
.dz-preview.dz-image-preview .dz-details {
|
||||
-webkit-transition: opacity 0.2s linear;
|
||||
-moz-transition: opacity 0.2s linear;
|
||||
-ms-transition: opacity 0.2s linear;
|
||||
-o-transition: opacity 0.2s linear;
|
||||
transition: opacity 0.2s linear;
|
||||
}
|
||||
|
||||
.dz-preview .dz-remove {
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.dz-preview .dz-remove:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.dz-preview:hover .dz-details {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.dz-preview .dz-details {
|
||||
z-index: 20;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
font-size: 13px;
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
padding: 2em 1em;
|
||||
text-align: center;
|
||||
color: rgba(0, 0, 0, 0.9);
|
||||
line-height: 150%;
|
||||
}
|
||||
|
||||
.dz-preview .dz-details .dz-size {
|
||||
margin-bottom: 1em;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.dz-preview .dz-details .dz-filename {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dz-preview .dz-details .dz-filename:hover span {
|
||||
border: 1px solid rgba(200, 200, 200, 0.8);
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.dz-preview .dz-details .dz-filename:not(:hover) {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.dz-preview .dz-details .dz-filename:not(:hover) span {
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.dz-preview .dz-details .dz-filename span, .dz-preview .dz-details .dz-size span {
|
||||
background-color: rgba(255, 255, 255, 0.4);
|
||||
padding: 0 0.4em;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.dz-preview:hover .dz-image img {
|
||||
-webkit-transform: scale(1.05, 1.05);
|
||||
-moz-transform: scale(1.05, 1.05);
|
||||
-ms-transform: scale(1.05, 1.05);
|
||||
-o-transform: scale(1.05, 1.05);
|
||||
transform: scale(1.05, 1.05);
|
||||
-webkit-filter: blur(8px);
|
||||
filter: blur(8px);
|
||||
}
|
||||
|
||||
.dz-preview .dz-image {
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
position: relative;
|
||||
display: block;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.dz-preview .dz-image img {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dz-preview.dz-success .dz-success-mark {
|
||||
-webkit-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1);
|
||||
-moz-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1);
|
||||
-ms-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1);
|
||||
-o-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1);
|
||||
animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1);
|
||||
}
|
||||
|
||||
.dz-preview.dz-error .dz-error-mark {
|
||||
opacity: 1;
|
||||
-webkit-animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1);
|
||||
-moz-animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1);
|
||||
-ms-animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1);
|
||||
-o-animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1);
|
||||
animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1);
|
||||
}
|
||||
|
||||
.dz-preview .dz-success-mark, .dz-preview .dz-error-mark {
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
z-index: 500;
|
||||
position: absolute;
|
||||
display: block;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin-left: -27px;
|
||||
margin-top: -27px;
|
||||
}
|
||||
|
||||
.dz-preview .dz-success-mark svg, .dz-preview .dz-error-mark svg {
|
||||
display: block;
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
}
|
||||
|
||||
.dz-preview.dz-processing .dz-progress {
|
||||
opacity: 1;
|
||||
-webkit-transition: all 0.2s linear;
|
||||
-moz-transition: all 0.2s linear;
|
||||
-ms-transition: all 0.2s linear;
|
||||
-o-transition: all 0.2s linear;
|
||||
transition: all 0.2s linear;
|
||||
}
|
||||
|
||||
.dz-preview.dz-complete .dz-progress {
|
||||
opacity: 0;
|
||||
-webkit-transition: opacity 0.4s ease-in;
|
||||
-moz-transition: opacity 0.4s ease-in;
|
||||
-ms-transition: opacity 0.4s ease-in;
|
||||
-o-transition: opacity 0.4s ease-in;
|
||||
transition: opacity 0.4s ease-in;
|
||||
}
|
||||
|
||||
.dz-preview:not(.dz-processing) .dz-progress {
|
||||
-webkit-animation: pulse 6s ease infinite;
|
||||
-moz-animation: pulse 6s ease infinite;
|
||||
-ms-animation: pulse 6s ease infinite;
|
||||
-o-animation: pulse 6s ease infinite;
|
||||
animation: pulse 6s ease infinite;
|
||||
}
|
||||
|
||||
.dz-preview .dz-progress {
|
||||
opacity: 1;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
height: 16px;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
margin-top: -8px;
|
||||
width: 80px;
|
||||
margin-left: -40px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
-webkit-transform: scale(1);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dz-preview .dz-progress .dz-upload {
|
||||
background: #333;
|
||||
background: linear-gradient(to bottom, #666, #444);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 0;
|
||||
-webkit-transition: width 300ms ease-in-out;
|
||||
-moz-transition: width 300ms ease-in-out;
|
||||
-ms-transition: width 300ms ease-in-out;
|
||||
-o-transition: width 300ms ease-in-out;
|
||||
transition: width 300ms ease-in-out;
|
||||
}
|
||||
|
||||
.dz-preview.dz-error .dz-error-message {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dz-preview.dz-error:hover .dz-error-message {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.dz-preview .dz-error-message {
|
||||
pointer-events: none;
|
||||
z-index: 1000;
|
||||
position: absolute;
|
||||
display: block;
|
||||
display: none;
|
||||
opacity: 0;
|
||||
-webkit-transition: opacity 0.3s ease;
|
||||
-moz-transition: opacity 0.3s ease;
|
||||
-ms-transition: opacity 0.3s ease;
|
||||
-o-transition: opacity 0.3s ease;
|
||||
transition: opacity 0.3s ease;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
top: 130px;
|
||||
left: -10px;
|
||||
width: 140px;
|
||||
background: #be2626;
|
||||
background: linear-gradient(to bottom, #be2626, #a92222);
|
||||
padding: 0.5em 1.2em;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.dz-preview .dz-error-message:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -6px;
|
||||
left: 64px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 6px solid transparent;
|
||||
border-right: 6px solid transparent;
|
||||
border-bottom: 6px solid #be2626;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -34,6 +34,9 @@
|
||||
<li class="nav-item px-3">
|
||||
<a href="{{ route('index') }}" class="nav-link">Front Stage</a>
|
||||
</li>
|
||||
<li class="nav-item px-3">
|
||||
<a href="#" class="nav-link medialibrary">Media Library</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="nav navbar-nav ml-auto">
|
||||
@component('components.languageDropdown')@endcomponent
|
||||
|
||||
@ -30,3 +30,36 @@ Route::group(['prefix' => config('admin.route'), 'middleware' => ['admin.area'],
|
||||
Route::get('/', 'AdminPageController@index')->name('index');
|
||||
});
|
||||
|
||||
/**
|
||||
* 媒體庫
|
||||
*/
|
||||
Route::group(['prefix' => 'media', 'as' => 'media.'], function(){
|
||||
Route::post('/', 'MediaLibraryController@fileUpload')
|
||||
->name('upload');
|
||||
Route::get('/', 'MediaLibraryController@getMedias')
|
||||
->name('get');
|
||||
Route::match(['put', 'patch'], '/{id}', 'MediaLibraryController@updateMedia')
|
||||
->where('id', '[0-9]+')
|
||||
->name('update');
|
||||
Route::delete('/{id}', 'MediaLibraryController@deleteMedia')
|
||||
->name('destroy');
|
||||
Route::match(['put', 'patch'], '/update-category', 'MediaLibraryController@updateCategory')
|
||||
->name('updateCategory');
|
||||
});
|
||||
|
||||
/**
|
||||
* 媒體分類
|
||||
*/
|
||||
Route::group(['prefix' => 'media-category', 'as' => 'media-category.'], function(){
|
||||
Route::match(['put', 'patch'], '/{id}', 'MediaCategoryController@update')
|
||||
->where('id', '[0-9]+')
|
||||
->name('update');
|
||||
Route::match(['put', 'patch'], '/update-order', 'MediaCategoryController@updateOrder')
|
||||
->name('updateOrder');
|
||||
Route::get('/', 'MediaCategoryController@index')
|
||||
->name('index');
|
||||
Route::post('/', 'MediaCategoryController@store')
|
||||
->name('store');
|
||||
Route::delete('/{id}', 'MediaCategoryController@destroy')
|
||||
->name('destroy');
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user