Compare commits

...

10 Commits

26 changed files with 1025 additions and 36 deletions

View File

@ -70,6 +70,7 @@ class Components extends BaseRenderer {
*/
public function imageControl($name, $label){
return amis()->ImageControl($name, $label)
->joinValues(false)
->autoUpload(true)->maxSize('5*1024*1024');//不能大于5M
}
@ -77,10 +78,37 @@ class Components extends BaseRenderer {
* 图片上传,带裁剪
*/
public function cropImageControl($name, $label, $aspectRatio = null){
return amis()->ImageControl($name, $label)
->crop([
'aspectRatio' => $aspectRatio ?? 1
])->autoUpload(true);
$cropImage = amis()->ImageControl($name, $label)->joinValues(false);
// if($aspectRatio){
$cropImage->crop([
'aspectRatio' => $aspectRatio ?? 1
]);
// }
$cropImage->autoUpload(true);
return $cropImage;
}
/**
* 普通文件上传
*/
public function fileControl($name, $label, $accept = '.txt', $multiple = false){
return amis()->FileControl($name, $label ?? __('admin.components.files'))
->multiple($multiple)
->joinValues(false)
->useChunk(false)
->maxSize(20*1024*1024)
->accept($accept);
}
public function chunkFileControl($name, $label, $accept = '*', $multiple = false){
return amis()->FileControl($name, $label ?? __('admin.components.files'))
->multiple($multiple)
->joinValues(false)
->useChunk(true)
->accept($accept)
->startChunkApi(admin_url('start_chunk_upload_file'))
->chunkApi(admin_url('save_chunk_upload_file'))
->finishChunkApi(admin_url('finish_chunk_upload_file'));
}
/**

View File

@ -0,0 +1,97 @@
<?php
namespace App\Admin\Controllers;
use Slowlyo\OwlAdmin\Admin;
use Slowlyo\OwlAdmin\Renderers\Page;
use Slowlyo\OwlAdmin\Renderers\Form;
use Slowlyo\OwlAdmin\Controllers\AdminController;
use Slowlyo\OwlAdmin\Renderers\Operation;
use App\Services\Admin\AdService;
use App\Admin\Components;
use App\Models\Keyword;
use App\Models\Ad;
/**
* 广告管理
*
* @property AdService $service
*/
class AdController extends AdminController
{
protected string $serviceName = AdService::class;
public function list(): Page
{
$crud = $this->baseCRUD()->tableLayout('fixed')
->headerToolbar([
$this->createButton(),
...$this->baseHeaderToolBar(),
])
->filter($this->baseFilter()->labelWidth('80px')->body([
amis()->GroupControl()->mode('horizontal')->body([
amis()->TextControl('id', __('admin.ads.id'))
->placeholder(__('admin.id')),
amis()->TextControl('remark', __('admin.ads.remark'))
->placeholder(__('admin.ads.remark')),
Components::make()->parentControl(admin_url('api/keywords/tree-list?parent_name=banner_address&has_owner=0'), 'address', __('admin.ads.address'), 'name', 'key'),
amis()->SelectControl('enable', __('admin.ads.is_enable'))
->options([
1=>'开启',0=>'关闭'
]),
]),
amis()->GroupControl()->mode('horizontal')->body([
amis()->InputDatetimeRange()->label(__('admin.ads.published_at'))->name('published_at'),
amis()->InputDatetimeRange()->label(__('admin.created_at'))->name('created_at'),
]),
]))
->columns([
amis()->TableColumn('id', __('admin.ads.id'))->width('50px')->sortable(),
amis()->TableColumn('address', __('admin.ads.address'))->type('mapping')
->map(Keyword::allChildrenOfKey('banner_address')->pluck('name', 'key')->toArray())
->itemSchema(amis()->Tag()->label('${item}')->color(Admin::setting()->get('system_theme_setting')['theme_color'] ?? '#1677ff')),
amis()->TableColumn('sort', __('admin.ads.sort'))->quickEdit(
amis()->NumberControl()->min(0)->saveImmediately(true)
),
amis()->TableColumn('resource', __('admin.ads.resource'))->type('image')->height('50px')->width('150px')->enlargeAble(true),
amis()->TableColumn('remark', __('admin.ads.remark')),
amis()->TableColumn('published_at', __('admin.ads.published_at'))->remark(__('admin.ads.published_at_remark')),
amis()->TableColumn('is_enable', __('admin.ads.is_enable'))->type('switch'),
amis()->TableColumn('created_at', __('admin.created_at'))->type('datetime')->sortable(true),
Operation::make()->label(__('admin.actions'))->buttons([
$this->rowEditButton(),
$this->rowDeleteButton(),
])
]);
return $this->baseList($crud);
}
public function form($isEdit = false): Form
{
return $this->baseForm()->body([
Components::make()->parentControl(admin_url('api/keywords/tree-list?parent_name=banner_address&has_owner=0'), 'address', __('admin.ads.address'), 'name', 'key')->required(true),
Components::make()->imageControl('resource', __('admin.ads.resource'))->required(true),
amis()->TextControl('remark', __('admin.ads.remark')),
Components::make()->sortControl('sort', __('admin.ads.sort')),
amis()->DateTimeControl('published_at', __('admin.ads.published_at'))->format('YYYY-MM-DD HH:mm:ss')->description(__('admin.ads.published_at_remark')),
amis()->SwitchControl('is_enable', __('admin.ads.is_enable'))->value(false),
amis()->RadiosControl('jump_type', __('admin.ads.jump_type'))->selectFirst(true)->options(Ad::jumpTypeMap())->required(true),
amis()->TextControl('jump_config.web_link', __('admin.ads.jump_config_arr.web_link'))->visibleOn('this.jump_type == '.Ad::TYPE_WEB)->required(true),
amis()->TextControl('jump_config.app_link', __('admin.ads.jump_config_arr.app_link'))->visibleOn('this.jump_type == '.Ad::TYPE_APP)->required(true),
amis()->GroupControl()->body([
amis()->TextControl('jump_config.mini_id', __('admin.ads.jump_config_arr.mini_id'))->visibleOn('this.jump_type == '.Ad::TYPE_MINI)->required(true),
amis()->TextControl('jump_config.mini_link', __('admin.ads.jump_config_arr.mini_link'))->visibleOn('this.jump_type == '.Ad::TYPE_MINI)->required(true),
]),
]);
}
public function detail(): Form
{
return $this->baseDetail()->body([
amis()->TextControl('id', 'ID')->static(),
amis()->TextControl('created_at', __('admin.created_at'))->static(),
amis()->TextControl('updated_at', __('admin.updated_at'))->static()
]);
}
}

View File

@ -22,8 +22,33 @@ class ArticleController extends AdminController
$this->createButton(),
...$this->baseHeaderToolBar(),
])
->filter($this->baseFilter()->body())
->itemBadge([
->filter($this->baseFilter()->body([
amis()->GroupControl()->mode('horizontal')->body([
amis()->TextControl('id', __('admin.articles.id'))
->placeholder(__('admin.id')),
amis()->TextControl('title', __('admin.articles.title'))
->placeholder(__('admin.articles.title')),
Components::make()->parentControl(admin_url('api/keywords/tree-list?parent_name=article_category&has_owner=0'), 'category', __('admin.articles.category'), 'name', 'key'),
Components::make()->keywordsTagControl('t_ids', __('admin.articles.tags'), 'article_tag'),
]),
amis()->GroupControl()->mode('horizontal')->body([
amis()->SelectControl('enable', __('admin.articles.is_enable'))
->columnRatio(3)
->options([
1=>'开启',0=>'关闭'
]),
amis()->SelectControl('recommend', __('admin.articles.is_recommend'))
->columnRatio(3)
->options([
1=>'开启',0=>'关闭'
]),
amis()->InputDatetimeRange()->label(__('admin.articles.published_at'))->name('published_at'),
]),
amis()->GroupControl()->mode('horizontal')->body([
amis()->InputDatetimeRange()->label(__('admin.created_at'))->name('created_at')->columnRatio(6),
]),
]))
->itemBadge([//行角标
'text' => __('admin.articles.is_recommend'),
'mode' => 'ribbon',
'position' => 'top-left',
@ -32,17 +57,17 @@ class ArticleController extends AdminController
'size' => 15
])
->columns([
amis()->TableColumn('id', __('admin.id')),
amis()->TableColumn('id', __('admin.id'))->sortable(true),
amis()->TableColumn('title', __('admin.articles.title'))->width('300px'),
amis()->TableColumn('category', __('admin.articles.category'))->type('mapping')
->map(Keyword::allChildrenOfKey('article_category')->pluck('name', 'key')->toArray())
->itemSchema(amis()->Tag()->label('${item}')->color(Admin::setting()->get('system_theme_setting')['theme_color'] ?? '#1677ff')),
amis()->TableColumn('tags', __('admin.articles.tags'))->type('mapping')->map(Keyword::tagsMap('article_tag')),
amis()->TableColumn('cover', __('admin.articles.cover'))->type('image')->height('50px')->width('50px')->enlargeAble(true),
amis()->TableColumn('published_at', __('admin.articles.published_at'))->remark('若无发布时间操作显示,则会自动生成当前发布时间'),
amis()->TableColumn('published_at', __('admin.articles.published_at'))->remark(__('admin.articles.published_at_remark')),
amis()->TableColumn('is_enable', __('admin.articles.is_enable'))->type('switch'),
amis()->TableColumn('is_recommend', __('admin.articles.is_recommend'))->type('switch'),
amis()->TableColumn('created_at', __('admin.created_at')),
amis()->TableColumn('created_at', __('admin.created_at'))->type('datetime')->sortable(true),
amis()->Operation()->label(__('admin.actions'))->buttons([
$this->rowEditButton(),
$this->rowDeleteButton(),
@ -62,9 +87,10 @@ class ArticleController extends AdminController
Components::make()->keywordsTagControl('t_ids', __('admin.articles.tags'), 'article_tag'),
Components::make()->cropImageControl('cover', __('admin.articles.cover')),
Components::make()->sortControl('sort', __('admin.articles.sort')),
amis()->DateTimeControl('published_at', __('admin.articles.published_at'))->format('YYYY-MM-DD HH:mm:ss')->description('*若创建时设置显示且未填写,则默认为创建时间'),
amis()->DateTimeControl('published_at', __('admin.articles.published_at'))->format('YYYY-MM-DD HH:mm:ss')->description(__('admin.articles.published_at_remark')),
amis()->SwitchControl('is_enable', __('admin.articles.is_enable'))->value(false),
amis()->SwitchControl('is_recommend', __('admin.articles.is_recommend'))->value(false),
Components::make()->fileControl('appendixes', __('admin.articles.appendixes'), '.xsl,.xlsx,.txt,.doc,.docx,.pdf,.pptx'),
])->md(4),
amis()->Wrapper()->body([
Components::make()->fuEditorControl('content', __('admin.articles.content')),

View File

@ -0,0 +1,12 @@
<?php
namespace App\Admin\Controllers;
use Slowlyo\OwlAdmin\Controllers\AdminController;
use App\Traits\UploadTrait;
class IndexController extends AdminController
{
use UploadTrait;
}

View File

@ -2,6 +2,7 @@
namespace App\Admin\Controllers;
use Illuminate\Support\Arr;
use Illuminate\Http\Request;
use Slowlyo\OwlAdmin\Renderers\Tab;
use Slowlyo\OwlAdmin\Renderers\Tabs;
@ -15,7 +16,6 @@ class SettingController extends AdminController
public function index()
{
$page = $this->basePage()->body([
Alert::make()->showIcon(true)->body("此处内容仅供演示, 设置项无实际意义,实际开发中请根据实际情况进行修改。"),
$this->form(),
]);
@ -25,19 +25,23 @@ class SettingController extends AdminController
public function form()
{
return $this->baseForm(false)
->redirect('')
->redirect('')->labelWidth('200px')
->api($this->getStorePath())
->data(settings()->all())
->body(
Tabs::make()->tabs([
Tab::make()->title('基本设置')->body([
TextControl::make()->label('网站名称')->name('site_name'),
InputKV::make()->label('附加配置')->name('addition_config'),
]),
Tab::make()->title('上传设置')->body([
TextControl::make()->label('上传域名')->name('upload_domain'),
TextControl::make()->label('上传路径')->name('upload_path'),
]),
amis()->RadiosControl('upload_disk', '上传驱动')->options([
'public'=>'本地存储',
'oss' => '阿里云OSS',
])->value('public')->required(true),
amis()->TextControl('oss_config.access_key_id', '阿里云AccessKeyId')->required(true)->size('lg')->visibleOn('${upload_disk == "oss"}'),
amis()->TextControl('oss_config.access_key_secret', '阿里云AccessKeySecret')->required(true)->size('lg')->visibleOn('${upload_disk == "oss"}'),
amis()->TextControl('oss_config.bucket', '对象存储Bucket')->required(true)->size('lg')->visibleOn('${upload_disk == "oss"}')->remark('示例: my-bucket'),
amis()->TextControl('oss_config.endpoint', '对象存储endpoint')->required(true)->size('lg')->visibleOn('${upload_disk == "oss"}')->remark('示例: oss-cn-shanghai.aliyuncs.com'),
amis()->TextControl('oss_config.domain', '自有域名')->size('lg')->visibleOn('${upload_disk == "oss"}')->remark('填写即启用 示例: my-domain.com'),
amis()->SwitchControl('oss_config.use_ssl', '开启SSL')->value(false)->visibleOn('${upload_disk == "oss"}'),
]),
])
);
}
@ -45,12 +49,43 @@ class SettingController extends AdminController
public function store(Request $request)
{
$data = $request->only([
'site_name',
'addition_config',
'upload_domain',
'upload_path',
'upload_disk',
'oss_config'
]);
//上传设置-修改env文件内配置
if(!empty($data['upload_disk'])){
$envData['FILESYSTEM_DISK'] = $data['upload_disk'];
if($envData['FILESYSTEM_DISK'] == 'oss'){//如果设置为OSS驱动则配置env变量
$envData['OSS_ACCESS_KEY_ID'] = Arr::get($data['oss_config'], 'access_key_id');
$envData['OSS_ACCESS_KEY_SECRET'] = Arr::get($data['oss_config'], 'access_key_secret');
$envData['OSS_BUCKET'] = Arr::get($data['oss_config'], 'bucket');
$envData['OSS_ENDPOINT'] = Arr::get($data['oss_config'], 'endpoint');
$envData['OSS_DOMAIN'] = Arr::get($data['oss_config'], 'domain');
if(!empty($envData['OSS_DOMAIN'])){
$envData['OSS_CNAME'] = 'true';
}else{
$envData['OSS_CNAME'] = 'false';
}
$envData['OSS_SSL'] = Arr::get($data['oss_config'], 'use_ssl') ? 'true':'false';
}
$envPath = base_path() . DIRECTORY_SEPARATOR . '.env';
$contentArray = collect(file($envPath, FILE_IGNORE_NEW_LINES));
$contentArray->transform(function ($item) use ($envData){
foreach ($envData as $key => $value){
if(str_contains($item, $key)){
return $key . '=' . $value;
}
}
return $item;
});
$content = implode("\n", $contentArray->toArray());
\File::put($envPath, $content);
}
return settings()->adminSetMany($data);
}
}

View File

@ -29,4 +29,15 @@ Route::group([
$router->resource('system/keywords', \App\Admin\Controllers\KeywordController::class);
$router->resource('articles', \App\Admin\Controllers\ArticleController::class);
$router->resource('ads', \App\Admin\Controllers\AdController::class);
//修改上传
$router->post('upload_file', [\App\Admin\Controllers\IndexController::class, 'uploadFile']);
$router->post('upload_image', [\App\Admin\Controllers\IndexController::class, 'uploadImage']);
$router->post('upload_rich', [\App\Admin\Controllers\IndexController::class, 'uploadRich']);
$router->post('start_chunk_upload_file', [\App\Admin\Controllers\IndexController::class, 'startChunk']);
$router->post('save_chunk_upload_file', [\App\Admin\Controllers\IndexController::class, 'saveChunk']);
$router->post('finish_chunk_upload_file', [\App\Admin\Controllers\IndexController::class, 'finishChunk']);
});

85
app/Models/Ad.php 100644
View File

@ -0,0 +1,85 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use EloquentFilter\Filterable;
use App\Casts\Storage;
class Ad extends Model
{
use HasFactory;
use Filterable;
public const TYPE_OFF = 0; //无跳转
public const TYPE_WEB = 1; //网页跳转
public const TYPE_APP = 2; //应用跳转
public const TYPE_MINI = 3;//小程序跳转
protected function serializeDate(\DateTimeInterface $date)
{
return $date->format('Y-m-d H:i:s');
}
protected $casts = [
'created_at' => 'datetime',
'updated_at' => 'datetime',
'published_at' => 'datetime',
'is_enable' => 'boolean',
'jump_config' => 'array',
];
protected $fillable = [
'resource',
'address',
'remark',
'published_at',
'is_enable',
'sort',
'jump_type',
'jump_config',
];
public static function jumpTypeMap() :array
{
return [
self::TYPE_WEB => '网页跳转',
self::TYPE_APP => '应用跳转',
self::TYPE_MINI => '小程序跳转',
self::TYPE_OFF => '无跳转',
];
}
public static function jumpTypeMapLabel()
{
return [
self::TYPE_OFF => "<span class='label'>无跳转</span>",
self::TYPE_WEB => "<span class='label label-info'>网页跳转</span>",
self::TYPE_APP => "<span class='label label-warning'>应用跳转</span>",
self::TYPE_MINI => "<span class='label label-green'>小程序跳转</span>",
'*'=>'其他:${jump_type}'
];
}
public static function typeMapLabel()
{
return [
self::TYPE_IN => "<span class='label label-info'>入住缴费</span>",
self::TYPE_CONTINUE => "<span class='label label-warning'>续住缴费</span>",
self::TYPE_EXIT => "<span class='label label-danger'>离开结算</span>",
'*'=>'其他:${live_in}'
];
}
public function scopeShow(){
$q->where('is_enable', true)->where('published_at', '>=', now());
}
public function scopeSort($q)
{
$q->orderBy('sort', 'asc')
->orderBy('published_at', 'desc')
->orderBy('created_at', 'desc');
}
}

View File

@ -26,7 +26,7 @@ class Article extends Model
'published_at' => 'datetime:Y-m-d H:i:s',
'is_enable' => 'boolean',
'is_recommend' => 'boolean',
'cover' => Storage::class,
'appendixes' => 'array',
];
protected $fillable = [
@ -42,6 +42,10 @@ class Article extends Model
'appendixes',
];
public function scopeShow(){
$q->where('is_enable', true)->where('published_at', '>=', now());
}
public function scopeSort($q)
{
$q->orderBy('is_recommend', 'desc')

View File

@ -0,0 +1,63 @@
<?php
namespace App\Models\Filters;
use Illuminate\Support\Arr;
use EloquentFilter\ModelFilter;
class AdFilter extends ModelFilter
{
/**
* 主键
*/
public function id($id)
{
return $this->where('id', $id);
}
/**
* 备注
*/
public function remark($remark)
{
return $this->where('remark','like', '%'.$remark.'%');
}
/**
* 位置
*/
public function address($address)
{
return $this->where('address', $address);
}
public function publishedAt($publishedAt){
$publishedAt = explode(',',$publishedAt);
return $this->where(function($q) use ($publishedAt) {
$startAt = Arr::get($publishedAt, 0) ?? null;
$endAt = Arr::get($publishedAt, 1) ?? null;
if(!empty($startAt)){
$q->where('published_at', '>=', $startAt);
}
if(!empty($endAt)){
$q->where('published_at', '<=', $endAt);
}
});
}
public function createdAt($createdAt){
$createdAt = explode(',',$createdAt);
return $this->where(function($q) use ($createdAt) {
$startAt = Arr::get($createdAt, 0) ?? null;
$endAt = Arr::get($createdAt, 1) ?? null;
if(!empty($startAt)){
$q->where('created_at', '>=', $startAt);
}
if(!empty($endAt)){
$q->where('created_at', '<=', $endAt);
}
});
}
public function enable($enable){
return $this->where('is_enable', $enable);
}
}

View File

@ -3,9 +3,17 @@
namespace App\Models\Filters;
use EloquentFilter\ModelFilter;
use Illuminate\Support\Arr;
class ArticleFilter extends ModelFilter
{
/**
* 主键
*/
public function id($id)
{
return $this->where('id', $id);
}
/**
* 标题
*/
@ -13,4 +21,60 @@ class ArticleFilter extends ModelFilter
{
return $this->where('title','like', '%'.$title.'%');
}
/**
* 获取分类下的文章
*/
public function category($category)
{
return $this->where('category', $category);
}
/**
* 查询带有标签的文章
*/
public function tIds($tIds){
$tIds = explode(',',$tIds);
return $this->where(function($q) use ($tIds) {
foreach ($tIds as $tId) {
$q->whereRaw("FIND_IN_SET('".$tId."',t_ids)");
}
});
}
public function publishedAt($publishedAt){
$publishedAt = explode(',',$publishedAt);
return $this->where(function($q) use ($publishedAt) {
$startAt = Arr::get($publishedAt, 0) ?? null;
$endAt = Arr::get($publishedAt, 1) ?? null;
if(!empty($startAt)){
$q->where('published_at', '>=', $startAt);
}
if(!empty($endAt)){
$q->where('published_at', '<=', $endAt);
}
});
}
public function createdAt($createdAt){
$createdAt = explode(',',$createdAt);
return $this->where(function($q) use ($createdAt) {
$startAt = Arr::get($createdAt, 0) ?? null;
$endAt = Arr::get($createdAt, 1) ?? null;
if(!empty($startAt)){
$q->where('created_at', '>=', $startAt);
}
if(!empty($endAt)){
$q->where('created_at', '<=', $endAt);
}
});
}
public function enable($enable){
return $this->where('is_enable', $enable);
}
public function recommend($recommend){
return $this->where('is_recommend', $recommend);
}
}

View File

@ -23,7 +23,7 @@ class Keyword extends Model
{
parent::boot();
// 监听 Keyword 的创建事件,用于初始化 path 和 lv 字段值
static::creating(function ($keyword) {
static::saving(function ($keyword) {
// 如果创建的是一个根类目
if (! $keyword->parent_id) {
// 将层级设为 1

View File

@ -0,0 +1,114 @@
<?php
namespace App\Services\Admin;
use App\Models\Ad;
use App\Models\Filters\AdFilter;
use App\Traits\UploadTrait;
use Illuminate\Support\Arr;
/**
* @method Ad getModel()
* @method Ad|\Illuminate\Database\Query\Builder query()
*/
class AdService extends BaseService
{
use UploadTrait;
protected string $modelName = Ad::class;
protected string $modelFilterName = AdFilter::class;
protected bool $modelSortAble = true;
public function store($data): bool
{
$columns = $this->getTableColumns();
$model = $this->getModel();
$isEnable = Arr::get($data, 'is_enabled');
$publishedAt = Arr::get($data, 'published_at');
if ($isEnable && empty($publishedAt)) {
$data['published_at'] = now();
}
if(isset($data['resource'])){
$data['resource'] = $this->saveImage('resource', 'ads/resource')[0];
}
//处理跳转配置
$jumpType = Arr::get($data, 'jump_type');
if($jumpType !== null){
switch($jumpType){
case Ad::TYPE_OFF:
$data['jump_config'] = null;
break;
case Ad::TYPE_WEB:
$data['jump_config'] = Arr::only($data['jump_config'], 'web_link');
break;
case Ad::TYPE_APP:
$data['jump_config'] = Arr::only($data['jump_config'], 'app_link');;
break;
case Ad::TYPE_MINI:
$data['jump_config'] = Arr::only($data['jump_config'], ['mini_id', 'mini_link']);;
break;
}
}
foreach ($data as $k => $v) {
if (!in_array($k, $columns)) {
continue;
}
$model->setAttribute($k, $v);
}
return $model->save();
}
public function update($primaryKey, $data): bool
{
$columns = $this->getTableColumns();
$model = $this->query()->whereKey($primaryKey)->first();
$isEnable = Arr::get($data, 'is_enable');
$publishedAt = Arr::get($data, 'published_at');
if ($isEnable && empty($publishedAt) && empty($model->published_at)) {
$data['published_at'] = now();
}
if(isset($data['resource'])){
$data['resource'] = $this->saveImage('resource', 'ads/resource')[0];
}
//处理跳转配置
$jumpType = Arr::get($data, 'jump_type');
if($jumpType !== null){
switch($jumpType){
case Ad::TYPE_OFF:
$data['jump_config'] = null;
break;
case Ad::TYPE_WEB:
$data['jump_config'] = Arr::only($data['jump_config'], 'web_link');
break;
case Ad::TYPE_APP:
$data['jump_config'] = Arr::only($data['jump_config'], 'app_link');;
break;
case Ad::TYPE_MINI:
$data['jump_config'] = Arr::only($data['jump_config'], ['mini_id', 'mini_link']);;
break;
}
}
foreach ($data as $k => $v) {
if (!in_array($k, $columns)) {
continue;
}
$model->setAttribute($k, $v);
}
return $model->save();
}
}

View File

@ -128,6 +128,8 @@ class AdminUserService extends BaseService
$query->where('username', 'like', "%{$keyword}%")->orWhere('name', 'like', "%{$keyword}%");
});
$this->sortable($query);
$items = (clone $query)->paginate(request()->input('perPage', 20))->items();
$total = (clone $query)->count();

View File

@ -4,6 +4,7 @@ namespace App\Services\Admin;
use App\Models\Article;
use App\Models\Filters\ArticleFilter;
use App\Traits\UploadTrait;
use Illuminate\Support\Arr;
/**
@ -12,10 +13,14 @@ use Illuminate\Support\Arr;
*/
class ArticleService extends BaseService
{
use UploadTrait;
protected string $modelName = Article::class;
protected string $modelFilterName = ArticleFilter::class;
protected bool $modelSortAble = true;
public function store($data): bool
{
$columns = $this->getTableColumns();
@ -27,6 +32,9 @@ class ArticleService extends BaseService
$data['published_at'] = now();
}
$data['cover'] = $this->saveImage('cover', 'articles/cover')[0] ?? '';
$data['appendixes'] = $this->saveFile('appendixes', 'articles/appendixes');
foreach ($data as $k => $v) {
if (!in_array($k, $columns)) {
continue;
@ -50,6 +58,14 @@ class ArticleService extends BaseService
$data['published_at'] = now();
}
if(isset($data['cover'])){
$data['cover'] = $this->saveImage('cover', 'articles/cover')[0] ?? '';
}
if(isset($data['appendixes'])){
$data['appendixes'] = $this->saveFile('appendixes', 'articles/appendixes');
}
foreach ($data as $k => $v) {
if (!in_array($k, $columns)) {
continue;

View File

@ -14,6 +14,8 @@ class BaseService extends AdminService
protected string $modelFilterName = '';
protected bool $modelSortAble = false;
public function getTree()
{
$list = $this->query()->orderByDesc('sort')->get();
@ -40,7 +42,13 @@ class BaseService extends AdminService
$query->filter(request()->input(), $filter);
}
return $query->orderByDesc($model->getUpdatedAtColumn() ?? $model->getKeyName());
if($this->modelSortAble){
$query->sort();
}
$this->sortable($query);
return $query;
}
public function getDetail($id)

View File

@ -19,7 +19,7 @@ class KeywordService extends BaseService
{
$list = $this->query()->filter(request()->all(), $this->modelFilterName)->orderByDesc('sort')->get();
$minNum = $list->min('parent_id');
return array2tree($list->toArray(), $minNum);
return !$list->isEmpty() ? array2tree($list->toArray(), $minNum) :[];
}
public function parentIsChild($id, $pid): bool

View File

@ -0,0 +1,210 @@
<?php
namespace App\Traits;
use Slowlyo\OwlAdmin\Admin;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Storage;
trait UploadTrait
{
public function uploadImage()
{
return $this->upload('image');
}
public function uploadFile()
{
return $this->upload();
}
/**
* 富文本内文件上传
*/
public function uploadRich()
{
$fromWangEditor = false;
$file = request()->file('file');
if (!$file) {
$fromWangEditor = true;
$file = request()->file('wangeditor-uploaded-image');
if (!$file) {
$file = request()->file('wangeditor-uploaded-video');
}
}
if (!$file) {
return $this->response()->additional(['errno' => 1])->fail(__('admin.upload_file_error'));
}
$path = $file->store(Admin::config('admin.upload.directory.rich'), Admin::config('admin.upload.disk'));
$link = Storage::disk(Admin::config('admin.upload.disk'))->url($path);
if ($fromWangEditor) {
return $this->response()->additional(['errno' => 0])->success(['url' => $link]);
}
return $this->response()->additional(compact('link'))->success(compact('link'));
}
/**
* 存放临时文件目录,按日期区分;
*/
protected function upload($type = 'file')
{
$file = request()->file('file');
if (!$file) {
return $this->response()->fail(__('admin.upload_file_error'));
}
$path = $file->store(Admin::config('admin.upload.tem_directory.' . $type).'/'.date('Y-m-d'), Admin::config('admin.upload.disk'));
return $this->response()->success(['value' => $path]);
}
public function saveImage($field = 'image', $path)
{
return $this->saveFile($field, $path);
}
/**
* 表单提交时,转存实际目录,并保留上传时文件名称;文件保存全路径
*/
public function saveFile($field = 'file', $path)
{
$file = request()->file($field);
if (!$file) {
$file = request()->get($field);
}
$fileArr = [];
//判断是否多个文件;
if(is_string($file) || isset($file['id'])){
$files = [$file];
}else{
$files = $file;
}
if($files){
foreach($files as $file){
if(is_array($file) && isset($file['state'])){
switch($file['state']){
case 'init':
if(strpos($file['value'], 'temporary') !== false){
$filePath = $path.'/'.$file['name'];
Storage::disk(Admin::config('admin.upload.disk'))->move($file['value'], $filePath);
$fileArr[] = Storage::disk(Admin::config('admin.upload.disk'))->url($filePath);
}else{
$fileArr[] = $file['value'];
}
break;
case 'uploaded':
if(isset($file['name'])){
$filePath = $path.'/'.$file['name'];
if(Str::startsWith($file['value'], ['http://', 'https://'])){
$fileUrl = parse_url($file['value']);
$fileValue = ltrim($fileUrl['path'], '/');
}else{
$fileValue = $file['value'];
}
Storage::disk(Admin::config('admin.upload.disk'))->move($file['value'], $filePath);
$fileArr[] = Storage::disk(Admin::config('admin.upload.disk'))->url($filePath);
}
break;
}
}else{
$fileArr[] = $file;
}
}
}
return $fileArr;
}
/**
* 开始分片上传, 生成唯一ID
*/
public function startChunk()
{
$type = 'file';
$fileName = request()->get('filename', '');
$uploadId = md5(time().$fileName);
//创建临时文件夹
if ( Storage::disk(Admin::config('admin.upload.disk'))->exists(Admin::config('admin.upload.tem_directory.' . $type).'/'.$uploadId) === false ) {
if ( Storage::disk(Admin::config('admin.upload.disk'))->makeDirectory(Admin::config('admin.upload.tem_directory.' . $type).'/'.$uploadId) === false ) {
return false;
}
}
return $this->response()->success(['uploadId' => $uploadId]);
}
/**
* 保存分片
*/
public function saveChunk()
{
$type = 'file';
$file = request()->file('file');
$uploadId = request()->get('uploadId');
$partNumber = request()->get('partNumber');
$partSize = request()->get('partSize');
// dd($file);
$fileName = $file->getClientOriginalName();
//判断该分片是否已存在,
$dirPath = Admin::config('admin.upload.tem_directory.' . $type).'/'.$uploadId;
if(Storage::disk(Admin::config('admin.upload.disk'))->exists($dirPath . '/'.$fileName.'_'.$partNumber)){
return $this->response()->fail(__('admin.upload_file_error'));
}else{
//验证分片大小-todo
$path = $file->storeAs($dirPath, $fileName.'_'.$partNumber, Admin::config('admin.upload.disk'));
$realPath = Storage::disk(Admin::config('admin.upload.disk'))->url($path);
$eTag = md5_file($realPath);
return $this->response()->success(['eTag' => $eTag]);
}
}
/**
* 完成分片
*/
public function finishChunk()
{
$type = 'file';
//合并文件
$fileName = request()->get('filename', '');
$uploadId = request()->get('uploadId', '');
$partList = request()->get('partList', []);
$basePath = Admin::config('admin.upload.tem_directory.' . $type).'/'.$uploadId;
$realPath = 'chunk/'.$fileName;
//获取分片列表中序号,查看分片是否都完成上传
$partNumberList = Arr::pluck($partList, 'partNumber');
if(max($partNumberList) === count($partNumberList)){
//判断是否已存在同名文件,进行删除
if(Storage::disk(Admin::config('admin.upload.disk'))->exists($realPath)){
$realPath = 'chunk/(1)'.$fileName;
}
for($i = 1; $i<=count($partNumberList); $i++){
$_file = Storage::disk(Admin::config('admin.upload.disk'))->get($basePath.'/'.$fileName.'_'.$i);
Storage::disk(Admin::config('admin.upload.disk'))->append($realPath, $_file, null);
}
//删除分片文件夹
Storage::disk(Admin::config('admin.upload.disk'))->deleteDirectory($basePath);
$value = Storage::disk(Admin::config('admin.upload.disk'))->url($realPath);
return $this->response()->success(['value'=>$value]);
}else{
return $this->response()->fail(__('admin.upload_file_error'));
}
}
}

View File

@ -6,6 +6,7 @@
"license": "MIT",
"require": {
"php": "^8.1",
"alphasnow/aliyun-oss-laravel": "^4.7",
"guzzlehttp/guzzle": "^7.2",
"laravel/framework": "^10.10",
"laravel/sanctum": "^3.3",

137
composer.lock generated
View File

@ -4,8 +4,143 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "ec2cedc62465f8a6d60ba0b0f78581dd",
"content-hash": "b2333ae7a977ba073ff6ef0f01f5de43",
"packages": [
{
"name": "aliyuncs/oss-sdk-php",
"version": "v2.6.0",
"dist": {
"type": "zip",
"url": "https://mirrors.cloud.tencent.com/repository/composer/aliyuncs/oss-sdk-php/v2.6.0/aliyuncs-oss-sdk-php-v2.6.0.zip",
"reference": "572d0f8e099e8630ae7139ed3fdedb926c7a760f",
"shasum": ""
},
"require": {
"php": ">=5.3"
},
"require-dev": {
"phpunit/phpunit": "*",
"satooshi/php-coveralls": "*"
},
"type": "library",
"autoload": {
"psr-4": {
"OSS\\": "src/OSS"
}
},
"license": [
"MIT"
],
"authors": [
{
"name": "Aliyuncs",
"homepage": "http://www.aliyun.com"
}
],
"description": "Aliyun OSS SDK for PHP",
"homepage": "http://www.aliyun.com/product/oss/",
"time": "2022-08-03T08:06:01+00:00"
},
{
"name": "alphasnow/aliyun-oss-flysystem",
"version": "3.3.3",
"dist": {
"type": "zip",
"url": "https://mirrors.cloud.tencent.com/repository/composer/alphasnow/aliyun-oss-flysystem/3.3.3/alphasnow-aliyun-oss-flysystem-3.3.3.zip",
"reference": "ac62f862cb743ca76f7c3c146ffdd8d52af86153",
"shasum": ""
},
"require": {
"aliyuncs/oss-sdk-php": "^2.5",
"league/flysystem": "^3.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.6",
"mockery/mockery": "^1.5",
"php-coveralls/php-coveralls": "*",
"phpstan/phpstan": "^1.4",
"phpunit/phpunit": "^9.5",
"vlucas/phpdotenv": "^5.4"
},
"type": "library",
"autoload": {
"psr-4": {
"AlphaSnow\\Flysystem\\Aliyun\\": "src/"
}
},
"license": [
"MIT"
],
"authors": [
{
"name": "Alpha Snow",
"email": "wind91@foxmail.com"
}
],
"description": "Flysystem adapter for the Aliyun storage",
"homepage": "https://alphasnow.github.io/aliyun-oss-flysystem/",
"keywords": [
"adapter",
"aliyun",
"filesystem",
"oss"
],
"time": "2023-06-29T08:45:24+00:00"
},
{
"name": "alphasnow/aliyun-oss-laravel",
"version": "4.7.1",
"dist": {
"type": "zip",
"url": "https://mirrors.cloud.tencent.com/repository/composer/alphasnow/aliyun-oss-laravel/4.7.1/alphasnow-aliyun-oss-laravel-4.7.1.zip",
"reference": "d4f5885bcff8c7a5c43be118e749a85fe5eb6a99",
"shasum": ""
},
"require": {
"alphasnow/aliyun-oss-flysystem": "^3.3",
"php": "^8.0.2"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.12",
"mockery/mockery": "^1.5",
"orchestra/testbench": "^7.13",
"php-coveralls/php-coveralls": "*",
"phpstan/phpstan": "^1.9",
"phpunit/phpunit": "^9.5"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"AlphaSnow\\LaravelFilesystem\\Aliyun\\AliyunServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"AlphaSnow\\LaravelFilesystem\\Aliyun\\": "src/"
}
},
"license": [
"MIT"
],
"authors": [
{
"name": "alphasnow",
"email": "wind91@foxmail.com"
}
],
"description": "alibaba cloud object storage service for laravel",
"homepage": "https://alphasnow.github.io/aliyun-oss-laravel/",
"keywords": [
"aliyun",
"filesystems",
"laravel",
"oss",
"storage"
],
"time": "2022-12-03T14:27:07+00:00"
},
{
"name": "brick/math",
"version": "0.11.0",

View File

@ -54,13 +54,19 @@ return [
],
'upload' => [
'disk' => 'public',
'disk' => env("FILESYSTEM_DISK", 'public'),
// 文件上传目录
'directory' => [
'image' => 'images',
'file' => 'files',
'rich' => 'rich',
],
// 临时目录
'tem_directory' => [
'image' => 'temporary/images',
'file' => 'temporary/file',
'rich' => 'temporary/rich',
]
],
'https' => env('ADMIN_HTTPS', false),

View File

@ -56,6 +56,22 @@ return [
'throw' => false,
],
"oss" => [
"driver" => "oss",
"access_key_id" => env("OSS_ACCESS_KEY_ID"), // 必填, 阿里云的AccessKeyId
"access_key_secret" => env("OSS_ACCESS_KEY_SECRET"), // 必填, 阿里云的AccessKeySecret
"bucket" => env("OSS_BUCKET"), // 必填, 对象存储的Bucket, 示例: my-bucket
"endpoint" => env("OSS_ENDPOINT"), // 必填, 对象存储的Endpoint, 示例: oss-cn-shanghai.aliyuncs.com
"internal" => env("OSS_INTERNAL", null), // 选填, 内网上传地址,填写即启用 示例: oss-cn-shanghai-internal.aliyuncs.com
"domain" => env("OSS_DOMAIN", null), // 选填, 绑定域名,填写即启用 示例: oss.my-domain.com
"is_cname" => env("OSS_CNAME", false), // 选填, 若Endpoint为自定义域名此项要为truehttps://github.com/aliyun/aliyun-oss-php-sdk/blob/572d0f8e099e8630ae7139ed3fdedb926c7a760f/src/OSS/OssClient.php#L113C1-L122C78
"prefix" => env("OSS_PREFIX", ""), // 选填, 统一存储地址前缀
"use_ssl" => env("OSS_SSL", false), // 选填, 是否使用HTTPS
"reverse_proxy" => env("OSS_REVERSE_PROXY", false), // 选填, 域名是否使用NGINX代理绑定
"throw" => env("OSS_THROW", true), // 选填, 是否抛出引起错误的异常,默认出现错误时,不抛出异常仅返回false
"options" => [], // 选填, 添加全局配置参数, 示例: [\OSS\OssClient::OSS_CHECK_MD5 => false]
"macros" => [] // 选填, 添加自定义Macro, 示例: [\App\Macros\ListBuckets::class, \App\Macros\CreateBucket::class]
],
],
/*

View File

@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('ads', function (Blueprint $table) {
$table->id();
$table->string('address')->comment('位置');
$table->string('resource')->comment('资源');
$table->string('remark')->nullable()->comment('备注');
$table->timestamp('published_at')->nullable()->comment('发布时间');
$table->unsignedTinyInteger('is_enable')->default(1)->comment('显示开关');
$table->unsignedInteger('sort')->default(0)->comment('排序');
$table->unsignedTinyInteger('jump_type')->default(0)->comment('跳转0不跳转。1网页跳转2应用跳转3微信小程序跳转');
$table->text('jump_config')->nullable()->comment('跳转配置');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('ads');
}
};

View File

@ -33,7 +33,7 @@ class AdminMenuSeeder extends Seeder
['title' => 'web_content', 'icon' => 'ic:outline-collections-bookmark', 'url' => '', 'order'=>3,
'children' =>[
['title'=>'articles', 'icon'=>'ic:outline-article','url'=>'/articles', 'order'=>1],
['title'=>'banners', 'icon'=>'lets-icons:img-box','url'=>'/banners', 'order'=>2],
['title'=>'ads', 'icon'=>'lets-icons:img-box','url'=>'/ads', 'order'=>2],
]
]
];

View File

@ -20,7 +20,7 @@ class KeywordSeeder extends Seeder
['key' => 'article_category', 'name' => '文章分类', 'list' => [
]],
['key' => 'article_tag', 'name' => '文章标签', 'list' => [
['key' => 'article_tag', 'name' => '文章标签', 'list' => [//标签value填写色号指定标签颜色
]],
['key' => 'banner_address', 'name' => '广告位置', 'list' => [

View File

@ -60,7 +60,7 @@ return [
'cancel' => '取消',
'please_login' => '请先登录',
'unauthorized' => '无权访问',
'id' => '主键',
'id' => 'ID',
'components' => [
'content' => '内容',
@ -72,7 +72,8 @@ return [
'enabled' => '已启用',
'disabled' => '已禁用',
],
'tag' => '标签'
'tag' => '标签',
'files' => '文件'
],
'code_generators' => [
@ -273,7 +274,7 @@ return [
'parent_keyword' => '父级关键字',
],
'articles' => [
'id' => '主键',
'id' => '主键ID',
'title' => '标题',
'content' => '内容',
'cover' =>'封面',
@ -285,6 +286,26 @@ return [
'is_enable' => '显示',
'is_recommend' => '推荐',
'sort' => '排序',
'appendixes' => '附件'
'appendixes' => '附件',
'published_at_remark' => '*若未设置发布时间且操作设置为显示,则默认生成发布时间',
],
'ads' => [
'id' => 'ID',
'address' => '位置',
'resource' =>'内容',
'published_at' => '定时发布',
'published_at_g' => '发布时间',
'is_enable' => '显示',
'remark' => '备注',
'sort' => '排序',
'published_at_remark' => '*若未设置发布时间且操作设置为显示,则默认生成发布时间',
'jump_type' => '跳转类型',
'jump_config'=>'跳转配置',
'jump_config_arr'=>[
'web_link' => '网页地址',
'app_link' => '应用路径',
'mini_id' => '小程序ID',
'mini_link'=> '小程序路径'
],
]
];

View File

@ -12,5 +12,5 @@ return [
'keywords' => '数据字典',
'web_content' => '内容管理',
'articles' => '文章管理',
'banners' => '广告管理',
'ads' => '广告管理',
];