diff --git a/app/Admin/Controllers/KeywordController.php b/app/Admin/Controllers/KeywordController.php index 813ffb8..33dd6e5 100644 --- a/app/Admin/Controllers/KeywordController.php +++ b/app/Admin/Controllers/KeywordController.php @@ -4,7 +4,6 @@ namespace App\Admin\Controllers; use Slowlyo\OwlAdmin\Renderers\Page; use Slowlyo\OwlAdmin\Renderers\Form; -use Slowlyo\OwlAdmin\Renderers\TableColumn; use Slowlyo\OwlAdmin\Renderers\TextControl; use Slowlyo\OwlAdmin\Controllers\AdminController; use App\Services\Admin\KeywordService; @@ -23,7 +22,7 @@ class KeywordController extends AdminController ->footerToolbar([]) //去掉分页-end ->headerToolbar([ - $this->createButton(true), + $this->createButton(true, 'md'), amis('reload')->align('right'), amis('filter-toggler')->align('right'), ]) @@ -38,13 +37,13 @@ class KeywordController extends AdminController )) ->columns([ // TableColumn::make()->name('id')->label('ID')->sortable(true), - TableColumn::make()->name('name')->label('名称'), - TableColumn::make()->name('key')->label('KEY')->copyable(true), - TableColumn::make()->name('value')->label('值'), - TableColumn::make()->name('sort')->label('排序'), - TableColumn::make()->name('created_at')->label('创建时间')->type('datetime')->sortable(true), + amis()->TableColumn('name', __('admin.keywords.name')), + amis()->TableColumn('key', __('admin.keywords.key'))->copyable(true), + amis()->TableColumn('value', __('admin.keywords.value')), + amis()->TableColumn('sort', __('admin.keywords.sort')), + amis()->TableColumn('created_at', __('admin.created_at'))->type('datetime')->sortable(true), amisMake()->Operation()->label(__('admin.actions'))->buttons([ - $this->rowEditButton(true), + $this->rowEditButton(true, 'md'), $this->rowDeleteButton(), ]), ]); @@ -56,10 +55,10 @@ class KeywordController extends AdminController { return $this->baseForm()->body([ 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('值'), - amisMake()->NumberControl()->name('sort')->value(0)->min()->label('排序'), + amis()->TextControl('name', __('admin.keywords.name'))->required(true), + amis()->TextControl('key', __('admin.keywords.key'))->required(true), + amis()->TextControl('value', __('admin.keywords.value')), + Components::make()->sortControl('sort', __('admin.keywords.sort')), ]); } diff --git a/app/Admin/Controllers/ProductCategoryController.php b/app/Admin/Controllers/ProductCategoryController.php new file mode 100644 index 0000000..24f872c --- /dev/null +++ b/app/Admin/Controllers/ProductCategoryController.php @@ -0,0 +1,67 @@ +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.product_categories.name')), + amis()->TableColumn('key', __('admin.product_categories.key'))->copyable(true), + amis()->TableColumn('cover', __('admin.product_categories.cover'))->type('image')->height('50px')->width('150px')->enlargeAble(true), + amis()->TableColumn('is_enable', __('admin.product_categories.is_enable'))->type('switch'), + amis()->TableColumn('is_show', __('admin.product_categories.is_show'))->type('switch'), + amis()->TableColumn('is_recommend', __('admin.product_categories.is_recommend'))->type('switch'), + 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/product_categories/tree-list')), + amis()->TextControl('name', __('admin.product_categories.name'))->required(true), + amis()->TextControl('key', __('admin.product_categories.key'))->required(true), + Components::make()->cropImageControl('cover', __('admin.product_categories.cover')), + Components::make()->sortControl('sort', __('admin.product_categories.sort')), + amis()->SwitchControl('is_enable', __('admin.product_categories.is_enable'))->value(false), + amis()->SwitchControl('is_show', __('admin.product_categories.is_show'))->value(false), + amis()->SwitchControl('is_recommend', __('admin.product_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/routes.php b/app/Admin/routes.php index b080bf1..4ff5e14 100644 --- a/app/Admin/routes.php +++ b/app/Admin/routes.php @@ -13,6 +13,8 @@ Route::group([ 'prefix' => 'api', ], function (Router $router) { $router->get('keywords/tree-list', '\App\Admin\Controllers\KeywordController@getTreeList')->name('api.keywords.tree-list'); + + $router->get('product_categories/tree-list', '\App\Admin\Controllers\ProductCategoryController@getTreeList')->name('api.product_categories.tree-list'); }); $router->resource('index', \App\Admin\Controllers\HomeController::class); @@ -32,6 +34,8 @@ Route::group([ $router->resource('ads', \App\Admin\Controllers\AdController::class); + $router->resource('product_categories', \App\Admin\Controllers\ProductCategoryController::class); + //修改上传 $router->post('upload_file', [\App\Admin\Controllers\IndexController::class, 'uploadFile']); $router->post('upload_image', [\App\Admin\Controllers\IndexController::class, 'uploadImage']); diff --git a/app/Models/Filters/ProductCategoryFilter.php b/app/Models/Filters/ProductCategoryFilter.php new file mode 100644 index 0000000..28325f5 --- /dev/null +++ b/app/Models/Filters/ProductCategoryFilter.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', '%-'. + ProductCategory::where('name','like', '%'.$parent_name.'%')->orWhere('key','like', '%'.$parent_name.'%')->value('id') + . '-%' ?? ''); + } +} diff --git a/app/Models/Product.php b/app/Models/Product.php new file mode 100644 index 0000000..c2ae1d8 --- /dev/null +++ b/app/Models/Product.php @@ -0,0 +1,13 @@ +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 ++; + $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/Services/Admin/KeywordService.php b/app/Services/Admin/KeywordService.php index 7e28bc1..152756d 100644 --- a/app/Services/Admin/KeywordService.php +++ b/app/Services/Admin/KeywordService.php @@ -74,7 +74,7 @@ class KeywordService extends BaseService $pid = Arr::get($data, 'parent_id'); if ($pid != 0) { if ($this->parentIsChild($primaryKey, $pid)) { - $this->setError('父级不允许设置为当前子权限'); + $this->setError('父级不允许设置为当前子级'); return false; } } diff --git a/app/Services/Admin/ProductCategoryService.php b/app/Services/Admin/ProductCategoryService.php new file mode 100644 index 0000000..160a6ab --- /dev/null +++ b/app/Services/Admin/ProductCategoryService.php @@ -0,0 +1,125 @@ +query()->filter(request()->all(), $this->modelFilterName)->orderByDesc('sort')->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', 'product_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', 'articles/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_02_114054_create_product_categories_table.php b/database/migrations/2024_01_02_114054_create_product_categories_table.php new file mode 100644 index 0000000..6327fe5 --- /dev/null +++ b/database/migrations/2024_01_02_114054_create_product_categories_table.php @@ -0,0 +1,42 @@ +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('product_categories'); + } +}; diff --git a/database/migrations/2024_01_02_114101_create_products_table.php b/database/migrations/2024_01_02_114101_create_products_table.php new file mode 100644 index 0000000..f501c72 --- /dev/null +++ b/database/migrations/2024_01_02_114101_create_products_table.php @@ -0,0 +1,53 @@ +id(); + $table->string('name')->comment('名称'); + $table->string('spu')->nullable()->comment('spu'); + $table->string('sku')->nullable()->unique()->comment('sku'); + $table->unsignedBigInteger('category_id')->nullable()->comment('分类'); + $table->string('t_ids')->nullable()->comment('标签'); + + $table->string('cover')->nullable()->comment('封面'); + $table->text('photo')->nullable()->comment('图片'); + $table->text('base_info')->nullable()->comment('基础信息'); + $table->text('description')->nullable()->comment('详情'); + + $table->text('attr_config')->nullable()->comment('属性'); + $table->text('attr_values')->nullable()->comment('属性值');//sku才有属性值 + + $table->unsignedInteger('virtual_sales')->default(0)->comment('虚拟销量'); + $table->unsignedInteger('sales')->default(0)->comment('销量'); + + $table->unsignedTinyInteger('is_sale')->default(0)->comment('上架开关'); + $table->unsignedTinyInteger('is_show')->default(0)->comment('显示开关'); + $table->unsignedTinyInteger('is_recommend')->default(0)->comment('推荐开关'); + $table->unsignedTinyInteger('is_hot')->default(0)->comment('热销开关'); + $table->unsignedTinyInteger('is_new')->default(0)->comment('上新开关'); + + $table->unsignedDecimal('sale_price', 10, 2)->default(0.00)->comment('售价'); + $table->unsignedInteger('stocks')->default(0)->comment('库存'); + + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('products'); + } +}; diff --git a/database/seeders/AdminMenuSeeder.php b/database/seeders/AdminMenuSeeder.php index 64d4d73..a602ac2 100644 --- a/database/seeders/AdminMenuSeeder.php +++ b/database/seeders/AdminMenuSeeder.php @@ -30,12 +30,23 @@ class AdminMenuSeeder extends Seeder ['title' => 'keywords', 'icon' => 'ph:codesandbox-logo-light', 'url' => '/system/keywords', 'order'=>6] ], ], - ['title' => 'web_content', 'icon' => 'ic:outline-collections-bookmark', 'url' => '', 'order'=>3, + ['title' => 'web_content', 'icon' => 'ic:outline-collections-bookmark', 'url' => '/web_content', 'order'=>3, 'children' =>[ ['title'=>'articles', 'icon'=>'ic:outline-article','url'=>'/articles', 'order'=>1], ['title'=>'ads', 'icon'=>'lets-icons:img-box','url'=>'/ads', 'order'=>2], ] - ] + ], + ['title' => 'mall', 'icon' => 'uil:shop', 'url' => '/mall', 'order'=> 4, + 'children' => [ + ['title'=>'product_categories', 'icon'=>'tabler:category-2', 'url'=>'/product_categories', 'order'=>1], + ['title'=>'products', 'icon'=>'ep:goods', 'url'=>'/products', 'order'=>2], + ['title'=>'shipping_tmps', 'icon'=>'streamline:shipping-truck', 'url'=>'/shipping_tmps', 'order'=>3], + ['title'=>'orders', 'icon'=>'icon-park-outline:transaction-order', 'url'=>'/orders', 'order'=>4], + ['title'=>'shipping_orders', 'icon'=>'akar-icons:shipping-box-02', 'url'=>'/shipping_orders', 'order'=>5], + ['title'=>'after_orders', 'icon'=>'material-symbols-light:inactive-order-outline-sharp', 'url'=>'/after_orders', 'order'=>6], + ['title'=>'product_stocks', 'icon'=>'material-symbols:warehouse-outline-rounded', 'url'=>'/product_stocks', 'order'=>7], + ] + ], ]; DB::table('admin_menus')->truncate(); try { diff --git a/lang/zh_CN/admin.php b/lang/zh_CN/admin.php index f69b279..c73a8d1 100644 --- a/lang/zh_CN/admin.php +++ b/lang/zh_CN/admin.php @@ -270,6 +270,10 @@ return [ 'please_install_laravel_excel' => '请先安装 laravel-excel 扩展', ], 'keywords' => [ + 'name' => '名称', + 'key' => 'KEY', + 'value' => '值', + 'sort' => '排序', 'search_name' => '名称/KEY', 'parent_keyword' => '父级关键字', ], @@ -307,5 +311,14 @@ return [ 'mini_id' => '小程序ID', 'mini_link'=> '小程序路径' ], + ], + 'product_categories' => [ + 'name' => '名称', + '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 dbedcd6..cad55a5 100644 --- a/lang/zh_CN/menu.php +++ b/lang/zh_CN/menu.php @@ -12,5 +12,13 @@ return [ 'keywords' => '数据字典', 'web_content' => '内容管理', 'articles' => '文章管理', - 'ads' => '广告管理', + 'ads' => '广告管理', + 'mall' => '商城管理', + 'product_categories'=> '商品分类', + 'products' => '商品管理', + 'shipping_tmps' => '运费模板', + 'orders' => '订单管理', + 'shipping_orders' => '发货管理', + 'after_orders' => '售后管理', + 'product_stocks' => '商品库存', ];