Compare commits

...

2 Commits
main ... mall

Author SHA1 Message Date
vine_liutk 478dbe1531 暂时保存 2024-01-15 13:50:18 +08:00
vine_liutk 77a017298a 添加商品分类管理 2024-01-04 17:43:20 +08:00
17 changed files with 631 additions and 17 deletions

View File

@ -35,7 +35,7 @@ class Components extends BaseRenderer {
* 2位小数输入框
*/
public function decimalControl($name ='decimal', $label = null){
return amisMake()->NumberControl()
return amisMake()->NumberControl()->size('sm')
->name($name)->label($label ?? __('admin.components.decimal'))
->kilobitSeparator(true)
->percision(2)

View File

@ -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')),
]);
}

View File

@ -0,0 +1,67 @@
<?php
namespace App\Admin\Controllers;
use Slowlyo\OwlAdmin\Renderers\Page;
use Slowlyo\OwlAdmin\Renderers\Form;
use Slowlyo\OwlAdmin\Controllers\AdminController;
use App\Services\Admin\ProductCategoryService;
use App\Admin\Components;
use Illuminate\Http\Request;
class ProductCategoryController extends AdminController
{
protected string $serviceName = ProductCategoryService::class;
public function list(): Page
{
$crud = $this->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();
}
}

View File

@ -0,0 +1,111 @@
<?php
namespace App\Admin\Controllers;
use Slowlyo\OwlAdmin\Admin;
use Slowlyo\OwlAdmin\Renderers\Page;
use Slowlyo\OwlAdmin\Renderers\Form;
use Slowlyo\OwlAdmin\Controllers\AdminController;
use App\Services\Admin\ProductService;
use App\Admin\Components;
use App\Models\Keyword;
class ProductController extends AdminController
{
protected string $serviceName = ProductService::class;
public function list():Page
{
$crud = $this->baseCRUD()->tableLayout('fixed')
->headerToolbar([
$this->createButton(),
...$this->baseHeaderToolBar(),
])
->filter($this->baseFilter()->body([
amis()->GroupControl()->mode('horizontal')->body([
amis()->TextControl('sku', __('admin.products.sku'))
->placeholder(__('admin.products.sku')),
amis()->TextControl('name', __('admin.products.name'))
->placeholder(__('admin.products.name')),
]),
]))
->columns([
amis()->TableColumn('name', __('admin.products.name'))->width('300px'),
amis()->TableColumn('sku', __('admin.sku'))->sortable(true),
amis()->TableColumn('category.name', __('admin.products.category')),
amis()->TableColumn('cover', __('admin.products.cover'))->type('image')->height('50px')->width('50px')->enlargeAble(true),
amis()->TableColumn('is_sale', __('admin.products.is_sale'))->type('switch'),
amis()->TableColumn('is_recommend', __('admin.products.is_recommend'))->type('switch'),
amis()->Operation()->label(__('admin.actions'))->buttons([
$this->rowEditButton(),
$this->rowDeleteButton(),
])
]);
return $this->baseList($crud);
}
public function form(): Form
{
return $this->baseForm()->panelClassName('px-0')->body([
amis()->Tabs()->tabsMode('line')->tabs([
//基础信息
amis()->Tab()->title(__('admin.products.tab1'))->body([
amis()->Grid()->columns([
amis()->Wrapper()->body([
amis()->TextControl('name', __('admin.products.name'))->required(true),
Components::make()->parentControl(admin_url('api/product_categories/tree-list'), 'category_id', __('admin.products.category')),
amis()->TextControl('spu', __('admin.products.spu'))->description('*未填写则自动生成唯一spu'),
amis()->TextareaControl('sub_title', __('admin.products.sub_title'))->minRows(5),
Components::make()->sortControl('virtual_sales', __('admin.products.virtual_sales')),
Components::make()->sortControl('stocks', __('admin.products.stocks'))->description('*已销售过的商品无法直接编辑库存,需前往商品库存中操作。'),
Components::make()->decimalControl('price', __('admin.products.price')),
amis()->SwitchControl('is_sale', __('admin.products.is_sale'))->value(false),
amis()->SwitchControl('is_show', __('admin.products.is_show'))->value(false),
])->md(4),
amis()->Wrapper()->body([
Components::make()->imageControl('photos', __('admin.products.photos'))->multiple(true)->draggable(true)->required(true),
Components::make()->cropImageControl('cover', __('admin.products.cover'))->description('*若不选择上传,则默认为相册第一张'),
Components::make()->sortControl('sort', __('admin.products.sort')),
amis()->SwitchControl('is_recommend', __('admin.products.is_recommend'))->value(false),
amis()->SwitchControl('is_hot', __('admin.products.is_hot'))->value(false),
amis()->SwitchControl('is_new', __('admin.products.is_new'))->value(false),
])->md(8)
]),
]),
//详情,信息
amis()->Tab()->title(__('admin.products.tab2'))->body([
//详情,基础信息,运费模板,属性标签,
amis()->Grid()->columns([
amis()->Wrapper()->body([
Components::make()->keywordsTagControl('t_ids', __('admin.products.tags'), 'product_tag'),
amis()->SelectControl('shipping_tmp_id', __('admin.products.shipping_tmp_id'))->required(true),
amis()->InputKV()->name('base_info')->label('参数信息')->keyPlaceholder('属性')->valuePlaceholder('值'),
])->md(4),
amis()->Wrapper()->body([
Components::make()->fuEditorControl('description', __('admin.products.description')),
])->md(8),
]),
]),
//多属性商品
amis()->Tab()->title(__('admin.products.tab3'))->body([
])
])
]);
}
public function detail(): Form
{
return $this->baseDetail()->body([]);
}
}

