diff --git a/app/Admin/Controllers/ArticleCategoryController.php b/app/Admin/Controllers/ArticleCategoryController.php new file mode 100644 index 0000000..442f024 --- /dev/null +++ b/app/Admin/Controllers/ArticleCategoryController.php @@ -0,0 +1,68 @@ +baseCRUD() + //去掉分页-start + ->loadDataOnce(true) + ->footerToolbar([]) + //去掉分页-end + ->headerToolbar([ + $this->createButton(true, 'md'), + amis('reload')->align('right'), + amis('filter-toggler')->align('right'), + ]) + ->filter($this->baseFilter()->body([ + + ] + )) + ->columns([ + amis()->TableColumn()->make()->name('id')->label('ID')->sortable(true), + amis()->TableColumn('name', __('admin.article_categories.name')), + amis()->TableColumn('key', __('admin.article_categories.key'))->copyable(true), + // amis()->TableColumn('cover', __('admin.article_categories.cover'))->type('image')->height('50px')->width('150px')->enlargeAble(true), + amis()->TableColumn('is_enable', __('admin.article_categories.is_enable'))->type('switch'), + amis()->TableColumn('is_show', __('admin.article_categories.is_show'))->type('switch'), + amis()->TableColumn('is_recommend', __('admin.article_categories.is_recommend'))->type('switch'), + amis()->TableColumn('sort', __('admin.article_categories.sort')), + amis()->TableColumn('created_at', __('admin.created_at'))->type('datetime')->sortable(true), + amisMake()->Operation()->label(__('admin.actions'))->buttons([ + $this->rowEditButton(true, 'md'), + $this->rowDeleteButton(), + ]), + ]); + + return $this->baseList($crud); + } + + public function form(): Form + { + return $this->baseForm()->body([ + Components::make()->parentControl(admin_url('api/article_categories/tree-list')), + amis()->TextControl('name', __('admin.article_categories.name'))->required(true), + // amis()->TextControl('key', __('admin.article_categories.key'))->required(true), + Components::make()->cropImageControl('cover', __('admin.article_categories.cover')), + Components::make()->sortControl('sort', __('admin.article_categories.sort')), + amis()->SwitchControl('is_enable', __('admin.article_categories.is_enable'))->value(false), + amis()->SwitchControl('is_show', __('admin.article_categories.is_show'))->value(false), + amis()->SwitchControl('is_recommend', __('admin.article_categories.is_recommend'))->value(false), + ]); + } + + public function getTreeList(Request $request){ + return $this->service->getTree(); + } +} \ No newline at end of file diff --git a/app/Admin/Controllers/ArticleController.php b/app/Admin/Controllers/ArticleController.php index ce77a01..51a6816 100644 --- a/app/Admin/Controllers/ArticleController.php +++ b/app/Admin/Controllers/ArticleController.php @@ -59,9 +59,7 @@ class ArticleController extends AdminController ->columns([ 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('cate.name', __('admin.articles.category')), 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(__('admin.articles.published_at_remark')), @@ -83,14 +81,14 @@ class ArticleController extends AdminController amis()->Grid()->columns([ amis()->Wrapper()->body([ amis()->TextControl('title', __('admin.articles.title'))->required(true), - Components::make()->parentControl(admin_url('api/keywords/tree-list?parent_name=article_category&has_owner=0'), 'category', __('admin.articles.category'), 'name', 'key'), + Components::make()->parentControl(admin_url('api/article_categories/tree-list'), 'category', __('admin.articles.category')), 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(__('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'), + Components::make()->fileControl('appendixes', __('admin.articles.appendixes'), '.xsl,.xlsx,.txt,.doc,.docx,.pdf,.pptx', true), ])->md(4), amis()->Wrapper()->body([ Components::make()->fuEditorControl('content', __('admin.articles.content')), diff --git a/app/Admin/routes.php b/app/Admin/routes.php index 86a4f2a..19a9267 100644 --- a/app/Admin/routes.php +++ b/app/Admin/routes.php @@ -14,6 +14,7 @@ Route::group([ ], function (Router $router) { $router->get('keywords/tree-list', '\App\Admin\Controllers\KeywordController@getTreeList')->name('api.keywords.tree-list'); $router->get('institutions/tree-list', '\App\Admin\Controllers\InstitutionController@getTreeList')->name('api.institution.tree-list'); + $router->get('article_categories/tree-list', '\App\Admin\Controllers\ArticleCategoryController@getTreeList')->name('api.article_categories.tree-list'); }); $router->resource('index', \App\Admin\Controllers\HomeController::class); @@ -35,6 +36,8 @@ Route::group([ $router->resource('friend_links', \App\Admin\Controllers\FriendLinkController::class); + $router->resource('article_categories', \App\Admin\Controllers\ArticleCategoryController::class); + //数据管理 $router->resource('financial_cate', \App\Admin\Controllers\KeywordController::class); diff --git a/app/Models/Article.php b/app/Models/Article.php index 26a95cd..374f644 100644 --- a/app/Models/Article.php +++ b/app/Models/Article.php @@ -60,4 +60,9 @@ class Article extends Model get: fn($value) => $this->t_ids ? explode(',', $this->t_ids) : [], ); } + + + public function cate(){ + return $this->belongsTo(ArticleCategory::class, 'category'); + } } diff --git a/app/Models/ArticleCategory.php b/app/Models/ArticleCategory.php new file mode 100644 index 0000000..9451e19 --- /dev/null +++ b/app/Models/ArticleCategory.php @@ -0,0 +1,55 @@ +format('Y-m-d H:i:s'); + } + + protected $fillable = [ + 'name', 'key', 'cover', + 'parent_id', 'parent_key', 'lv', 'path', + 'is_enable', 'is_show', 'is_recommend', + 'sort', + ]; + + protected static function boot() + { + parent::boot(); + // 监听 Keyword 的创建事件,用于初始化 path 和 lv 字段值 + static::saving(function ($keyword) { + // 如果创建的是一个根类目 + if (! $keyword->parent_id) { + // 将层级设为 1 + $keyword->lv = 1; + // 将 path 设为 - + $keyword->path = '-'; + } else { + // 将层级设为父类目的层级 + 1 + $keyword->lv = $keyword->parent->lv +1; + $keyword->parent_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/Models/Filters/ArticleCategoryFilter.php b/app/Models/Filters/ArticleCategoryFilter.php new file mode 100644 index 0000000..f7696c7 --- /dev/null +++ b/app/Models/Filters/ArticleCategoryFilter.php @@ -0,0 +1,31 @@ +where('name','like', '%'.$name.'%') + ->orWhere('key','like', '%'.$name.'%'); + } + + public function parentName($parent_name) + { + if(request('has_owner', 1)){ + $this->where(function($q) use ($parent_name){ + $q->where('name','like', '%'.$parent_name.'%') + ->orWhere('key','like', '%'.$parent_name.'%'); + }); + } + return $this->orWhere('path','like', '%-'. + ArticleCategory::where('name','like', '%'.$parent_name.'%')->orWhere('key','like', '%'.$parent_name.'%')->value('id') + . '-%' ?? ''); + } +} diff --git a/app/Services/Admin/ArticleCategoryService.php b/app/Services/Admin/ArticleCategoryService.php new file mode 100644 index 0000000..e97d39e --- /dev/null +++ b/app/Services/Admin/ArticleCategoryService.php @@ -0,0 +1,125 @@ +query()->filter(request()->all(), $this->modelFilterName)->get(); + $minNum = $list->min('parent_id'); + return !$list->isEmpty() ? array2tree($list->toArray(), $minNum) :[]; + } + + public function parentIsChild($id, $pid): bool + { + $parent = $this->query()->find($pid); + + 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(); + + $data['cover'] = $this->saveImage('cover', 'article_category/cover')[0] ?? ''; + 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(); + + $pid = Arr::get($data, 'parent_id'); + if ($pid != 0) { + if ($this->parentIsChild($primaryKey, $pid)) { + $this->setError('父级不允许设置为当前子级'); + return false; + } + } + + $model = $this->query()->whereKey($primaryKey)->first(); + + if(isset($data['cover'])){ + $data['cover'] = $this->saveImage('cover', 'article_category/cover')[0] ?? ''; + } + 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 (isset($data['key']) && (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/database/migrations/2024_01_23_102611_create_article_categories_table.php b/database/migrations/2024_01_23_102611_create_article_categories_table.php new file mode 100644 index 0000000..b32cefe --- /dev/null +++ b/database/migrations/2024_01_23_102611_create_article_categories_table.php @@ -0,0 +1,41 @@ +id(); + $table->string('name')->comment('名称'); + $table->string('key')->nullable()->unique(); + $table->string('cover')->nullable()->comment('封面'); + + $table->unsignedBigInteger('parent_id')->default(0)->comment('上级ID'); + $table->string('parent_key')->nullable('上级key'); + $table->unsignedInteger('lv')->default(1)->comment('层级'); + $table->string('path')->default('-')->comment('所有的父级ID'); + + $table->unsignedTinyInteger('is_enable')->default(1)->comment('启用开关'); + $table->unsignedTinyInteger('is_show')->default(0)->comment('显示开关'); + $table->unsignedTinyInteger('is_recommend')->default(0)->comment('推荐开关'); + + $table->unsignedInteger('sort')->default(0)->comment('排序'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('article_categories'); + } +}; diff --git a/database/seeders/AdminMenuSeeder.php b/database/seeders/AdminMenuSeeder.php index ee73341..feeed61 100644 --- a/database/seeders/AdminMenuSeeder.php +++ b/database/seeders/AdminMenuSeeder.php @@ -35,6 +35,8 @@ class AdminMenuSeeder extends Seeder 'children' => [ ['title'=>'ads', 'icon'=>'lets-icons:img-box','url'=>'/ads', 'order'=>0], ['title'=>'friend_links', 'icon'=>'mdi:link-variant','url'=>'/friend_links', 'order'=>1], + ['title'=>'article_categories', 'icon'=>'tabler:category-2', 'url'=>'/article_categories', 'order'=>2], + ['title'=>'articles', 'icon'=>'ic:outline-article','url'=>'/articles', 'order'=>3], ] ], ['title' => 'data_content', 'icon' => 'ph:codesandbox-logo-light', 'url' => '/data_content', 'order'=>3, //数据管理 diff --git a/database/seeders/ArticleCategorySeeder.php b/database/seeders/ArticleCategorySeeder.php new file mode 100644 index 0000000..4c28afb --- /dev/null +++ b/database/seeders/ArticleCategorySeeder.php @@ -0,0 +1,56 @@ +get()->sortBy('pid'); + + $newCategories = []; + foreach($oldCates as $cate){ + if(empty($cate->name)){ + continue; + } + $_category = [ + 'id' => $cate -> id, + 'parent_id' => $cate -> pid ?? 0, + 'name' => $cate->name, + 'cover' => $cate->icon ?? null, + 'sort' => $cate -> sorted, + 'is_recommend' => $cate->recommend, + 'is_show' => $cate->showInOut, + 'lv' => !empty($cate->pid) ? $newCategories[$cate->pid]['lv'] + 1:1, + 'path' => !empty($cate->pid) ? $newCategories[$cate->pid]['path'] . $cate->pid .'-':'-', + 'created_at' => now(), + 'updated_at' => now(), + ]; + + $newCategories[$cate->id] = $_category; + } + + if(count($newCategories) > 0){ + DB::table('article_categories')->truncate(); + try { + DB::begintransaction(); + DB::table('article_categories')->insert($newCategories); + DB::commit(); + } catch (Throwable $th) { + DB::rollBack(); + report($th); + } + } + } +} \ No newline at end of file diff --git a/lang/zh_CN/admin.php b/lang/zh_CN/admin.php index 8b287e3..32f9128 100644 --- a/lang/zh_CN/admin.php +++ b/lang/zh_CN/admin.php @@ -326,5 +326,16 @@ return [ 'cover'=> '图片', 'sort' => '排序', 'is_enable'=>'显示', - ] + ], + 'article_categories' => [ + 'name' => '名称', + 'parent' => '上级分类', + 'parent_id' => '上级分类', + 'key' => 'KEY', + 'cover'=>'图标', + 'sort'=>'排序', + 'is_enable' => '启用', + 'is_show' => '展示', + 'is_recommend' => '推荐' + ], ]; diff --git a/lang/zh_CN/menu.php b/lang/zh_CN/menu.php index f944423..ae01683 100644 --- a/lang/zh_CN/menu.php +++ b/lang/zh_CN/menu.php @@ -34,4 +34,5 @@ return [ 'welfare_cate' => '福利类型', 'job_cate' => '工种管理', 'friend_links' => '友情链接', + 'article_categories'=> '文章分类', ];