commit eaaafbb10e19ca19f4e1c155b95eb0c826f1e76b Author: panliang <1163816051@qq.com> Date: Fri Sep 9 15:27:28 2022 +0800 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9d4b362 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.DS_Store +phpunit.phar +/vendor +composer.phar +composer.lock +*.project +.idea/ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..859c1c2 --- /dev/null +++ b/README.md @@ -0,0 +1,68 @@ +# Dcat Admin Extension Goods + +Dcat-admin 商品管理 + +## 安装 + +- 进入项目目录 +- `mkdir packages && cd packages` +- `git clone https://gitea.peidikeji.cn/pdkj/dcat-admin-goods.git` +- `composer config repositories.peidikeji/dcat-admin-user path ./packages/dcat-admin-goods` +- `composer require peidikeji/dcat-admin-goods:dev-develop` +- `php artisan vendor:publish --provider=Peidikeji\Goods\GoodsServiceProvider` + +## 数据表 + +### 商品分类: goods_category + +| column | type | nullable | default | comment | +| - | - | - | - | - | +| id | bigint | not null | - | 主键 | +| name | varchar(191) | not null | - | 分类名称 | +| image | varchar(191) | null | - | 分类图片 | +| description | varchar(191) | null | - | 描述 | +| parent_id | bigint | not null | 0 | 上级 id | +| level | int | not null | 1 | 层级 | +| sort | int | not null | 1 | 排序(asc) | +| is_enable | int | not null | 1 | 是否可用(0, 1) | +| path | varchar(191) | not null | '-' | 所有上级 id(1-2-3-, -) | +| created_at | timestamp | null | - | 创建时间 | +| updated_at | timestamp | null | - | 更新时间 | + +### 商品类别: goods_type + +| column | type | nullable | default | comment | +| - | - | - | - | - | +| id | bigint | not null | - | 主键 | +| name | varchar(191) | not null | - | 名称 | +| attr | json | null | - | 属性展示 | +| spec | json | null | - | 规格筛选 | +| part | json | null | - | 配件多选 | + +> goods_type.attr 存储格式 + +```json +[ + {"name": "主体", "values": ["入网型号", "上市年份"]}, + {"name": "基本信息", "values": ["尺寸", "颜色", "CPU型号", "重量"]} +] +``` + +> goods_type.spec 存储格式 + +```json +[ + {"name": "颜色", "values": ["红色", "白色", "灰色"]}, + {"name": "版本", "values": ["128G", "256G", "512G", "1TB"]} +] +``` + +> goods_type.part 存储格式 + +```json +[ + {"name": "配件", "values": ["耳机", "快充", "手机壳"]}, +] +``` + +### 商品品牌: goods_brand diff --git a/assets/goods.css b/assets/goods.css new file mode 100644 index 0000000..95218e7 --- /dev/null +++ b/assets/goods.css @@ -0,0 +1,12 @@ +.grid-attr .group:not(:first-child) { + margin-top: 10px; +} +.grid-attr .group-item { + display: inline-block;vertical-align: top; +} +.grid-attr .group-title { + min-width: 80px +} +.grid-attr .group-value:not(:first-child) { + margin-top: 10px; +} diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..2aa6503 --- /dev/null +++ b/composer.json @@ -0,0 +1,33 @@ +{ + "name": "peidikeji/dcat-admin-goods", + "alias": "goods", + "description": "基础商品管理", + "type": "library", + "keywords": ["dcat-admin", "extension"], + "homepage": "https://github.com/peidikeji/goods", + "license": "MIT", + "authors": [ + { + "name": "panliang", + "email": "1163816051@qq.com" + } + ], + "require": { + "php": ">=8.1.0", + "peidikeji/dcat-admin": "*", + "tucker-eric/eloquentfilter": "^3.1", + "laravel/framework": "^9.0" + }, + "autoload": { + "psr-4": { + "Peidikeji\\Goods\\": "src/" + } + }, + "extra": { + "laravel": { + "providers": [ + "Peidikeji\\Goods\\GoodsServiceProvider" + ] + } + } +} diff --git a/database/2022_08_11_184332_create_goods_table.php b/database/2022_08_11_184332_create_goods_table.php new file mode 100644 index 0000000..ae8363c --- /dev/null +++ b/database/2022_08_11_184332_create_goods_table.php @@ -0,0 +1,143 @@ +id(); + $table->string('name')->comment('分类名称'); + $table->string('image')->nullable()->comment('封面图'); + $table->string('description')->nullable()->comment('描述'); + $table->unsignedBigInteger('parent_id')->default(0)->comment('父级ID'); + $table->unsignedInteger('level')->default(1)->comment('层级'); + $table->unsignedInteger('sort')->default(1)->comment('排序 asc'); + $table->unsignedTinyInteger('is_enable')->default(1)->comment('状态'); + $table->string('path')->default('-')->comment('所有的父级ID'); + + $table->comment('商品-分类'); + }); + + Schema::create('goods_type', function (Blueprint $table) { + $table->id(); + $table->string('name'); + $table->json('attr')->nullable()->comment('属性[{name: "属性名", values: [可选值]}]'); + $table->json('spec')->nullable()->comment('规格[{name: "属性名", values: [可选值]}]'); + $table->json('part')->nullable()->comment('配件[{name: "属性名", values: [可选值]}]'); + $table->comment('商品-类型'); + }); + + Schema::create('goods_brand', function (Blueprint $table) { + $table->id(); + $table->string('name')->comment('名称'); + $table->string('image')->nullable()->comment('图标'); + $table->comment('商品-品牌'); + }); + + Schema::create('goods', function (Blueprint $table) { + $table->id(); + $table->unsignedBigInteger('category_id')->comment('所属分类, 关联 goods_category.id'); + $table->unsignedBigInteger('merchant_id')->nullable()->comment('商户ID'); + $table->unsignedBigInteger('type_id')->nullable()->comment('所属类别'); + $table->unsignedBigInteger('brand_id')->nullable()->comment('所属品牌'); + $table->string('name')->comment('商品名称'); + $table->string('goods_sn')->comment('编号'); + $table->string('cover_image')->nullable()->comment('封面图'); + $table->json('images')->nullable()->comment('图片集'); + $table->string('description')->nullable()->comment('描述'); + $table->json('content')->nullable()->comment('详细'); + $table->unsignedInteger('on_sale')->default(0)->comment('是否上架'); + $table->unsignedInteger('is_recommend')->default(0)->comment('是否推荐'); + $table->unsignedInteger('stock')->default(0)->comment('库存'); + $table->unsignedInteger('sold_count')->default(0)->comment('销量'); + $table->decimal('price', 12, 2)->comment('售价'); + $table->decimal('vip_price', 12, 2)->comment('会员价'); + $table->decimal('score_max_amount', 12, 2)->default(0)->comment('积分抵扣最大值'); + $table->json('attr')->nullable()->comment('属性[{name, values: [{name, value}]}]'); + $table->json('spec')->nullable()->comment('规格[{name, values: [{name, value}]}]'); + $table->json('part')->nullable()->comment('配件[{name, values: [{name, value}]}]'); + + $table->unsignedInteger('check_status')->default(0)->comment('审核状态(0: 未提交, 1: 审核中, 2: 审核通过, 3: 审核不通过)'); + $table->string('check_remarks')->nullable()->comment('审核备注'); + $table->timestamp('check_at')->nullable()->comment('审核通过时间'); + $table->unsignedBigInteger('check_user_id')->nullable()->comment('审核人'); + + $table->timestamps(); + + $table->comment('商品'); + }); + + Schema::create('goods_checks', function (Blueprint $table) { + $table->id(); + $table->unsignedBigInteger('goods_id')->comment('所属商品, 关联 goods.id'); + + $table->unsignedBigInteger('category_id')->comment('所属分类, 关联 goods_category.id'); + $table->unsignedBigInteger('merchant_id')->nullable()->comment('商户ID'); + $table->unsignedBigInteger('type_id')->nullable()->comment('所属类别'); + $table->unsignedBigInteger('brand_id')->nullable()->comment('所属品牌'); + $table->string('name')->comment('商品名称'); + $table->string('goods_sn')->comment('编号'); + $table->string('cover_image')->nullable()->comment('封面图'); + $table->json('images')->nullable()->comment('图片集'); + $table->string('description')->nullable()->comment('描述'); + $table->json('content')->nullable()->comment('详细'); + $table->unsignedInteger('on_sale')->default(0)->comment('是否上架'); + $table->unsignedInteger('stock')->default(0)->comment('库存'); + $table->unsignedInteger('sold_count')->default(0)->comment('销量'); + $table->decimal('price', 12, 2)->comment('售价'); + $table->decimal('vip_price', 12, 2)->comment('会员价'); + $table->decimal('score_max_amount', 12, 2)->default(0)->comment('积分抵扣最大值'); + $table->json('attr')->nullable()->comment('属性[{name, values: [{name, value}]}]'); + $table->json('spec')->nullable()->comment('规格[{name, values: [{name, value}]}]'); + $table->json('part')->nullable()->comment('配件[{name, values: [{name, value}]}]'); + + $table->unsignedInteger('check_status')->default(0)->comment('审核状态(0: 未提交, 1: 审核中, 2: 审核通过, 3: 审核不通过)'); + $table->string('check_remarks')->nullable()->comment('审核备注'); + $table->timestamp('check_at')->nullable()->comment('审核通过时间'); + $table->unsignedBigInteger('check_user_id')->nullable()->comment('审核人'); + + $table->timestamps(); + + $table->comment('商品-上架审核'); + }); + + Schema::create('goods_sku', function (Blueprint $table) { + $table->id(); + $table->string('sn')->comment('货号'); + $table->unsignedBigInteger('goods_id')->comment('所属商品, 关联 goods.id'); + $table->string('name')->comment('名称'); + $table->decimal('price', 12, 2)->comment('价格'); + $table->decimal('vip_price', 12, 2)->comment('会员价'); + $table->decimal('score_max_amount', 12, 2)->default(0)->comment('积分抵扣最大值'); + $table->unsignedInteger('stock')->comment('库存'); + $table->unsignedInteger('sold_count')->default(0)->comment('销量'); + $table->json('spec')->nullable()->comment('规格[{name, value, price}]'); + + $table->comment('商品-SKU'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('goods_sku'); + Schema::dropIfExists('goods_checks'); + Schema::dropIfExists('goods'); + Schema::dropIfExists('goods_category'); + Schema::dropIfExists('goods_type'); + Schema::dropIfExists('goods_brand'); + } +} diff --git a/database/2022_08_19_151926_create_goods_cart_table.php b/database/2022_08_19_151926_create_goods_cart_table.php new file mode 100644 index 0000000..34cd13b --- /dev/null +++ b/database/2022_08_19_151926_create_goods_cart_table.php @@ -0,0 +1,50 @@ +id(); + + $table->unsignedBigInteger('user_id')->comment('用户ID'); + $table->unsignedBigInteger('goods_id')->comment('商品ID'); + $table->unsignedBigInteger('merchant_id')->nullable()->comment('店铺 ID'); + + $table->string('goods_name'); + $table->string('goods_sn')->nullable(); + $table->string('goods_sku_sn')->nullable(); + $table->unsignedBigInteger('goods_sku_id')->nullable()->comment('商品sku ID'); + $table->string('cover_image')->nullable()->comment('封面图'); + $table->decimal('price', 12, 2)->comment('售价'); + $table->decimal('vip_price', 12, 2)->comment('售价'); + $table->json('attr')->nullable()->comment('属性'); + $table->json('spec')->nullable()->comment('规格'); + $table->json('part')->nullable()->comment('配件'); + + $table->unsignedInteger('amount')->comment('数量'); + $table->timestamps(); + + $table->comment('购物车'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('goods_cart'); + } +}; diff --git a/goods-attr.json b/goods-attr.json new file mode 100644 index 0000000..076531b --- /dev/null +++ b/goods-attr.json @@ -0,0 +1,74 @@ +[ + { + "table": "goods-type.spec", + "name": "颜色", + "values": [ + "白色", + "红色", + "蓝色" + ] + }, + { + "table": "goods-type.attr", + "name": "主体", + "values": [ + "入网型号", + "上市年份" + ] + }, + { + "table": "goods-type.part", + "name": "套餐", + "values": [ + "套餐1", + "套餐2" + ] + }, + { + "table": "goods.attr", + "name": "主体", + "values": [ + { "name": "入网型号", "value": "5G" }, + { "name": "上市年份", "value": "2020" } + ] + }, + { + "table": "goods.spec", + "name": "颜色", + "values": [ + { "name": "白色", "value": 0 }, + { "name": "红色", "value": 0 }, + { "name": "蓝色", "value": 1 } + ] + }, + { + "table": "goods.part", + "name": "套餐", + "values": [ + { "name": "套餐1", "value": 150 }, + { "name": "套餐2", "value": 100 } + ] + }, + { + "table": "goods-sku.attr", + "name": "主体", + "values": [ + { "name": "入网型号", "value": "5G" }, + { "name": "上市年份", "value": "2020" } + ] + }, + { + "table": "goods-sku.spec", + "name": "颜色", + "value": "白色", + "price": 0 + }, + { + "table": "goods-sku.part", + "name": "颜色", + "values": [ + { "name": "套餐1", "price": 150 }, + { "name": "套餐2", "price": 100 } + ] + } +] diff --git a/lang/en/goods-brand.php b/lang/en/goods-brand.php new file mode 100644 index 0000000..0b67a5f --- /dev/null +++ b/lang/en/goods-brand.php @@ -0,0 +1,3 @@ + [ + 'GoodsBrand' => '品牌管理', + 'goods' => '商品管理', + 'brand' => '品牌', + 'create' => '创建', + 'edit' => '修改', + ], + 'fields' => [ + 'name' => '名称', + 'image' => '图片', + ], +]; diff --git a/lang/zh_CN/goods-category.php b/lang/zh_CN/goods-category.php new file mode 100644 index 0000000..97d342b --- /dev/null +++ b/lang/zh_CN/goods-category.php @@ -0,0 +1,22 @@ + [ + 'GoodsCategory' => '商品分类', + 'goods' => '商品管理', + 'category' => '分类管理', + 'root' => '无', + 'goods-category' => '商品分类', + ], + 'fields' => [ + 'name' => '分类名称', + 'image' => '图片', + 'description' => '描述', + 'sort' => '排序(正序)', + 'parent_id' => '上级', + 'is_enable' => '开启', + 'parent' => [ + 'name' => '上级', + ], + ], +]; diff --git a/lang/zh_CN/goods-sku.php b/lang/zh_CN/goods-sku.php new file mode 100644 index 0000000..e40fbc4 --- /dev/null +++ b/lang/zh_CN/goods-sku.php @@ -0,0 +1,24 @@ + [ + 'GoodsSku' => '货品管理', + 'goods' => '商品管理', + 'sku' => '货品管理', + 'create' => '添加', + ], + 'fields' => [ + 'sn' => '货号', + 'name' => '名称', + 'price' => '售价', + 'vip_price' => '会员价', + 'weight' => '重量', + 'volume' => '体积', + 'shipping_tmp_id' => '运费模板', + 'stock' => '库存', + 'spec' => '规格', + 'origin_price' => '原价', + 'discount_price' => '折扣价', + 'score_max_amount' => '最大抵扣积分', + ], +]; diff --git a/lang/zh_CN/goods-type.php b/lang/zh_CN/goods-type.php new file mode 100644 index 0000000..2653bf8 --- /dev/null +++ b/lang/zh_CN/goods-type.php @@ -0,0 +1,17 @@ + [ + 'GoodsType' => '商品类别', + 'goods' => '商品管理', + 'type' => '商品类别', + ], + 'fields' => [ + 'name' => '名称', + 'spec' => '规格', + 'attr' => '属性', + 'part' => '配件', + 'values' => '可选值', + 'group' => '分组', + ], +]; diff --git a/lang/zh_CN/goods.php b/lang/zh_CN/goods.php new file mode 100644 index 0000000..89cd30a --- /dev/null +++ b/lang/zh_CN/goods.php @@ -0,0 +1,60 @@ + [ + 'Goods' => '商品信息', + 'goods' => '商品信息', + 'create' => '创建', + 'edit' => '修改', + 'attr' => '属性', + 'spec' => '规格', + 'part' => '配件', + 'GoodsCheck' => '上架审核', + 'goods-check' => '上架审核', + ], + 'fields' => [ + 'merchant_id' => '店铺', + 'merchant' => [ + 'name' => '店铺', + ], + 'category_id' => '分类', + 'category' => [ + 'name' => '分类', + ], + 'brand_id' => '品牌', + 'brand' => [ + 'name' => '品牌', + ], + 'type_id' => '类别', + 'type' => [ + 'name' => '类别', + ], + 'name' => '名称', + 'goods_sn' => '编号', + 'cover_image' => '封面图', + 'price' => '售价', + 'vip_price' => '会员价', + 'score_max_amount' => '积分最大抵扣', + 'weight' => '重量', + 'volume' => '体积', + 'shipping_tmp_id' => '运费模板', + 'spec' => '规格', + 'attr' => '属性', + 'part' => '配件', + 'on_sale' => '上架', + 'stock' => '库存', + 'sold_count' => '销量', + 'images' => '详细图', + 'content' => '内容', + 'created_at' => '创建时间', + 'updated_at' => '更新时间', + 'check_remarks' => '未通过原因', + 'check_status' => '审核状态', + 'check_user_id' => '审核人', + 'check_user' => [ + 'name' => '审核人', + ], + 'check_at' => '审核时间', + 'is_recommend' => '推荐', + ], +]; diff --git a/routes/admin.php b/routes/admin.php new file mode 100644 index 0000000..2551c89 --- /dev/null +++ b/routes/admin.php @@ -0,0 +1,24 @@ + config('admin.route.prefix'), + 'middleware' => config('admin.route.middleware'), +], function () { + Route::resource('goods-category', GoodsCategoryController::class)->names('dcat.admin.goods_category'); + Route::resource('goods-brand', GoodsBrandController::class)->names('dcat.admin.goods_brand'); + Route::resource('goods-type', GoodsTypeController::class)->names('dcat.admin.goods_type'); + + Route::resource('goods/{goods}/sku', GoodsSkuController::class)->names('dcat.admin.goods_sku'); + + Route::get('goods/{goods}/attr', [GoodsController::class, 'attr'])->name('dcat.admin.goods.attr'); + Route::get('goods/{goods}/spec', [GoodsController::class, 'spec'])->name('dcat.admin.goods.spec'); + Route::get('goods/{goods}/part', [GoodsController::class, 'part'])->name('dcat.admin.goods.part'); + Route::resource('goods-check', GoodsCheckController::class)->names('dcat.admin.goods_check'); + Route::resource('goods', GoodsController::class)->names('dcat.admin.goods'); + + Route::get('api/goods', [GoodsController::class, 'list'])->name('dcat.admin.api.goods'); +}); diff --git a/routes/api.php b/routes/api.php new file mode 100644 index 0000000..8e98a6c --- /dev/null +++ b/routes/api.php @@ -0,0 +1,20 @@ + ['api'], + 'prefix' => 'api', +], function () { + Route::get('goods/category', [GoodsCategoryController::class, 'index']); + + Route::get('goods/cart-by-merchant', [GoodsCartController::class, 'groupByMerchant']); + Route::delete('goods/cart', [GoodsCartController::class, 'destroy']); + Route::apiResource('goods/cart', GoodsCartController::class)->only(['index', 'store', 'update']); + + Route::get('goods/{id}', [GoodsController::class, 'show']); + Route::get('goods/{id}/skus', [GoodsController::class, 'skus']); + Route::get('goods', [GoodsController::class, 'index']); +}); diff --git a/src/Action/Check/RowHandleCheck.php b/src/Action/Check/RowHandleCheck.php new file mode 100644 index 0000000..9b6d9ba --- /dev/null +++ b/src/Action/Check/RowHandleCheck.php @@ -0,0 +1,19 @@ +payload(['id' => $this->row('id')]); + + return Modal::make()->lg()->body($form)->title($this->title())->button($this->title); + } +} diff --git a/src/Action/Check/RowSubmitCheck.php b/src/Action/Check/RowSubmitCheck.php new file mode 100644 index 0000000..3053858 --- /dev/null +++ b/src/Action/Check/RowSubmitCheck.php @@ -0,0 +1,35 @@ +getKey()); + + try { + DB::beginTransaction(); + GoodsService::make()->submitCheck($goods); + DB::commit(); + + return $this->response()->success('操作成功')->refresh(); + } catch (\Exception $e) { + DB::rollBack(); + + return $this->response()->error($e->getMessage()); + } + } + + public function confirm() + { + return ['是否确定?']; + } +} diff --git a/src/Action/Goods/RowGoodsSale.php b/src/Action/Goods/RowGoodsSale.php new file mode 100644 index 0000000..a508b12 --- /dev/null +++ b/src/Action/Goods/RowGoodsSale.php @@ -0,0 +1,27 @@ +row->on_sale ? '下架' : '上架'; + } + + public function handle() + { + $info = Goods::findOrFail($this->getKey()); + Goods::where('id', $this->getKey())->update(['on_sale' => ! $info->on_sale]); + + return $this->response()->success('操作成功')->refresh(); + } + + public function confirm() + { + return ['是否确定?']; + } +} diff --git a/src/Filters/GoodsCategoryFilter.php b/src/Filters/GoodsCategoryFilter.php new file mode 100644 index 0000000..03bc884 --- /dev/null +++ b/src/Filters/GoodsCategoryFilter.php @@ -0,0 +1,18 @@ +where('parent_id', $v); + } + + public function level($v) + { + $this->where('level', $v); + } +} diff --git a/src/Filters/GoodsFilter.php b/src/Filters/GoodsFilter.php new file mode 100644 index 0000000..58741ad --- /dev/null +++ b/src/Filters/GoodsFilter.php @@ -0,0 +1,54 @@ +where('name', 'like', '%'.$v.'%'); + } + + public function search($key) + { + $this->where(function ($q) use ($key) { + $q->orWhere('name', 'like', "%$key%")->orWhere('description', 'like', "%$key%"); + }); + } + + public function merchant($v) + { + if (! $v) { + $this->whereNull('merchant_id'); + } else { + $this->where('merchant_id', $v); + } + } + + public function recommend($v) + { + $this->where('is_recommend', $v); + } + + public function category($v) + { + $this->where('category_id', $v); + } + + public function sort($v) + { + $this->orderBy($v, request('sort_by', 'asc')); + } + + public function state($state): GoodsFilter + { + return match ((int) $state) { + default => $this->where('on_sale', true), + 2 => $this->where('check_status', CheckStatus::Processing), + 3 => $this->where('on_sale', false) + }; + } +} diff --git a/src/Form/Attr.php b/src/Form/Attr.php new file mode 100644 index 0000000..fd3084d --- /dev/null +++ b/src/Form/Attr.php @@ -0,0 +1,41 @@ + [], + 'keys' => [], + 'type' => null, + ]; + + public function header(array $headers) + { + $this->addVariables([ + 'headers' => $headers, + ]); + + return $this; + } + + public function keys($keys) + { + $this->addVariables([ + 'keys' => $keys, + ]); + + return $this; + } + + public function type($type) + { + $this->addVariables(['type' => $type]); + + return $this; + } +} diff --git a/src/Form/Check/HandleCheckForm.php b/src/Form/Check/HandleCheckForm.php new file mode 100644 index 0000000..7c81cff --- /dev/null +++ b/src/Form/Check/HandleCheckForm.php @@ -0,0 +1,38 @@ +payload['id']); + try { + DB::beginTransaction(); + GoodsService::make()->handleCheck($info, Admin::user(), (bool) $input['check_status'], $input['check_remarks']); + DB::commit(); + + return $this->response()->success('操作成功')->refresh(); + } catch (\Exception $e) { + DB::rollBack(); + + return $this->response()->error($e->getMessage()); + } + } + + public function form() + { + $this->radio('check_status')->options([1 => '通过', 0 => '不通过'])->default(1); + $this->textarea('check_remarks'); + } +} diff --git a/src/Form/Goods/AttrForm.php b/src/Form/Goods/AttrForm.php new file mode 100644 index 0000000..fb5c298 --- /dev/null +++ b/src/Form/Goods/AttrForm.php @@ -0,0 +1,32 @@ +payload['goods_id']); + $goods->update(['attr' => json_decode($input['attr'])]); + + return $this->response()->success('保存成功'); + } + + public function form() + { + $attr = $this->model()->type?->attr; + $this->spec('attr')->header(['分组', '名称', '属性值'])->type($attr); + } + + protected function renderResetButton() + { + return ' 返回'; + } +} diff --git a/src/Form/Goods/PartForm.php b/src/Form/Goods/PartForm.php new file mode 100644 index 0000000..3add385 --- /dev/null +++ b/src/Form/Goods/PartForm.php @@ -0,0 +1,44 @@ +payload['goods_id']); + $part = json_decode($input['part'], true); + foreach ($part as &$item) { + foreach ($item['values'] as &$subItem) { + } + $subItem['value'] = floatval($subItem['value']); + } + $goods->update(['part' => $part]); + + return $this->response()->success('保存成功'); + } + + public function form() + { + $part = $this->model()->type?->part; + + $this->spec('part')->header(['名称', '可选值', '价格'])->type($part); + } + + protected function renderResetButton() + { + return ' 返回'; + } + + protected function getSubmitButtonLabel() + { + return '保存'; + } +} diff --git a/src/Form/Goods/SpecForm.php b/src/Form/Goods/SpecForm.php new file mode 100644 index 0000000..359636f --- /dev/null +++ b/src/Form/Goods/SpecForm.php @@ -0,0 +1,44 @@ +payload['goods_id']); + $spec = json_decode($input['spec'], true) ?: []; + foreach ($spec as &$item) { + foreach ($item['values'] as &$subItem) { + $subItem['value'] = floatval($subItem['value']); + } + } + $goods->update(['spec' => $spec]); + + return $this->response()->success('保存成功'); + } + + public function form() + { + $spec = $this->model()->type?->spec; + + $this->spec('spec')->header(['名称', '可选值', '价格'])->type($spec); + } + + protected function renderResetButton() + { + return ' 返回'; + } + + protected function getSubmitButtonLabel() + { + return '保存'; + } +} diff --git a/src/Form/GoodsType/AttrForm.php b/src/Form/GoodsType/AttrForm.php new file mode 100644 index 0000000..a54417f --- /dev/null +++ b/src/Form/GoodsType/AttrForm.php @@ -0,0 +1,36 @@ +payload['type_id']); + $info->update(['attr' => json_decode($input['attr'])]); + + return $this->response()->success('保存成功'); + } + + public function form() + { + $this->fill(['attr' => $this->payload['attr']]); + + $this->attr('attr')->header(['分组', '名称'])->keys(['name']); + + $reset = data_get($this->payload, 'reset', true); + $this->resetButton((bool) $reset); + } + + protected function renderResetButton() + { + return (! empty($this->buttons['reset'])) ? ' 返回' : ''; + } +} diff --git a/src/Form/GoodsType/PartForm.php b/src/Form/GoodsType/PartForm.php new file mode 100644 index 0000000..7efd84c --- /dev/null +++ b/src/Form/GoodsType/PartForm.php @@ -0,0 +1,36 @@ +payload['type_id']); + $info->update(['part' => json_decode($input['part'])]); + + return $this->response()->success('保存成功'); + } + + public function form() + { + $this->fill(['part' => $this->payload['part']]); + + $this->attr('part')->header(['名称', '可选值'])->keys(['name']); + + $reset = data_get($this->payload, 'reset', true); + $this->resetButton((bool) $reset); + } + + protected function renderResetButton() + { + return (! empty($this->buttons['reset'])) ? ' 返回' : ''; + } +} diff --git a/src/Form/GoodsType/SpecForm.php b/src/Form/GoodsType/SpecForm.php new file mode 100644 index 0000000..527129d --- /dev/null +++ b/src/Form/GoodsType/SpecForm.php @@ -0,0 +1,36 @@ +payload['type_id']); + $info->update(['spec' => json_decode($input['spec'])]); + + return $this->response()->success('保存成功'); + } + + public function form() + { + $this->fill(['spec' => $this->payload['spec']]); + + $this->attr('spec')->header(['名称', '可选值'])->keys(['name']); + + $reset = data_get($this->payload, 'reset', true); + $this->resetButton((bool) $reset); + } + + protected function renderResetButton() + { + return (! empty($this->buttons['reset'])) ? ' 返回' : ''; + } +} diff --git a/src/Form/Spec.php b/src/Form/Spec.php new file mode 100644 index 0000000..6206f3c --- /dev/null +++ b/src/Form/Spec.php @@ -0,0 +1,39 @@ + [], + 'type' => null, + ]; + + public function header(array $headers) + { + $this->addVariables([ + 'headers' => $headers, + ]); + + return $this; + } + + public function type($type) + { + $this->addVariables(['type' => $type]); + + return $this; + } + + protected function formatFieldData($data) + { + // 获取到当前字段值 + $value = parent::formatFieldData($data); + + return $value ?: []; + } +} diff --git a/src/GoodsService.php b/src/GoodsService.php new file mode 100644 index 0000000..bc5028d --- /dev/null +++ b/src/GoodsService.php @@ -0,0 +1,146 @@ +id)->delete(); + } + + /** + * 根据规格生成SKU + * + * @param Goods $goods 商品 + * @param array $options {spec: 指定规格, price: 基础价格, name: 基础名称, stock: 默认库存, name_add: 是否在名称上面追加属性值, price_add: 是否在价格上面追加属性的加价} + */ + public function generateSku(Goods $goods, $options = []) + { + $spec = data_get($options, 'spec', $goods->spec); + $price = data_get($options, 'price', $goods->price); + $vipPrice = data_get($options, 'vip_price', $goods->vip_price); + + $weight = $goods->weight; + $volume = $goods->volume; + $shippingTmpId = data_get($options, 'shipping_tmp_id', $goods->shipping_tmp_id); + + $name = data_get($options, 'name', $goods->name); + $stock = data_get($options, 'stock', $goods->stock); + $nameAdd = (bool) data_get($options, 'name_add', false); + $priceAdd = (bool) data_get($options, 'price_add', false); + if ($spec) { + $specList = []; + foreach ($spec as $item) { + $items = []; + foreach ($item['values'] as $value) { + array_push($items, [ + 'name' => $item['name'], + 'value' => $value['name'], + 'price' => floatval($value['value']), + ]); + } + array_push($specList, $items); + } + $cartesianList = Arr::crossJoin(...$specList); + foreach ($cartesianList as $items) { + $specPrice = $priceAdd ? $price + array_sum(array_column($items, 'price')) : $price; + $specVipPrice = $priceAdd ? $vipPrice + array_sum(array_column($items, 'price')) : $vipPrice; + $specName = $nameAdd ? $name.' '.implode(' ', array_column($items, 'value')) : $name; + $exists = $goods->skus()->jsonArray($items)->exists(); + $attributes = [ + 'name' => $specName, + 'price' => $specPrice, + 'vip_price' => $specVipPrice, + 'stock' => $stock, + 'spec' => $items, + 'weight' => $weight, + 'volume' => $volume, + 'shipping_tmp_id' => $shippingTmpId, + ]; + if ($exists) { + $goods->skus()->jsonArray($items)->update($attributes); + } else { + $attributes['sn'] = $this->generateSn(); + $goods->skus()->create($attributes); + } + } + } + } + + /** + * 申请审核 + * + * @param Goods $goods + * @param Administrator $user + * @return GoodsCheck + */ + public function submitCheck(Goods $goods, Administrator $user = null) + { + $goods->update([ + 'check_status' => CheckStatus::Processing, + ]); + + $attributes = Arr::except($goods->toArray(), ['check_status', 'check_remarks', 'check_at', 'check_user_id']); + $attributes['check_status'] = CheckStatus::Processing; + + $check = $goods->checkLogs()->create($attributes); + + // 如果当前用户拥有审核权限, 则自动通过审核 + $user = $user ?: Admin::user(); + if ($user->can('dcat.admin.goods.check')) { + $this->handleCheck($check, $user, true, '自动通过审核'); + } + + return $check; + } + + /** + * 审核商品 + * + * @param GoodsCheck $check + * @param Administrator $admin + * @param bool $status true: 通过, false: 不通过 + * @param string $remarks 审核备注 + */ + public function handleCheck(GoodsCheck $check, Administrator $admin, bool $status, string $remarks = null) + { + if ($check->check_status === CheckStatus::Success || $check->check_status === CheckStatus::Fail) { + throw new BizException('已经审核过了'); + } + $goods = $check->goods; + if ($goods->check_status !== CheckStatus::Processing) { + throw new BizException('商品未申请审核'); + } + + $attributes = [ + 'check_status' => $status ? CheckStatus::Success : CheckStatus::Fail, + 'check_at' => now(), + 'check_remarks' => $remarks, + 'check_user_id' => $admin->id, + ]; + $check->update($attributes); + + $goods->update(array_merge($attributes, ['on_sale' => 1])); + } +} diff --git a/src/GoodsServiceProvider.php b/src/GoodsServiceProvider.php new file mode 100644 index 0000000..998d1e1 --- /dev/null +++ b/src/GoodsServiceProvider.php @@ -0,0 +1,32 @@ +loadRoutesFrom(__DIR__.'/../routes/admin.php'); + $this->loadRoutesFrom(__DIR__.'/../routes/api.php'); + + // $this->loadMigrationsFrom(__DIR__.'/../database/'); + + $this->loadViewsFrom(__DIR__.'/../views', 'dcat-admin-goods'); + + $this->publishes([ + __DIR__.'/../assets' => public_path('vendor/dcat-admin-goods'), + ], 'dcat-admin-goods-assets'); + + $this->publishes([ + __DIR__.'/../database/' => database_path('migrations'), + ], 'dcat-admin-goods-migrations'); + + $this->loadTranslationsFrom(__DIR__.'/../lang', 'dcat-admin-goods'); + } +} diff --git a/src/Http/Admin/GoodsBrandController.php b/src/Http/Admin/GoodsBrandController.php new file mode 100644 index 0000000..c5d4d5d --- /dev/null +++ b/src/Http/Admin/GoodsBrandController.php @@ -0,0 +1,53 @@ +disableRowSelector(); + + $grid->disableViewButton(); + + $grid->column('name'); + $grid->column('image')->image('', 120); + }); + } + + protected function form() + { + return Form::make(new GoodsBrand(), function (Form $form) { + $form->text('name'); + $form->image('image') + ->autoUpload() + ->saveFullUrl() + ->move('goods/brand'); + + $form->disableResetButton(); + $form->disableCreatingCheck(); + $form->disableViewCheck(); + $form->disableEditingCheck(); + + $form->deleting(function (Form $form) { + $data = $form->model()->toArray(); + foreach ($data as $item) { + $id = data_get($item, 'id'); + // 品牌下面包含商品, 阻止删除 + if (Goods::where('brand_id', $id)->exists()) { + return $form->response()->error('请先删除关联的商品'); + } + } + }); + }); + } +} diff --git a/src/Http/Admin/GoodsCategoryController.php b/src/Http/Admin/GoodsCategoryController.php new file mode 100644 index 0000000..84b1cc5 --- /dev/null +++ b/src/Http/Admin/GoodsCategoryController.php @@ -0,0 +1,63 @@ +disableRowSelector(); + + $grid->column('name')->tree(true, false); + $grid->column('image')->image('', 100); + $grid->column('sort')->editable(['mask' => '{alias:\'numeric\',min:0,max:999}']); + $grid->column('is_enable')->switch(); + + return $grid; + } + + protected function form() + { + $form = Form::make(GoodsCategory::with(['parent'])); + + $form->select('parent_id')->help('不选默认为顶级')->options(GoodsCategory::selectOptions())->default(0); + + $form->text('name')->required(); + $form->image('image') + ->uniqueName() + ->move('goods/category') + ->autoUpload(); + $form->number('sort') + ->min(0) + ->default(1) + ->help('数值越小, 越靠前'); + $form->switch('is_enable')->default(1); + $form->text('description'); + + return $form; + } + + protected function detail($id) + { + $info = GoodsCategory::with(['parent'])->findOrFail($id); + $show = Show::make($info); + + $show->field('name'); + $show->field('parent.name'); + $show->field('image')->image('', 100); + $show->field('description'); + $show->field('sort'); + + return $show; + } +} diff --git a/src/Http/Admin/GoodsCheckController.php b/src/Http/Admin/GoodsCheckController.php new file mode 100644 index 0000000..9d9d777 --- /dev/null +++ b/src/Http/Admin/GoodsCheckController.php @@ -0,0 +1,75 @@ +model()->sort(); + + $grid->column('merchant.name'); + $grid->column('category.name'); + $grid->column('name')->display(function () { + return ($this->cover_image ? ' ' : '').''.$this->name.''; + }); + $grid->column('price'); + $grid->column('vip_price'); + $grid->column('check_status')->display(fn () => $this->check_status->dot()); + $grid->column('created_at', '申请时间'); + + $grid->showViewButton(); + $grid->actions(function (Actions $actions) { + $row = $actions->row; + if ($row->check_status === CheckStatus::Processing) { + $actions->append(new RowHandleCheck()); + } + }); + }); + } + + protected function detail($id) + { + Admin::css([ + 'vendor/dcat-admin-goods/goods.css', + ]); + $info = GoodsCheck::with(['category', 'brand', 'type'])->findOrFail($id); + $show = Show::make($info); + $show->field('goods_sn'); + $show->field('category.name'); + $show->field('brand.name'); + $show->field('type.name'); + $show->field('name'); + $show->field('price'); + $show->field('vip_price'); + $show->field('score_discount_amount'); + $show->field('cover_image')->image('', 100); + $show->field('images')->image('', 100); + $show->field('content')->image(''); + $show->field('spec')->view('dcat-admin-goods::goods.grid-attr'); + $show->field('attr')->view('dcat-admin-goods::goods.grid-attr'); + $show->field('part')->view('dcat-admin-goods::goods.grid-attr'); + $show->field('on_sale')->bool(); + $show->field('sold_count'); + $show->field('check_status')->unescape()->as(fn () => $this->check_status->label()); + $show->field('check_at'); + $show->field('check_remarks'); + $show->field('check_user.name'); + $show->field('created_at')->as(fn ($v) => $this->created_at->format('Y-m-d H:i:s')); + $show->field('updated_at')->as(fn ($v) => $this->updated_at->format('Y-m-d H:i:s')); + + return $show; + } +} diff --git a/src/Http/Admin/GoodsController.php b/src/Http/Admin/GoodsController.php new file mode 100644 index 0000000..b34eaef --- /dev/null +++ b/src/Http/Admin/GoodsController.php @@ -0,0 +1,288 @@ +all()); + + $query->select(['id', 'name as text']); + + if ($request->filled('_paginate') || $request->filled('amp;_paginate')) { + $list = $query->paginate(); + } else { + $list = $query->get(); + } + + return $list; + } + + public function attr($goods, Content $content) + { + $goods = Goods::with(['type'])->findOrFail($goods); + $form = AttrForm::make([ + 'type' => $goods->type, + 'attr' => $goods->attr, + ])->payload(['type_id' => $goods->type?->id, 'goods_id' => $goods->id])->appendHtmlAttribute('class', 'bg-white'); + + return $content + ->translation($this->translation()) + ->title($goods->name) + ->description($goods->type?->name) + ->body($form); + } + + public function spec($goods, Content $content) + { + $goods = Goods::findOrFail($goods); + $form = SpecForm::make([ + 'type' => $goods->type, + 'spec' => $goods->spec, + ])->payload(['type_id' => $goods->type_id, 'goods_id' => $goods->id])->appendHtmlAttribute('class', 'bg-white'); + + return $content + ->translation($this->translation()) + ->title($goods->name) + ->description($goods->type?->name) + ->body($form); + } + + public function part($goods, Content $content) + { + $goods = Goods::findOrFail($goods); + $form = PartForm::make([ + 'type' => $goods->type, + 'part' => $goods->part, + ])->payload(['type_id' => $goods->type_id, 'goods_id' => $goods->id])->appendHtmlAttribute('class', 'bg-white'); + + return $content + ->translation($this->translation()) + ->title($goods->name) + ->description($goods->type?->name) + ->body($form); + } + + protected function grid() + { + return Grid::make(Goods::with(['category', 'brand', 'type', 'merchant', 'skus']), function (Grid $grid) { + $grid->model()->sort(); + + $grid->selector(function (Selector $selector) { + $brands = GoodsBrand::get(); + $types = GoodsType::get(); + $categories = GoodsCategory::where('level', 3)->where('path', 'like', '-1-%')->get(); + $prices = ['0-999', '1000-1999', '2000-4999', '5000+']; + $merchants = Merchant::checked()->sort()->get(); + $selector->selectOne('merchant_id', __('dcat-admin-goods::goods.fields.merchant_id'), $merchants->pluck('name', 'id')); + $selector->selectOne('category_id', __('dcat-admin-goods::goods.fields.category_id'), $categories->pluck('name', 'id')); + $selector->selectOne('brand_id', __('dcat-admin-goods::goods.fields.brand_id'), $brands->pluck('name', 'id')); + $selector->selectOne('type_id', __('dcat-admin-goods::goods.fields.type_id'), $types->pluck('name', 'id')); + $selector->selectOne('price', __('dcat-admin-goods::goods.fields.price'), $prices, function ($q, $value) use ($prices) { + $parsePrice = data_get($prices, $value); + if ($parsePrice) { + $parts = explode('-', $parsePrice); + $parts = array_map(fn ($v) => (int) $v, $parts); + if (count($parts) > 1) { + $q->whereBetween('price', $parts); + } else { + $q->where('price', '>', $parts[0]); + } + } + }); + }); + + $grid->column('merchant.name'); + $grid->column('goods_sn'); + $grid->column('category.name'); + $grid->column('brand.name'); + $grid->column('type.name')->label(); + $grid->column('name')->display(function () { + return ($this->cover_image ? ' ' : '').''.$this->name.''; + }); + $grid->column('price'); + $grid->column('vip_price'); + $grid->column('stock') + ->if(fn () => $this->skus->count() > 0) + ->display(fn () => $this->skus->sum('stock')) + ->else() + ->editable(); + $grid->column('on_sale')->bool(); + $grid->column('is_recommend')->switch(); + $grid->column('check_status')->display(fn () => $this->check_status->dot()); + $grid->column('sold_count'); + + $grid->disableRowSelector(); + $grid->createMode(Grid::CREATE_MODE_DEFAULT); + + $user = Admin::user(); + + $grid->showCreateButton($user->can('dcat.admin.goods.create')); + + $grid->actions(function (Actions $actions) use ($user) { + $row = $actions->row; + + $actions->view($user->can('dcat.admin.goods.show')); + + if ($user->can('dcat.admin.goods.edit') && ! $row->on_sale && $row->check_status !== CheckStatus::Processing) { + $actions->edit(); + $actions->append('属性介绍'); + $actions->append('商品规格'); + $actions->append('商品配件'); + } + + if ($row->spec) { + $actions->append('货品列表'); + } + + if ($user->can('dcat.admin.goods.edit') && $row->check_status === CheckStatus::Success) { + $actions->append(new RowGoodsSale()); + } + + $actions->delete($user->can('dcat.admin.goods.destroy') && ! $row->on_sale); + + if (! $row->on_sale && $row->check_status !== CheckStatus::Success && $row->check_status !== CheckStatus::Processing) { + $actions->append(new RowSubmitCheck()); + } + }); + }); + } + + protected function detail($id) + { + Admin::css([ + 'vendor/dcat-admin-goods/goods.css', + ]); + $info = Goods::with(['category', 'merchant', 'brand', 'type', 'checkUser'])->findOrFail($id); + $show = Show::make($info); + $show->field('merchant.name'); + $show->field('goods_sn'); + $show->field('category.name'); + $show->field('brand.name'); + $show->field('type.name'); + $show->field('name'); + $show->field('price'); + $show->field('vip_price'); + $show->field('score_max_amount'); + $show->field('cover_image')->image('', 100); + $show->field('images')->image('', 100); + $show->field('content')->image(''); + $show->field('spec')->view('dcat-admin-goods::goods.grid-attr'); + $show->field('attr')->view('dcat-admin-goods::goods.grid-attr'); + $show->field('part')->view('dcat-admin-goods::goods.grid-attr'); + $show->field('on_sale')->bool(); + $show->field('is_recommend')->bool(); + $show->field('sold_count'); + $show->field('check_status')->unescape()->as(fn () => $this->check_status->label()); + $show->field('check_at'); + $show->field('check_remarks'); + $show->field('check_user.name'); + $show->field('created_at')->as(fn ($v) => $this->created_at->format('Y-m-d H:i:s')); + $show->field('updated_at')->as(fn ($v) => $this->updated_at->format('Y-m-d H:i:s')); + + return $show; + } + + protected function form() + { + return Form::make(Goods::with(['merchant']), function (Form $form) { + $model = $form->model(); + $isCreating = $form->isCreating(); + $unique = Rule::unique('goods', 'goods_sn'); + + if ($isCreating) { + // $form->select('type_id')->options(GoodsType::pluck('name', 'id')); + $form->select('merchant_id')->ajax('api/merchants?_paginate=1'); + } else { + // $type = $model->type_id ? GoodsType::find($model->type_id) : null; + // $form->display('type_id')->with(fn () => $model->type_id ? $type->name : ''); + $form->display('merchant.name', __('dcat-admin-goods::goods.fields.merchant_id')); + $unique->ignore($model->id); + } + $form->select('category_id')->options(GoodsCategory::selectOptions(null, false))->required(); + // $form->select('brand_id')->options(GoodsBrand::pluck('name', 'id')); + $form->text('name')->required(); + $form->text('goods_sn')->rules([$unique], [ + 'unique' => '商品编号已经存在', + ]); + $form->image('cover_image') + ->autoUpload() + ->saveFullUrl() + ->move('goods/cover-image') + ->required(); + $form->multipleImage('images') + ->autoUpload() + ->saveFullUrl() + ->move('goods/images'); + $form->multipleImage('content') + ->autoUpload() + ->saveFullUrl() + ->move('goods/content'); + + if ($isCreating || !$model->spec) { + $form->number('price')->min(0)->attribute('step', 0.01); + $form->number('vip_price')->min(0)->attribute('step', 0.01); + + $discountRatio = Setting::where('slug', 'discount_profit_ratio')->value('value'); + $form->number('score_max_amount')->min(0)->help('购买商品时, 允许使用多少积分, 积分抵扣比例: 1 积分 = '.$discountRatio.' 元'); + + $form->currency('weight')->default(0)->symbol('克'); + $form->currency('volume')->default(0)->symbol('立方'); + + $form->select('shipping_tmp_id')->options(ShippingTmp::all()->pluck('name', 'id'))->help('运费模板,不选择默认免邮'); + } else { + $form->display('help', '提示')->value('商品其他信息, 请到 货品列表 去修改'); + } + + $form->hidden('stock')->default(0); + $form->hidden('is_recommend')->default(0); + $form->disableResetButton(); + $form->disableCreatingCheck(); + $form->disableViewCheck(); + $form->disableEditingCheck(); + + $form->creating(function (Form $form) { + if (! $form->goods_sn) { + $form->goods_sn = GoodsService::make()->generateSn(); + } + }); + + $form->deleting(function (Form $form) { + $data = $form->model()->toArray(); + // 删除 SKU + GoodsSku::whereIn('goods_id', array_column($data, 'id'))->delete(); + }); + }); + } +} diff --git a/src/Http/Admin/GoodsSkuController.php b/src/Http/Admin/GoodsSkuController.php new file mode 100644 index 0000000..ec3a7a1 --- /dev/null +++ b/src/Http/Admin/GoodsSkuController.php @@ -0,0 +1,279 @@ +model()->where('goods_id', $goods->id); + + $grid->selector(function (Selector $selector) use ($goods) { + $specs = $goods->spec; + if ($specs) { + foreach ($specs as $key => $item) { + $values = array_column($item['values'], 'name'); + $selector->selectOne('spec_'.$key, $item['name'], array_column($item['values'], 'name'), function ($q, $value) use ($values, $item) { + $selected = array_values(Arr::only($values, $value)); + if (count($selected) > 0) { + $q->jsonArray([['name' => $item['name'], 'value' => $selected[0]]]); + } + }); + } + } + }); + + $grid->column('id'); + $grid->column('sn'); + $grid->column('name'); + // $grid->column('reset')->display(fn() => $goods->price); + $grid->column('price'); + $grid->column('stock'); + if ($goods->spec) { + foreach ($goods->spec as $key => $item) { + $grid->column('spec_'.$key, $item['name'])->display(function () use ($item) { + $filtered = current(array_filter($this->spec, fn ($subItem) => $subItem['name'] === $item['name'])); + $value = data_get($filtered, 'value'); + $price = data_get($filtered, 'price'); + + return ''.$value.''; + }); + } + } + // $grid->column('spec')->view('dcat-admin-goods::grid.attr'); + + $user = Admin::user(); + $grid->showCreateButton($user->can('dcat.admin.goods_sku.create')); + $grid->showDeleteButton($user->can('dcat.admin.goods_sku.destroy')); + $grid->showEditButton($user->can('dcat.admin.goods_sku.edit')); + }); + + return $content + ->translation($this->translation) + ->title($goods->name) + ->description(admin_trans_label()) + ->body($grid); + } + + public function show($goods, $id, Content $content) + { + $goods = Goods::findOrFail($goods); + $info = GoodsSku::findOrFail($id); + $show = Show::make($info, function (Show $show) { + $show->field('sn'); + $show->field('name'); + $show->field('price'); + $show->field('vip_price'); + $show->field('stock'); + $show->field('spec')->view('dcat-admin-goods::grid.spec'); + }); + + return $content + ->translation($this->translation) + ->title(admin_trans_label()) + ->description(trans('admin.show')) + ->body($show); + } + + protected function editForm($goods) + { + return Form::make(new GoodsSku(), function (Form $form) use ($goods) { + $unqiue = Rule::unique('goods_sku', 'sn')->ignore($form->model()->id); + $form->text('sn')->rules([$unqiue], ['unique' => '货号已经存在'])->required(); + $form->text('name')->default($goods->name); + $form->number('price')->min(0)->attribute('step', 0.01)->default($goods->price); + $form->number('vip_price')->min(0)->attribute('step', 0.01)->default($goods->vip_price); + $discountRatio = Setting::where('slug', 'discount_profit_ratio')->value('value'); + $form->number('score_max_amount')->min(0)->help('购买商品时, 允许使用多少积分, 积分抵扣比例: 1 积分 = '.$discountRatio.' 元'); + + $form->currency('weight')->default(0)->symbol('克')->saving(fn ($v) => ! empty($v) ?? null); + $form->currency('volume')->default(0)->symbol('立方')->saving(fn ($v) => ! empty($v) ?? null); + $form->select('shipping_tmp_id')->options(ShippingTmp::all()->pluck('name', 'id'))->help('运费模板,不选择默认免邮'); + + $form->number('stock')->min(0)->default($goods->stock); + $form->hidden('spec')->customFormat(fn ($v) => json_encode($v)); + $form->hidden('goods_id')->default($goods->id); + + $spec = $form->model()->spec; + if ($goods->spec) { + foreach ($goods->spec as $item) { + $values = array_column($item['values'], 'name', 'name'); + $value = null; + if ($spec) { + $filtered = current(array_filter($spec, fn ($subItem) => $subItem['name'] === $item['name'])); + $value = array_search($filtered['value'], $values); + } + $form->radio($item['name'], $item['name'])->options($values)->value($value); + } + } + + $form->saving(function (Form $form) use ($goods) { + $info = $form->model(); + $spec = []; + if ($goods->spec) { + foreach ($goods->spec as $item) { + array_push($spec, ['name' => $item['name'], 'value' => $form->input($item['name'])]); + $form->deleteInput($item['name']); + } + } + $form->input('spec', $spec); + $query = GoodsSku::where('goods_id', $goods->id)->jsonArray($spec); + if ($form->isEditing()) { + $query->where('id', '!=', $info->id); + } + if ($query->exists()) { + return $form->response()->error('该规格已经存在'); + } + }); + + $form->disableCreatingCheck(); + $form->disableEditingCheck(); + $form->disableViewCheck(); + $form->disableResetButton(); + }); + } + + public function edit($goods, $id, Content $content) + { + $goods = Goods::findOrFail($goods); + + return $content + ->translation($this->translation) + ->title(admin_trans_label()) + ->description(trans('admin.edit')) + ->body($this->editForm($goods)->edit($id)); + } + + public function create($goods, Content $content) + { + $goods = Goods::findOrFail($goods); + + return $content + ->translation($this->translation) + ->title(__('dcat-admin-goods::goods-sku.labels.sku')) + ->description(__('dcat-admin-goods::goods-sku.labels.create')) + ->body($this->createForm($goods)); + } + + protected function createForm($goods) + { + return Form::make(new GoodsSku(), function (Form $form) use ($goods) { + $form->text('name')->default($goods->name); + if ($goods->spec) { + $form->checkbox('name_append', '')->options([1 => '是否在名称上面追加属性值']); + } + $form->number('price')->min(0)->default($goods->price); + if ($goods->spec) { + $form->checkbox('price_append', '')->options([1 => '是否在价格上面追加属性的加价'])->default([1]); + } + + $discountRatio = Setting::where('slug', 'discount_profit_ratio')->value('value'); + $form->number('score_max_amount')->min(0)->help('购买商品时, 允许使用多少积分, 积分抵扣比例: 1 积分 = '.$discountRatio.' 元'); + + $form->select('shipping_tmp_id')->default($goods->shipping_tmp_id ?? 0)->options(ShippingTmp::all()->pluck('name', 'id'))->help('运费模板,不选择默认免邮'); + + $form->number('stock')->min(0)->default($goods->stock); + + if ($goods->spec) { + foreach ($goods->spec as $item) { + $values = array_column($item['values'], 'name', 'name'); + $form->checkbox($item['name'], $item['name'])->options($values); + // $form->embeds('input_'.$item['name'], '自定义' . $item['name'], function (EmbeddedForm $form) use ($item) { + // $form->text('', '属性值')->setElementName('input_'.$item['name'].'_name'); + // $form->text('', '价格')->setElementName('input_'.$item['name'].'_price')->default(0); + // }); + } + + $form->checkbox('clear', '')->options([1 => '清空现有的货品'])->default(1); + } + + $form->disableCreatingCheck(); + $form->disableEditingCheck(); + $form->disableViewCheck(); + $form->disableResetButton(); + + $form->saving(function (Form $form) use ($goods) { + $price = $form->price; + $name = $form->name; + $spec = []; + if ($goods->spec) { + $goodsSpecs = $goods->spec; + foreach ($goodsSpecs as &$item) { + // 自定义属性 + $add = ['name' => '', 'value' => '']; + if ($form->input('input_'.$item['name'].'_name')) { + $add = ['name' => $form->input('input_'.$item['name'].'_name'), 'value' => round($form->input('input_'.$item['name'].'_value'), 2, PHP_ROUND_HALF_DOWN)]; + } + + $values = $form->input($item['name']); + // 过滤空值 + $values = array_filter($values, fn ($v) => $v); + // 补充自定义属性 + if ($add['name']) { + array_push($item['values'], $add); + array_push($values, $add['name']); + } + if (count($values) === 0) { + return $form->response()->error('请勾选 '.$item['name']); + } + array_push($spec, ['name' => $item['name'], 'values' => array_filter($item['values'], fn ($v) => in_array($v['name'], $values))]); + $form->deleteInput($item['name']); + } + $goods->update(['spec' => $goodsSpecs]); + } + $service = GoodsService::make(); + if ($form->clear) { + $service->clearSku($goods); + } + $service->generateSku($goods, [ + 'spec' => $spec, + 'price' => $price, + 'name' => $name, + 'name_add' => (bool) data_get($form->name_append, 0), + 'price_add' => (bool) data_get($form->price_append, 0), + 'stock' => $form->stock, + 'shipping_tmp_id' => $form->shipping_tmp_id ?? null, + ]); + + return $form->response()->success('添加成功')->redirect(admin_route('goods_sku.index', ['goods' => $goods->id])); + }); + }); + } + + public function update($goods, $id) + { + return $this->editForm(Goods::findOrFail($goods))->update($id); + } + + public function store($goods) + { + return $this->createForm(Goods::findOrFail($goods))->store(); + } + + public function destroy($goods, $id) + { + GoodsSku::where('id', $id)->delete(); + + return $this->editForm($goods)->response()->success('删除成功'); + } +} diff --git a/src/Http/Admin/GoodsTypeController.php b/src/Http/Admin/GoodsTypeController.php new file mode 100644 index 0000000..ab4d0ee --- /dev/null +++ b/src/Http/Admin/GoodsTypeController.php @@ -0,0 +1,79 @@ +disableRowSelector(); + $grid->disableViewButton(); + $grid->disableEditButton(); + + $grid->column('name')->editable(); + $grid->column('attr')->display(fn () => view('dcat-admin-goods::goods-type.grid-attr', ['value' => $this->attr]))->modal(__('dcat-admin-goods::goods-type.fields.attr'), function (Modal $modal) { + $modal->icon(''); + + return AttrForm::make()->payload(['type_id' => $this->id, 'reset' => false, 'attr' => $this->attr]); + }); + $grid->column('spec')->display(fn () => view('dcat-admin-goods::goods-type.grid-attr', ['value' => $this->spec]))->modal(__('dcat-admin-goods::goods-type.fields.spec'), function (Modal $modal) { + $modal->icon(''); + + return SpecForm::make()->payload(['type_id' => $this->id, 'reset' => false, 'spec' => $this->spec]); + }); + $grid->column('part')->display(fn () => view('dcat-admin-goods::goods-type.grid-attr', ['value' => $this->part]))->modal(__('dcat-admin-goods::goods-type.fields.part'), function (Modal $modal) { + $modal->icon(''); + + return PartForm::make()->payload(['type_id' => $this->id, 'reset' => false, 'part' => $this->part]); + }); + }); + } + + protected function form() + { + return Form::make(new GoodsType(), function (Form $form) { + $form->text('name'); + + // $form->textarea('attr'); + // $form->textarea('spec'); + // $form->textarea('part'); + + $form->disableResetButton(); + $form->disableCreatingCheck(); + $form->disableViewCheck(); + $form->disableEditingCheck(); + + $form->deleting(function (Form $form) { + $data = $form->model()->toArray(); + foreach ($data as $item) { + $id = data_get($item, 'id'); + // 下面包含商品, 阻止删除 + if (Goods::where('type_id', $id)->exists()) { + return $form->response()->error('请先删除关联的商品'); + } + } + }); + }); + } +} diff --git a/src/Http/Api/GoodsCartController.php b/src/Http/Api/GoodsCartController.php new file mode 100644 index 0000000..969ebcf --- /dev/null +++ b/src/Http/Api/GoodsCartController.php @@ -0,0 +1,124 @@ +user(); + + $query = $user->carts(); + + if ($request->filled('merchant_id')) { + $id = $request->input('merchant_id'); + if ($id) { + $query->where('merchant_id', $id); + } else { + $query->whereNull('merchant_id'); + } + } + + $list = $query->get(); + + return $this->json(GoodsCartResource::collection($list)); + } + + public function groupByMerchant() + { + $user = auth('api')->user(); + $list = $user->carts()->get()->groupBy('merchant_id'); + + $data = []; + foreach ($list as $id => $items) { + $subData = GoodsCartResource::collection($items); + $merchant = $id ? MerchantTinyResource::make(Merchant::findOrFail($id)) : ['id' => '', 'name' => '自营店铺']; + array_push($data, [ + 'merchant' => $merchant, + 'list' => $subData, + ]); + } + + return $this->json($data); + } + + public function store(Request $request) + { + $request->validate([ + 'goods_id' => 'required', + ]); + $goods = Goods::show()->findOrFail($request->input('goods_id')); + $useSku = $goods->spec && count($goods->spec) > 0; + if ($useSku && ! $request->filled('spec')) { + return $this->error('商品属性必选'); + } + + $amount = $request->input('amount', 1); + $user = auth('api')->user(); + + $sku = $useSku ? $goods->skus()->jsonArray($request->input('spec'))->first() : null; + if ($useSku && ! $sku) { + return $this->error('商品Sku 不存在'); + } + + $info = $user->carts()->where('goods_id', $goods->id)->when($useSku, fn ($q) => $q->where('goods_sku_sn', $sku->sn))->first(); + if ($info) { + $info->increment('amount', $amount, [ + 'goods_sku_id' => $sku ? $sku->id : null, + ]); + } else { + $info = $user->carts()->create([ + 'goods_id' => $goods->id, + 'goods_sn' => $goods->goods_sn, + 'merchant_id' => $goods->merchant_id, + 'amount' => $amount, + 'goods_sku_id' => $sku ? $sku->id : null, + 'goods_sku_sn' => $sku ? $sku->sn : null, + 'goods_name' => $sku ? $sku->name : $goods->name, + 'cover_image' => $goods->cover_image, + 'price' => $sku ? $sku->price : $goods->price, + 'vip_price' => $sku ? $sku->vip_price : $goods->vip_price, + 'attr' => $goods->attr, + 'spec' => $sku ? $sku->spec : null, + 'part' => $request->input('part'), + ]); + } + + return $this->json(GoodsCartResource::make($info)); + } + + public function update($id, Request $request) + { + $request->validate([ + 'amount' => 'required', + ]); + + $user = auth('api')->user(); + + $info = $user->carts()->findOrFail($id); + + $info->update($request->only(['amount'])); + + return $this->json(GoodsCartResource::make($info)); + } + + public function destroy(Request $request) + { + $request->validate([ + 'id' => 'required|array', + ]); + + $user = auth('api')->user(); + + $user->carts()->whereIn('id', $request->input('id'))->delete(); + + return $this->success(); + } +} diff --git a/src/Http/Api/GoodsCategoryController.php b/src/Http/Api/GoodsCategoryController.php new file mode 100644 index 0000000..d2bd74e --- /dev/null +++ b/src/Http/Api/GoodsCategoryController.php @@ -0,0 +1,23 @@ + function ($q) { + $q->show()->sort(); + }, + ])->filter(['level' => 2])->show()->sort(); + + $list = $query->get(); + + return $this->json(GoodsCategoryResource::collection($list)); + } +} diff --git a/src/Http/Api/GoodsController.php b/src/Http/Api/GoodsController.php new file mode 100644 index 0000000..9b6e4f4 --- /dev/null +++ b/src/Http/Api/GoodsController.php @@ -0,0 +1,50 @@ +filter($request->all()); + + $list = $query->show()->sort()->paginate($request->input('per_page')); + + return $this->json(GoodsTinyResource::collection($list)); + } + + public function show($id) + { + $info = Goods::with(['merchant', 'skus'])->show()->findOrFail($id); + + return $this->json(GoodsResource::make($info)); + } + + public function skus($id, Request $request) + { + $info = Goods::show()->findOrFail($id); + + $query = $info->skus(); + + if ($request->filled('spec')) { + $spec = explode(',', $request->input('spec')); + $spec = array_map(function ($item) { + $ex = explode(':', $item); + + return ['name' => data_get($ex, 0), 'value' => data_get($ex, 1)]; + }, $spec); + $query->jsonArray($spec); + } + + $list = $query->get(); + + return $this->json(GoodsSkuResource::collection($list)); + } +} diff --git a/src/Http/Resources/GoodsCartResource.php b/src/Http/Resources/GoodsCartResource.php new file mode 100644 index 0000000..13eba26 --- /dev/null +++ b/src/Http/Resources/GoodsCartResource.php @@ -0,0 +1,38 @@ + $this->id, + 'user_id' => $this->user_id, + + 'merchant_id' => $this->merchant_id, + 'mercahnt' => MerchantTinyResource::make($this->whenLoaded('merchant')), + + 'goods_id' => $this->goods_id, + 'goods_sn' => $this->goods_sn, + 'goods' => GoodsTinyResource::make($this->whenLoaded('goods')), + + 'goods_sku_sn' => $this->goods_sku_sn, + 'goods_sku_id' => $this->goods_sku_id, + 'goods_sku' => GoodsSkuResource::make($this->whenLoaded('goodsSku')), + + 'goods_name' => $this->goods_name, + 'cover_image' => $this->cover_image, + 'attr' => $this->attr, + 'spec' => $this->spec, + 'part' => $this->part, + + 'price' => floatval($this->price), + 'vip_price' => floatval($this->vip_price), + 'amount' => $this->amount, + ]; + } +} diff --git a/src/Http/Resources/GoodsCategoryResource.php b/src/Http/Resources/GoodsCategoryResource.php new file mode 100644 index 0000000..e210d97 --- /dev/null +++ b/src/Http/Resources/GoodsCategoryResource.php @@ -0,0 +1,26 @@ + $this->id, + 'name' => $this->name, + 'image' => $this->image, + 'parent_id' => $this->parent_id, + 'level' => $this->level, + 'sort' => $this->sort, + 'path' => $this->path, + + 'children' => static::collection($this->whenLoaded('children')), + + 'type' => Str::startsWith($this->path, '-2-') ? 'shop' : '', + ]; + } +} diff --git a/src/Http/Resources/GoodsResource.php b/src/Http/Resources/GoodsResource.php new file mode 100644 index 0000000..9c7b91a --- /dev/null +++ b/src/Http/Resources/GoodsResource.php @@ -0,0 +1,29 @@ + $this->content, + 'weight' => $this->weight, + 'volume' => $this->volume, + 'shipping_tmp_id' => $this->shipping_tmp_id, + 'attr' => $this->attr, + 'part' => $this->part, + 'spec' => $this->spec, + 'created_at' => $this->created_at->timestamp, + 'check_user_id' => $this->check_user_id, + 'check_at' => $this->check_at?->timestamp, + 'check_remarks' => $this->check_remarks, + 'check_status' => $this->check_status, + 'check_status_text' => $this->check_status->text(), + + 'skus' => GoodsSkuResource::collection($this->whenLoaded('skus')), + ]); + } +} diff --git a/src/Http/Resources/GoodsSkuResource.php b/src/Http/Resources/GoodsSkuResource.php new file mode 100644 index 0000000..4ecf204 --- /dev/null +++ b/src/Http/Resources/GoodsSkuResource.php @@ -0,0 +1,25 @@ + $this->id, + 'sn' => $this->sn, + 'goods_id' => $this->goods_id, + 'name' => $this->name, + 'price' => $this->price, + 'vip_price' => $this->vip_price, + 'stock' => $this->stock, + 'spec' => $this->spec, + 'weight' => $this->weight, + 'volume' => $this->volume, + 'shipping_tmp_id' => $this->shipping_tmp_id, + ]; + } +} diff --git a/src/Http/Resources/GoodsTinyResource.php b/src/Http/Resources/GoodsTinyResource.php new file mode 100644 index 0000000..dd72d4e --- /dev/null +++ b/src/Http/Resources/GoodsTinyResource.php @@ -0,0 +1,32 @@ + $this->id, + 'merchant_id' => $this->merchant_id, + 'merchant' => MerchantTinyResource::make($this->whenLoaded('merchant')), + 'category_id' => $this->category_id, + 'type_id' => $this->category_id, + 'brand_id' => $this->brand_id, + 'goods_sn' => $this->goods_sn, + 'name' => $this->name, + 'cover_image' => $this->cover_image, + 'images' => $this->images, + 'description' => $this->description, + 'price' => floatval($this->price), + 'vip_price' => floatval($this->vip_price), + 'on_sale' => $this->on_sale, + 'sold_count' => $this->sold_count, + 'stock' => $this->stock, + 'created_at' => $this->created_at->timestamp, + ]; + } +} diff --git a/src/Models/Goods.php b/src/Models/Goods.php new file mode 100644 index 0000000..368dceb --- /dev/null +++ b/src/Models/Goods.php @@ -0,0 +1,106 @@ + 'array', + 'spec' => 'array', + 'part' => 'array', + 'content' => 'array', + 'images' => 'array', + 'check_status' => CheckStatus::class, + ]; + + protected $dates = ['check_at']; + + protected static function booted() + { + static::creating(function ($model) { + if (! $model->goods_sn) { + $model->goods_sn = Str::uuid(); + } + }); + static::updating(function (Goods $model) { + if ($model->isDirty(['merchant_id', 'category_id', 'brand_id', 'goods_sn', 'name', 'cover_image', 'description', 'content', 'price', 'vip_price']) && $model->check_status === CheckStatus::Success) { + $model->check_status = CheckStatus::None; + $model->check_at = null; + $model->check_user_id = null; + $model->check_remarks = null; + } + }); + } + + public function modelFilter() + { + return GoodsFilter::class; + } + + public function category() + { + return $this->belongsTo(GoodsCategory::class, 'category_id'); + } + + public function merchant() + { + return $this->belongsTo(Merchant::class, 'merchant_id'); + } + + public function type() + { + return $this->belongsTo(GoodsType::class, 'type_id'); + } + + public function brand() + { + return $this->belongsTo(GoodsBrand::class, 'brand_id'); + } + + public function checkUser() + { + return $this->belongsTo(Administrator::class, 'check_user_id'); + } + + public function skus() + { + return $this->hasMany(GoodsSku::class, 'goods_id'); + } + + public function checkLogs() + { + return $this->hasMany(GoodsCheck::class, 'goods_id'); + } + + public function scopeSort($q) + { + return $q->orderBy('created_at', 'desc'); + } + + public function scopeShow($q) + { + return $q->where('on_sale', 1)->where('check_status', CheckStatus::Success); + } +} diff --git a/src/Models/GoodsBrand.php b/src/Models/GoodsBrand.php new file mode 100644 index 0000000..c0a6b58 --- /dev/null +++ b/src/Models/GoodsBrand.php @@ -0,0 +1,19 @@ +hasMany(Goods::class, 'brand_id'); + } +} diff --git a/src/Models/GoodsCart.php b/src/Models/GoodsCart.php new file mode 100644 index 0000000..b841f76 --- /dev/null +++ b/src/Models/GoodsCart.php @@ -0,0 +1,40 @@ + 'json', + 'spec' => 'json', + 'part' => 'json', + ]; + + public function user() + { + return $this->belongsTo(User::class, 'user_id'); + } + + public function goods() + { + return $this->belongsTo(Goods::class, 'goods_id'); + } + + public function merchant() + { + return $this->belongsTo(Merchant::class, 'merchant_id'); + } + + public function goodsSku() + { + return $this->belongsTo(GoodsSku::class, 'goods_sku_id'); + } +} diff --git a/src/Models/GoodsCategory.php b/src/Models/GoodsCategory.php new file mode 100644 index 0000000..3a2e198 --- /dev/null +++ b/src/Models/GoodsCategory.php @@ -0,0 +1,99 @@ +parent_id) { + // 将层级设为 1 + $category->level = 1; + // 将 path 设为 - + $category->path = '-'; + } else { + // 将层级设为父类目的层级 + 1 + $category->level = $category->parent->level + 1; + // 将 path 值设为父类目的 path 追加父类目 ID 以及最后跟上一个 - 分隔符 + $category->path = $category->parent->path.$category->parent_id.'-'; + } + }); + + static::deleting(function ($category) { + // 所有下级分类 + $ids = GoodsCategory::where('path', 'like', '%-'.$category->id.'-%')->pluck('id'); + // 检查下级分类是否包含商品 + if (Goods::whereIn('category_id', array_merge($ids, [$category->id]))->exists()) { + // todo 阻止删除该分类 + } + // 删除所有下级分类 + GoodsCategory::where('path', 'like', '%-'.$category->id.'-%')->delete(); + }); + } + + public static function selectOptions(\Closure $closure = null, $rootText = null) + { + $options = (new static())->withQuery($closure)->buildSelectOptions(static::query()->where('path', 'like', '-1-%')->sort()->get()->toArray(), 1); + + $list = collect($options); + + if ($rootText !== false) { + $rootText = $rootText ?: admin_trans_label('root'); + $list->prepend($rootText, 0); + } + + return $list->all(); + } + + public function modelFilter() + { + return GoodsCategoryFilter::class; + } + + public function parent() + { + return $this->belongsTo(self::class, 'parent_id'); + } + + public function children() + { + return $this->hasMany(self::class, 'parent_id'); + } + + public function goods() + { + return $this->hasMany(Goods::class, 'category_id'); + } + + public function scopeSort($q) + { + return $q->orderBy('sort'); + } + + public function scopeShow($q) + { + return $q->where('is_enable', 1); + } +} diff --git a/src/Models/GoodsCheck.php b/src/Models/GoodsCheck.php new file mode 100644 index 0000000..192a043 --- /dev/null +++ b/src/Models/GoodsCheck.php @@ -0,0 +1,61 @@ + 'array', + 'spec' => 'array', + 'part' => 'array', + 'content' => 'array', + 'images' => 'array', + 'check_status' => CheckStatus::class, + ]; + + public function category() + { + return $this->belongsTo(GoodsCategory::class, 'category_id'); + } + + public function merchant() + { + return $this->belongsTo(Merchant::class, 'merchant_id'); + } + + public function type() + { + return $this->belongsTo(GoodsType::class, 'type_id'); + } + + public function brand() + { + return $this->belongsTo(GoodsBrand::class, 'brand_id'); + } + + public function goods() + { + return $this->belongsTo(Goods::class, 'goods_id'); + } + + public function scopeSort($q) + { + return $q->orderBy('check_status', 'asc')->orderBy('created_at', 'desc'); + } +} diff --git a/src/Models/GoodsSku.php b/src/Models/GoodsSku.php new file mode 100644 index 0000000..693e56d --- /dev/null +++ b/src/Models/GoodsSku.php @@ -0,0 +1,43 @@ + 'array', + ]; + + public $timestamps = false; + + public function goods() + { + return $this->belongsTo(Goods::class, 'goods_id'); + } + + /** + * Mysql Json 查询 + * 数据库存储格式: [{"name": "颜色", "price": 0, "value": "白色"}, {"name": "内存", "price": 0, "value": "32G"}] + * + * @param Builder $q + * @param array $params [{"name": "颜色", "value": "白色"}, {"name": "内存", "value": "32G"}] + */ + public function scopeJsonArray($q, $params) + { + foreach ($params as $item) { + foreach ($item as $key => $value) { + $value = is_string($value) ? '"'.$value.'"' : $value; + $q->whereRaw('json_contains(spec->>"$[*].'.$key."\", '".$value."')"); + } + } + + return $q; + } +} diff --git a/src/Models/GoodsType.php b/src/Models/GoodsType.php new file mode 100644 index 0000000..a2d1f22 --- /dev/null +++ b/src/Models/GoodsType.php @@ -0,0 +1,20 @@ + 'array', + 'spec' => 'array', + 'part' => 'array', + ]; + + public $timestamps = false; +} diff --git a/src/Renderable/GoodsTable.php b/src/Renderable/GoodsTable.php new file mode 100644 index 0000000..eda5a85 --- /dev/null +++ b/src/Renderable/GoodsTable.php @@ -0,0 +1,36 @@ +model()->sort()->show(); + + $grid->column('name')->display(function () { + return ($this->cover_image ? ' ' : '').''.$this->name.''; + }); + $grid->column('price'); + $grid->column('vip_price'); + $grid->column('stock') + ->if(fn () => $this->skus->count() > 0) + ->display(fn () => $this->skus->sum('stock')) + ->else() + ->editable(); + $grid->column('sold_count'); + + $grid->disableActions(); + $grid->showRowSelector(); + }); + } +} diff --git a/views/form/attr.blade.php b/views/form/attr.blade.php new file mode 100644 index 0000000..b2f3482 --- /dev/null +++ b/views/form/attr.blade.php @@ -0,0 +1,227 @@ +
+ + + + + @foreach($headers as $item) + + @endforeach + + + + + @foreach($value as $index => $item) + + + + @foreach($item['values'] as $subItem) + + + + + @endforeach + + + + + @endforeach + + + + + +
{{ $item }}
{{ $item['name'] }}
{{ $subItem }} + +
+ + + +
+ + + +
+
+ + diff --git a/views/form/spec.blade.php b/views/form/spec.blade.php new file mode 100644 index 0000000..c140fff --- /dev/null +++ b/views/form/spec.blade.php @@ -0,0 +1,225 @@ +
+ + + + + @foreach($headers as $item) + + @endforeach + + + + + @foreach($value?:[] as $item) + + + + @foreach($item['values'] as $subItem) + + + + + + @endforeach + + + + + + @endforeach + + + + + +
{{ $item }} + @if($type) + + @endif +
{{ $item['name'] }}
{{ $subItem['name'] }}{{ $subItem['value'] }} + +
+ + + + + +
+ + + +
+
+ + diff --git a/views/goods-type/grid-attr.blade.php b/views/goods-type/grid-attr.blade.php new file mode 100644 index 0000000..7dc24bb --- /dev/null +++ b/views/goods-type/grid-attr.blade.php @@ -0,0 +1,21 @@ + + + +@if($value) +
+ @foreach($value as $item) +
+
+ {{ $item['name'] }} +
+
+ @foreach($item['values'] as $subItem) +
+ {{ $subItem }} +
+ @endforeach +
+
+ @endforeach +
+@endif diff --git a/views/goods/grid-attr.blade.php b/views/goods/grid-attr.blade.php new file mode 100644 index 0000000..be92229 --- /dev/null +++ b/views/goods/grid-attr.blade.php @@ -0,0 +1,22 @@ + + + +@if($value) +
+ @foreach($value as $item) +
+
+ {{ $item['name'] }} +
+
+ @foreach($item['values'] as $subItem) +
+ {{ $subItem['name'] }} + {{ $subItem['value'] }} +
+ @endforeach +
+
+ @endforeach +
+@endif