View File

@ -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,10 @@ Route::group([
$router->resource('ads', \App\Admin\Controllers\AdController::class);
$router->resource('product_categories', \App\Admin\Controllers\ProductCategoryController::class);
$router->resource('products', \App\Admin\Controllers\ProductController::class);
//修改上传
$router->post('upload_file', [\App\Admin\Controllers\IndexController::class, 'uploadFile']);
$router->post('upload_image', [\App\Admin\Controllers\IndexController::class, 'uploadImage']);

View File

@ -0,0 +1,31 @@
<?php
namespace App\Models\Filters;
use EloquentFilter\ModelFilter;
use App\Models\ProductCategory;
class ProductCategoryFilter extends ModelFilter
{
/**
* 关键字
*/
public function name($name)
{
return $this->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')
. '-%' ?? '');
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace App\Models\Filters;
use EloquentFilter\ModelFilter;
class ProductFilter extends ModelFilter
{
/**
* 关键字
*/
public function name($name)
{
return $this->where('name','like', '%'.$name.'%');
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use EloquentFilter\Filterable;
class Product extends Model
{
use HasFactory;
use Filterable;
protected function serializeDate(\DateTimeInterface $date)
{
return $date->format('Y-m-d H:i:s');
}
protected $fillable = [
'name', 'sub_title', 'cover', 'photos', 'base_info', 'description',
'spu', 'sku', 'parent_id', 'category_id', 't_ids',
];
}

View File

@ -0,0 +1,56 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use EloquentFilter\Filterable;
class ProductCategory extends Model
{
use HasFactory;
use Filterable;
protected function serializeDate(\DateTimeInterface $date)
{
return $date->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');
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,125 @@
<?php
namespace App\Services\Admin;
use Illuminate\Support\Arr;
use App\Models\ProductCategory;
use App\Models\Filters\ProductCategoryFilter;
use App\Traits\UploadTrait;
/**
* @method ProductCategory getModel()
* @method ProductCategory|\Illuminate\Database\Query\Builder query()
*/
class ProductCategoryService extends BaseService
{
protected string $modelName = ProductCategory::class;
protected string $modelFilterName = ProductCategoryFilter::class;
use UploadTrait;
public function getTree()
{
$list = $this->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();
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace App\Services\Admin;
use App\Models\Product;
use App\Models\Filters\ProductFilter;
use App\Traits\UploadTrait;
use Illuminate\Support\Arr;
/**
* @method Product getModel()
* @method Product|\Illuminate\Database\Query\Builder query()
*/
class ProductService extends BaseService
{
use UploadTrait;
protected string $modelName = Product::class;
protected string $modelFilterName = ProductFilter::class;
}

View File

@ -0,0 +1,42 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('product_categories', function (Blueprint $table) {
$table->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');
}
};

View File

@ -0,0 +1,56 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->string('name')->comment('名称');
$table->string('spu')->nullable()->comment('spu');
$table->string('sku')->nullable()->unique()->comment('sku');
$table->unsignedBigInteger('parent_id')->nullable()->comment('父级');
$table->unsignedBigInteger('category_id')->nullable()->comment('分类');
$table->string('t_ids')->nullable()->comment('标签');
$table->text('sub_title')->nullable()->comment('副标题');
$table->string('cover')->nullable()->comment('封面');
$table->text('photos')->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->unsignedBigInteger('shipping_tmp_id')->nullable()->comment('运费模板');
$table->unsignedInteger('stocks')->default(0)->comment('库存');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('products');
}
};

View File

@ -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 {

View File

@ -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,43 @@ return [
'mini_id' => '小程序ID',
'mini_link'=> '小程序路径'
],
],
'product_categories' => [
'name' => '名称',
'key' => 'KEY',
'cover'=>'分类图',
'sort'=>'排序',
'is_enable' => '启用',
'is_show' => '展示',
'is_recommend' => '推荐'
],
'products' => [
'name' => '名称',
'spu' => 'SPU',
'sku' => 'SKU',
'category' => '分类',
'sub_title'=> '副标题',
'cover' => '封面',
'photos' => '相册',
'base_info' => '基础信息',
'description' => '详情',
'price' => '售价',
'virtual_sales' => '虚拟销量',
'stocks' => '库存',
'tags' => '标签',
'shipping_tmp_id' => '运费模板',
'sort' => '排序',
'is_sale' => '上架',
'is_show' => '展示',
'is_recommend' => '推荐',
'is_hot' => '热销',
'is_new' => '新品',
'tab1' => '基础信息',
'tab2' => '详细信息',
'tab3' => '规格属性',
]
];

View File

@ -12,5 +12,13 @@ return [
'keywords' => '数据字典',
'web_content' => '内容管理',
'articles' => '文章管理',
'ads' => '广告管理',
'ads' => '广告管理',
'mall' => '商城管理',
'product_categories'=> '商品分类',
'products' => '商品管理',
'shipping_tmps' => '运费模板',
'orders' => '订单管理',
'shipping_orders' => '发货管理',
'after_orders' => '售后管理',
'product_stocks' => '商品库存',
];