diff --git a/app/Admin/Components.php b/app/Admin/Components.php new file mode 100644 index 0000000..da89c02 --- /dev/null +++ b/app/Admin/Components.php @@ -0,0 +1,18 @@ +TreeSelectControl() + ->name('parent_id')->label('父级') + ->showIcon(false) + ->labelField($labelField) + ->valueField($valueField) + ->value(0)->source($apiUrl); + } +} \ No newline at end of file diff --git a/app/Admin/Controllers/KeywordController.php b/app/Admin/Controllers/KeywordController.php index 4a7173e..f6e7748 100644 --- a/app/Admin/Controllers/KeywordController.php +++ b/app/Admin/Controllers/KeywordController.php @@ -8,6 +8,8 @@ use Slowlyo\OwlAdmin\Renderers\TableColumn; use Slowlyo\OwlAdmin\Renderers\TextControl; use Slowlyo\OwlAdmin\Controllers\AdminController; use App\Services\Admin\KeywordService; +use App\Admin\Components; +use Illuminate\Http\Request; class KeywordController extends AdminController { @@ -18,16 +20,24 @@ class KeywordController extends AdminController public function list(): Page { $crud = $this->baseCRUD() + //关闭查询 ->filterTogglable(false) + //去掉分页-start + ->loadDataOnce(true) + ->footerToolbar([]) + //去掉分页-end ->headerToolbar([ $this->createButton(true), - ...$this->baseHeaderToolBar(), + amis('reload')->align('right'), + amis('filter-toggler')->align('right'), ]) ->columns([ - TableColumn::make()->name('id')->label('ID')->sortable(true), + // TableColumn::make()->name('id')->label('ID')->sortable(true), + TableColumn::make()->name('name')->label('名称'), + TableColumn::make()->name('key')->label('KEY'), + TableColumn::make()->name('value')->label('值'), TableColumn::make()->name('created_at')->label('创建时间')->type('datetime')->sortable(true), - TableColumn::make()->name('updated_at')->label('更新时间')->type('datetime')->sortable(true), - $this->rowActions(true), + $this->rowActionsOnlyEditAndDelete(true), ]); return $this->baseList($crud); @@ -36,18 +46,15 @@ class KeywordController extends AdminController public function form(): Form { return $this->baseForm()->body([ - TextControl::make()->name('name')->label('名称')->required(), - - + Components::make()->parentControl(admin_url('api/keywords/tree-list')), + TextControl::make()->name('name')->label('名称')->required(true), + TextControl::make()->name('key')->label('KEY')->required(true), + TextControl::make()->name('value')->label('值')->required(true), + amisMake()->NumberControl()->name('sort')->value(0)->min()->label('排序'), ]); } - public function detail(): Form - { - return $this->baseDetail()->body([ - TextControl::make()->static(true)->name('id')->label('ID'), - TextControl::make()->static(true)->name('created_at')->label('创建时间'), - TextControl::make()->static(true)->name('updated_at')->label('更新时间') - ]); - } + public function getTreeList(Request $request){ + return $this->service->getTree(); + } } diff --git a/app/Admin/routes.php b/app/Admin/routes.php index c15becb..399c3e0 100644 --- a/app/Admin/routes.php +++ b/app/Admin/routes.php @@ -8,6 +8,11 @@ Route::group([ 'prefix' => config('admin.route.prefix'), 'middleware' => config('admin.route.middleware'), ], function (Router $router) { + $router->group([ + 'prefix' => 'api', + ], function (Router $router) { + $router->get('keywords/tree-list', '\App\Admin\Controllers\KeywordController@getTreeList')->name('api.keywords.tree-list'); + }); $router->resource('dashboard', \App\Admin\Controllers\HomeController::class); diff --git a/app/Models/Keyword.php b/app/Models/Keyword.php index d1d544f..8c53e54 100644 --- a/app/Models/Keyword.php +++ b/app/Models/Keyword.php @@ -4,8 +4,44 @@ namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use EloquentFilter\Filterable; class Keyword extends Model { use HasFactory; + use Filterable; + + protected $fillable = ['name', 'key', 'value', 'parent_id', 'type_key', 'path', 'sort', 'level']; + + protected static function boot() + { + parent::boot(); + // 监听 Keyword 的创建事件,用于初始化 path 和 level 字段值 + static::creating(function ($keyword) { + // 如果创建的是一个根类目 + if (! $keyword->parent_id) { + // 将层级设为 1 + $keyword->level = 1; + // 将 path 设为 - + $keyword->path = '-'; + } else { + // 将层级设为父类目的层级 + 1 + $keyword->level = $keyword->parent->level + 1; + $keyword->type_key = $keyword->parent->key; + // 将 path 值设为父类目的 path 追加父类目 ID 以及最后跟上一个 - 分隔符 + $keyword->path = $keyword->parent->path.$keyword->parent_id.'-'; + } + }); + } + + public function parent() + { + return $this->belongsTo(static::class, 'parent_id'); + } + + public function children() + { + return $this->hasMany(static::class, 'parent_id'); + } + } diff --git a/app/Providers/QueryLoggerServiceProvider.php b/app/Providers/QueryLoggerServiceProvider.php new file mode 100644 index 0000000..f7c1c0a --- /dev/null +++ b/app/Providers/QueryLoggerServiceProvider.php @@ -0,0 +1,67 @@ +app['events']->listen([ + QueryExecuted::class, + TransactionBeginning::class, + TransactionCommitted::class, + TransactionRolledBack::class, + ], function ($event) { + Log::debug(match (true) { + $event instanceof TransactionBeginning => 'begin transaction', + $event instanceof TransactionCommitted => 'commit transaction', + $event instanceof TransactionRolledBack => 'rollback transaction', + default => $this->prepareSql($event), + }); + }); + } + + /** + * @param \Illuminate\Database\Events\QueryExecuted $query + * @return string + */ + protected function prepareSql(QueryExecuted $query): string + { + $sql = str_replace(['%', '?'], ['%%', '%s'], $query->sql); + + $bindings = $query->connection->prepareBindings($query->bindings); + + if (count($bindings)) { + $sql = vsprintf($sql, array_map([$query->connection->getPdo(), 'quote'], $bindings)); + } + + return sprintf('[%s] %s', $this->formatDuration($query->time), $sql); + } + + /** + * @param float $milliseconds + * @return string + */ + protected function formatDuration($milliseconds): string + { + return match (true) { + $milliseconds >= 1000 => round($milliseconds / 1000, 2).'s', + $milliseconds < 0.01 => round($milliseconds * 1000).'μs', + default => $milliseconds.'ms', + }; + } +} diff --git a/app/Services/Admin/KeywordService.php b/app/Services/Admin/KeywordService.php index 40a1467..598aa51 100644 --- a/app/Services/Admin/KeywordService.php +++ b/app/Services/Admin/KeywordService.php @@ -2,6 +2,7 @@ namespace App\Services\Admin; +use Illuminate\Support\Arr; use App\Models\Keyword; use Slowlyo\OwlAdmin\Services\AdminService; @@ -12,4 +13,102 @@ use Slowlyo\OwlAdmin\Services\AdminService; class KeywordService extends AdminService { protected string $modelName = Keyword::class; + + public function getTree() + { + $list = $this->query()->orderByDesc('sort')->get()->toArray(); + return array2tree($list); + } + + public function parentIsChild($id, $parent_id): bool + { + $parent = $this->query()->find($parent_id); + + do { + if ($parent->parent_id == $id) { + return true; + } + // 如果没有parent 则为顶级 退出循环 + $parent = $parent->parent; + } while ($parent); + + return false; + } + + public function list() + { + return ['items' => $this->getTree()]; + } + + public function store($data): bool + { + if ($this->hasRepeated($data)) { + return false; + } + + $columns = $this->getTableColumns(); + + $model = $this->getModel(); + + foreach ($data as $k => $v) { + if (!in_array($k, $columns)) { + continue; + } + + $model->setAttribute($k, $v); + } + + return $model->save(); + } + + public function update($primaryKey, $data): bool + { + if ($this->hasRepeated($data, $primaryKey)) { + return false; + } + + $columns = $this->getTableColumns(); + + $parent_id = Arr::get($data, 'parent_id'); + if ($parent_id != 0) { + if ($this->parentIsChild($primaryKey, $parent_id)) { + $this->setError('父级不允许设置为当前子权限'); + return false; + } + } + + $model = $this->query()->whereKey($primaryKey)->first(); + + foreach ($data as $k => $v) { + if (!in_array($k, $columns)) { + continue; + } + + $model->setAttribute($k, $v); + } + + return $model->save(); + } + + public function hasRepeated($data, $id = 0): bool + { + $query = $this->query()->when($id, fn($query) => $query->where('id', '<>', $id)); + + if ((clone $query)->where('key', $data['key'])->exists()) { + $this->setError('KEY重复'); + return true; + } + + return false; + } + + public function delete(string $ids): mixed + { + $ids = explode(',', $ids); + if(count($ids) == 1){ + $this->query()->where('path', 'like', '%-'.$ids[0].'-%')->delete(); + } + + return $this->query()->whereIn('id', $ids)->delete(); + } } diff --git a/composer.json b/composer.json index ef7c477..30761a5 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,8 @@ "laravel/framework": "^9.19", "laravel/sanctum": "^3.0", "laravel/tinker": "^2.7", - "slowlyo/owl-admin": "^2.1" + "slowlyo/owl-admin": "^2.1", + "tucker-eric/eloquentfilter": "^3.2" }, "require-dev": { "fakerphp/faker": "^1.9.1", @@ -66,7 +67,7 @@ "repositories": { "packagist": { "type": "composer", - "url": "https://packagist.phpcomposer.com" + "url": "https://mirrors.aliyun.com/composer/" } } } diff --git a/composer.lock b/composer.lock index a0a92bd..2e968db 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "a5b72c385ebe487317edadb68fb376ab", + "content-hash": "0955c00d24fb997149c86a6cf19f653a", "packages": [ { "name": "brick/math", @@ -3983,6 +3983,77 @@ "homepage": "https://github.com/tijsverkoyen/CssToInlineStyles", "time": "2023-01-03T09:29:04+00:00" }, + { + "name": "tucker-eric/eloquentfilter", + "version": "3.2.0", + "source": { + "type": "git", + "url": "https://github.com/Tucker-Eric/EloquentFilter.git", + "reference": "faaad783b7f23af7ba7e23baaa56d71af51504a9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Tucker-Eric/EloquentFilter/zipball/faaad783b7f23af7ba7e23baaa56d71af51504a9", + "reference": "faaad783b7f23af7ba7e23baaa56d71af51504a9", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "illuminate/config": "~6.0|~7.0|~8.0|~9.0|~10.0", + "illuminate/console": "~6.0|~7.0|~8.0|~9.0|~10.0", + "illuminate/database": "~6.0|~7.0|~8.0|~9.0|~10.0", + "illuminate/filesystem": "~6.0|~7.0|~8.0|~9.0|~10.0", + "illuminate/pagination": "~6.0|~7.0|~8.0|~9.0|~10.0", + "illuminate/support": "~6.0|~7.0|~8.0|~9.0|~10.0", + "php": ">=7.2" + }, + "require-dev": { + "mockery/mockery": "^1.3", + "phpunit/phpunit": "^8" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "EloquentFilter\\ServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "EloquentFilter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eric Tucker", + "email": "tucker.ericm@gmail.com" + } + ], + "description": "An Eloquent way to filter Eloquent Models", + "keywords": [ + "eloquent", + "filter", + "laravel", + "model", + "query", + "search" + ], + "support": { + "issues": "https://github.com/Tucker-Eric/EloquentFilter/issues", + "source": "https://github.com/Tucker-Eric/EloquentFilter/tree/3.2.0" + }, + "time": "2023-02-07T18:34:53+00:00" + }, { "name": "vlucas/phpdotenv", "version": "v5.5.0", diff --git a/config/app.php b/config/app.php index da1f925..b01c153 100644 --- a/config/app.php +++ b/config/app.php @@ -196,7 +196,7 @@ return [ // App\Providers\BroadcastServiceProvider::class, App\Providers\EventServiceProvider::class, App\Providers\RouteServiceProvider::class, - + App\Providers\QueryLoggerServiceProvider::class, ], /* diff --git a/database/migrations/2023_03_16_145212_create_keywords_table.php b/database/migrations/2023_03_16_145212_create_keywords_table.php index 37c987d..ce4ac02 100644 --- a/database/migrations/2023_03_16_145212_create_keywords_table.php +++ b/database/migrations/2023_03_16_145212_create_keywords_table.php @@ -22,6 +22,7 @@ return new class extends Migration $table->unsignedInteger('sort')->default(0)->comment('排序'); $table->unsignedBigInteger('parent_id')->default(0)->comment('上级ID'); $table->unsignedInteger('level')->default(1)->comment('层级'); + $table->string('path')->default('-')->comment('所有的父级ID'); $table->timestamps(); }); }