+
+ diff --git a/.gitignore b/.gitignore index 8c9279ce..1ee66503 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,6 @@ public/.well-known /.settings /.buildpath /.project + +CLAUDE.md +/skills diff --git a/app/admin/controller/v1/Banner.php b/app/admin/controller/v1/Banner.php index a28a041f..52ac5edc 100644 --- a/app/admin/controller/v1/Banner.php +++ b/app/admin/controller/v1/Banner.php @@ -91,7 +91,6 @@ class Banner $banner = SysBannerModel::withoutField([ 'at_page', - 'unique_label', 'language_id', 'created_at', 'updated_at', @@ -142,10 +141,11 @@ class Banner 'name', 'desc', 'recommend', + 'unique_label', 'at_platform' => 'pc', 'status' => 1 ]); - + $validate = new SysBannerValidate; if (!$validate->scene('edit')->check(array_merge($put, ['id' => $id]))) { return error($validate->getError()); diff --git a/app/admin/controller/v1/NavigationItem.php b/app/admin/controller/v1/NavigationItem.php index 3875d7f7..fa63be15 100644 --- a/app/admin/controller/v1/NavigationItem.php +++ b/app/admin/controller/v1/NavigationItem.php @@ -67,6 +67,8 @@ class NavigationItem 'id', 'pid', 'name', + 'desc', + 'image', 'nav_id', 'sort', 'status', @@ -93,7 +95,9 @@ class NavigationItem 'pid', 'nav_id', 'name', + 'desc', 'icon', + 'image', 'link_to' => 'custom', 'link', 'sort', @@ -121,7 +125,9 @@ class NavigationItem 'pid', 'nav_id', 'name', + 'desc', 'icon', + 'image', 'link_to', 'link', 'sort', @@ -172,7 +178,7 @@ class NavigationItem if (empty($nav)) { return error('请确认要操作对象是否存在'); } - + if (!$nav->delete()) { return error('操作失败'); } diff --git a/app/admin/controller/v1/Product.php b/app/admin/controller/v1/Product.php index 3b635255..5914a8ed 100644 --- a/app/admin/controller/v1/Product.php +++ b/app/admin/controller/v1/Product.php @@ -24,7 +24,7 @@ class Product 'category_id', 'created_at', 'is_show', - 'page/d' => 1, + 'page/d' => 1, 'size/d' => 10 ]); @@ -83,7 +83,7 @@ class Product ]) ->bypk(request()->param('id')) ->find() - ->bindAttr('category', ['category_name']) + ?->bindAttr('category', ['category_name']) ->hidden(['category']); if (empty($product)) { return error('产品不存在'); @@ -108,7 +108,7 @@ class Product // 获取关联产品 $product->related = ProductRelatedModel::field([ - 'related_product_id', + 'related_product_id', 'sort' ]) ->with(['product' => function($query) { @@ -149,8 +149,8 @@ class Product 'seo_desc' ]); $put = array_merge( - $put, - ['skus' => json_decode($put['skus'], true)], + $put, + ['skus' => json_decode($put['skus'], true)], ['related' => json_decode($put['related'], true)], ); @@ -305,7 +305,7 @@ class Product return success('操作成功'); } - + // 导出 public function export() { @@ -444,7 +444,7 @@ class Product }); } } - + return $products->toArray(); } } diff --git a/app/admin/controller/v1/ProductCategoryRecommend.php b/app/admin/controller/v1/ProductCategoryRecommend.php new file mode 100644 index 00000000..3a1581de --- /dev/null +++ b/app/admin/controller/v1/ProductCategoryRecommend.php @@ -0,0 +1,203 @@ +get([ + 'keywords', + 'category_name', + 'page/d' => 1, + 'size/d' => 10 + ]); + + // 查询数据 + $data = ProductCategoryRecommendModel::withJoin(['category' => function($query) use ($param) { + if (!empty($param['category_name'])) { + $query->where('category.name', 'like', '%' . $param['category_name'] . '%'); + } + }]) + ->withSearch(['keywords'], ['keywords' => $param['keywords']??null]) + ->language(request()->lang_id) + ->order(['sort' => 'asc', 'id' => 'desc']) + ->paginate([ + 'list_rows' => $param['size'], + 'page' => $param['page'], + ]) + ->bindAttr('category', ['category_name' => 'name']) + ->hidden(['category', 'language_id', 'updated_at', 'deleted_at']) + ?->each(function($item) { + // 列表页面图片输出缩略图 + if (!empty($item['image'])) { + $item['image'] = thumb($item['image']); + } + }); + + return success('获取成功', $data); + } + + /** + * 导出Excel + */ + public function export() + { + $schema = [ + 'id' => 'ID', + 'image' => '图片', + 'category_name' => '分类名称', + 'desc' => '产品介绍', + 'link' => '链接地址', + 'sort' => '排序', + 'disabled' => '状态', + 'created_at' => '添加时间' + ]; + + // 获取导出数据 + $data = $this->getProductCategoryRecommendData(); + + // 导出 + return xlsx_writer($data, $schema, '产品推荐列表' . date('YmdHis')); + } + // 获取要导出的推荐记录数据 + private function getProductCategoryRecommendData() + { + $server = request()->server(); + $image_host = $server['REQUEST_SCHEME'] . "://" . $server['SERVER_NAME'] . '/'; + $param = request()->get(['keywords']); + + // 查询数据 + return ProductCategoryRecommendModel::withJoin(['category' => function($query) use ($param) { + if (!empty($param['category_name'])) { + $query->where('category.name', 'like', '%' . $param['category_name'] . '%'); + } + }]) + ->withSearch(['keywords'], ['keywords' => $param['keywords']??null]) + ->language(request()->lang_id) + ->order(['sort' => 'asc', 'id' => 'desc']) + ->select() + ->bindAttr('category', ['category_name' => 'name']) + ->hidden(['category', 'language_id', 'updated_at', 'deleted_at']) + ?->each(function($item) use($image_host) { + // 拼接完整图片URL + if (!empty($item['image'])) { + $item['image'] = url_join($image_host, $item['image']); + } + // 状态 + $item['disabled'] = $item['disabled'] == 1 ? '禁用' : '启用'; + }) + ->toArray(); + } + + /** + * 获取详细数据 + */ + public function read() + { + $id = request()->param('id/d'); + $record = ProductCategoryRecommendModel::bypk($id) + ->withoutField(['language_id', 'created_at', 'updated_at', 'deleted_at']) + ->find(); + if (empty($record)) { + return error('推荐数据不存在'); + } + + return success('success', $record); + } + + /** + * 新增数据 + */ + public function save() + { + $post = request()->post([ + 'category_id', + 'title', + 'image', + 'desc', + 'link', + 'sort', + 'disabled' + ]); + $data = array_merge($post, ['language_id' => request()->lang_id]); + + // 参数校验 + $validate = new ProductCategoryRecommendValidate(); + if (!$validate->scene('create')->check($data)) { + return error($validate->getError()); + } + + // 保存推荐数据 + $recommend = ProductCategoryRecommendModel::create($data); + if ($recommend->isEmpty()) { + return error('保存失败'); + } + return success('保存成功'); + } + + /** + * 更新数据 + */ + public function update() + { + $id = request()->param('id/d'); + $post = request()->post([ + 'category_id', + 'title', + 'image', + 'desc', + 'link', + 'sort', + 'disabled' + ]); + $data = array_merge($post, ['language_id' => request()->lang_id]); + + // 参数校验 + $validate = new ProductCategoryRecommendValidate(); + $check_data = array_merge($data, ['id' => $id]); + if (!$validate->scene('update')->check($check_data)) { + return error($validate->getError()); + } + + // 更新推荐数据 + $recommend = ProductCategoryRecommendModel::bypk($id)->find(); + if (empty($recommend)) { + return error('请确认操作对象是否存在'); + } + if (!$recommend->save($data)) { + return error('操作失败'); + } + + return success('操作成功'); + } + + /** + * 删除 + */ + public function delete() + { + $id = request()->param('id/d'); + + // 删除推荐记录数据 + $record = ProductCategoryRecommendModel::bypk($id)->find(); + if (empty($record)) { + return error('请确认操作对象是否正确'); + } + if (!$record->delete()) { + return error('操作失败'); + } + + return success('操作成功'); + } +} diff --git a/app/admin/controller/v1/SysMallStoreEntrance.php b/app/admin/controller/v1/SysMallStoreEntrance.php new file mode 100644 index 00000000..94f38910 --- /dev/null +++ b/app/admin/controller/v1/SysMallStoreEntrance.php @@ -0,0 +1,201 @@ +get([ + 'name', + 'page/d' => 1, + 'size/d' => 10 + ]); + + // 查询数据 + $data = SysMallStoreEntranceModel::withoutField([ + 'language_id', + 'hover_image', + 'updated_at', + 'deleted_at' + ]) + ->withSearch(['name'], ['name' => $param['name']??null]) + ->language(request()->lang_id) + ->order(['sort' => 'asc', 'id' => 'desc']) + ->paginate([ + 'list_rows' => $param['size'], + 'page' => $param['page'], + ]) + ?->each(function($item) { + // 列表页面图片输出缩略图 + if (!empty($item['image'])) { + $item['image'] = thumb($item['image']); + } + }); + + return success('获取成功', $data); + } + + /** + * 获取详情 + */ + public function read() + { + $id = request()->param('id/d'); + $record = SysMallStoreEntranceModel::bypk($id) + ->withoutField(['language_id', 'created_at', 'updated_at', 'deleted_at']) + ->find(); + if (empty($record)) { + return error('商城店铺入口数据不存在'); + } + + return success('success', $record); + } + + /** + * 新增数据 + */ + public function save() + { + $post = request()->post([ + 'name', + 'image', + 'hover_image', + 'link', + 'sort', + 'disabled' + ]); + $data = array_merge($post, ['language_id' => request()->lang_id]); + + // 参数校验 + $validate = new SysMallStoreEntranceValidate(); + if (!$validate->scene('create')->check($data)) { + return error($validate->getError()); + } + + // 保存数据 + $entrance = SysMallStoreEntranceModel::create($data); + if ($entrance->isEmpty()) { + return error('保存失败'); + } + return success('保存成功'); + } + + /** + * 更新数据 + */ + public function update() + { + $id = request()->param('id/d'); + $post = request()->post([ + 'name', + 'image', + 'hover_image', + 'link', + 'sort', + 'disabled' + ]); + $data = array_merge($post, ['language_id' => request()->lang_id]); + + // 参数校验 + $validate = new SysMallStoreEntranceValidate(); + $check_data = array_merge($data, ['id' => $id]); + if (!$validate->scene('update')->check($check_data)) { + return error($validate->getError()); + } + + // 更新数据 + $entrance = SysMallStoreEntranceModel::bypk($id)->find(); + if (empty($entrance)) { + return error('请确认操作对象是否存在'); + } + if (!$entrance->save($data)) { + return error('操作失败'); + } + + return success('操作成功'); + } + + /** + * 删除 + */ + public function delete() + { + $id = request()->param('id/d'); + + // 删除数据 + $record = SysMallStoreEntranceModel::bypk($id)->find(); + if (empty($record)) { + return error('请确认操作对象是否正确'); + } + if (!$record->delete()) { + return error('操作失败'); + } + + return success('操作成功'); + } + + /** + * 导出Excel + */ + public function export() + { + $schema = [ + 'id' => 'ID', + 'image' => '图片', + 'hover_image' => '悬浮图', + 'name' => '名称', + 'link' => '链接地址', + 'sort' => '排序', + 'disabled' => '状态', + 'created_at' => '添加时间' + ]; + + // 获取导出数据 + $data = $this->getExportData(); + + // 导出 + return xlsx_writer($data, $schema, '系统商城店铺入口列表' . date('YmdHis')); + } + + // 获取要导出的数据 + private function getExportData() + { + $server = request()->server(); + $image_host = $server['REQUEST_SCHEME'] . "://" . $server['SERVER_NAME'] . '/'; + $param = request()->get(['name']); + + // 查询数据 + return SysMallStoreEntranceModel::withoutField([ + 'language_id', + 'updated_at', + 'deleted_at' + ]) + ->withSearch(['name'], ['name' => $param['name']??null]) + ->language(request()->lang_id) + ->order(['sort' => 'asc', 'id' => 'desc']) + ->select() + ?->each(function($item) use($image_host) { + // 拼接完整图片URL + if (!empty($item['image'])) { + $item['image'] = url_join($image_host, $item['image']); + } + if (!empty($item['hover_image'])) { + $item['hover_image'] = url_join($image_host, $item['hover_image']); + } + // 状态转换 + $item['disabled'] = $item['disabled'] == 1 ? '禁用' : '启用'; + }) + ->toArray(); + } +} diff --git a/app/admin/model/v1/ProductCategoryRecommendModel.php b/app/admin/model/v1/ProductCategoryRecommendModel.php new file mode 100644 index 00000000..317bb968 --- /dev/null +++ b/app/admin/model/v1/ProductCategoryRecommendModel.php @@ -0,0 +1,47 @@ +belongsTo(\app\index\model\LanguageModel::class, 'language_id', 'id'); + } + + // 关联产品分类 + public function category() + { + return $this->belongsTo(\app\index\model\ProductCategoryModel::class, 'category_id', 'id'); + } + + // 所属语言范围查询 + public function scopeLanguage($query, $language) + { + $query->where($this->getTable() . '.language_id', '=', $language); + } + + // 关键词搜索 + public function searchKeywordsAttr($query, string|null $keywords) + { + if (is_null($keywords)) { + return; + } + $query->where($this->getTable() . '.title', 'like', "%{$keywords}%") + ->whereOr($this->getTable() . '.desc', 'like', "%{$keywords}%"); + } +} diff --git a/app/admin/model/v1/SysMallStoreEntranceModel.php b/app/admin/model/v1/SysMallStoreEntranceModel.php new file mode 100644 index 00000000..f1cd1a81 --- /dev/null +++ b/app/admin/model/v1/SysMallStoreEntranceModel.php @@ -0,0 +1,61 @@ +belongsTo(\app\index\model\LanguageModel::class, 'language_id', 'id'); + } + + // 所属语言范围查询 + public function scopeLanguage($query, $language) + { + $query->where($this->getTable() . '.language_id', '=', $language); + } + + // 查询启用状态 + public function scopeEnabled($query) + { + $query->where('disabled', '=', 0); + } + + // 查询禁用状态 + public function scopeDisabled($query) + { + $query->where('disabled', '=', 1); + } + + // 按名称搜索 + public function searchNameAttr($query, $value, $data) + { + if (is_null($value)) { + return; + } + $query->where('name', 'like', "%{$value}%"); + } + + // 按链接地址搜索 + public function searchLinkAttr($query, $value, $data) + { + if (is_null($value)) { + return; + } + $query->where('link_url', 'like', "%{$value}%"); + } +} diff --git a/app/admin/route/v1.php b/app/admin/route/v1.php index e2aa35f2..b8e56853 100644 --- a/app/admin/route/v1.php +++ b/app/admin/route/v1.php @@ -87,7 +87,7 @@ Route::group('v1', function () { // 视频分类列表 Route::get('categorys', 'VideoCategory/list'); - + // 视频分类 Route::group('category', function () { // 视频分类分页数据 @@ -311,6 +311,27 @@ Route::group('v1', function () { // 分类删除 Route::delete('delete/:id', 'ProductCategory/delete'); + + // 产品分类推荐数据 + Route::group('recommend', function () { + // 推荐数据分页列表 + Route::get('index', 'ProductCategoryRecommend/index'); + + // 推荐数据导出 + Route::get('export', 'ProductCategoryRecommend/export'); + + // 推荐数据详情 + Route::get('read/:id', 'ProductCategoryRecommend/read'); + + // 推荐数据新增 + Route::post('save', 'ProductCategoryRecommend/save'); + + // 推荐数据更新 + Route::put('update/:id', 'ProductCategoryRecommend/update'); + + // 推荐数据删除 + Route::delete('delete/:id', 'ProductCategoryRecommend/delete'); + }); }); // 产品购买链接 @@ -483,7 +504,7 @@ Route::group('v1', function () { // 分页 Route::get('index', 'Navigation/index'); - + // 导航详情 Route::get('read/:id', 'Navigation/read'); @@ -574,6 +595,29 @@ Route::group('v1', function () { // 反馈管理 - 产品询盘列表 Route::get('product/inquiry/index', 'ProductInquiry/index'); + // 系统商城店铺入口 + Route::group('mall', function() { + Route::group('store', function() { + // 店铺入口列表分页 + Route::get('index', 'SysMallStoreEntrance/index'); + + // 店铺入口导出 + Route::get('export', 'SysMallStoreEntrance/export'); + + // 店铺入口详情 + Route::get('read/:id', 'SysMallStoreEntrance/read'); + + // 店铺入口新增 + Route::post('save', 'SysMallStoreEntrance/save'); + + // 店铺入口更新 + Route::put('update/:id', 'SysMallStoreEntrance/update'); + + // 店铺入口删除 + Route::delete('delete/:id', 'SysMallStoreEntrance/delete'); + }); + }); + // 配置项列表 Route::group('config', function() { // 配置分组 diff --git a/app/admin/validate/v1/NavigationItemValidate.php b/app/admin/validate/v1/NavigationItemValidate.php index ee3e7c7e..103f2fa3 100644 --- a/app/admin/validate/v1/NavigationItemValidate.php +++ b/app/admin/validate/v1/NavigationItemValidate.php @@ -20,7 +20,9 @@ class NavigationItemValidate extends Validate 'nav_id' => 'require|integer', 'pid' => 'integer|different:id|checkPidNotBeChildren', 'name' => 'require|max:64', + 'desc' => 'max:255', 'icon' => 'max:64', + 'image' => 'max:255', 'link_to' => 'require|max:64|in:article,article_category,product,product_category,system_page,custom', 'link' => 'max:255', 'sort' => 'integer', @@ -44,7 +46,9 @@ class NavigationItemValidate extends Validate 'pid.checkPidNotBeChildren' => '父级ID不能为自身的子导航', 'name.require' => '导航名称不能为空', 'name.max' => '导航名称最多不能超过64个字符', + 'desc.max' => '导航名称最多不能超过:rule个字符', 'icon.max' => '图标最多不能超过64个字符', + 'image.max' => '图标最多不能超过:rule个字符', 'link_to.require' => '链接类型不能为空', 'link_to.max' => '链接类型最多不能超过64个字符', 'link_to.in' => '链接类型必须是article,article_category,product_category,product,system_page,custom中之一', @@ -67,7 +71,7 @@ class NavigationItemValidate extends Validate if (env('DB_VERSION', '5') == '8') { $children = Db::query( preg_replace( - '/\s+/u', + '/\s+/u', ' ', "WITH RECURSIVE tree_by AS ( SELECT a.id, a.pid FROM $table_name a WHERE a.id = {$data['id']} @@ -80,13 +84,13 @@ class NavigationItemValidate extends Validate } else { $children = \think\facade\Db::query(" SELECT t2.id - FROM ( - SELECT + FROM ( + SELECT @r AS _id, (SELECT @r := GROUP_CONCAT(id) FROM $table_name WHERE FIND_IN_SET(pid, _id)) AS parent_id - FROM - (SELECT @r := {$data['id']}) vars, $table_name h - WHERE @r <> 0) t1 - JOIN $table_name t2 + FROM + (SELECT @r := {$data['id']}) vars, $table_name h + WHERE @r <> 0) t1 + JOIN $table_name t2 ON FIND_IN_SET(t2.pid, t1._id) ORDER BY t2.id; "); diff --git a/app/admin/validate/v1/ProductCategoryRecommendValidate.php b/app/admin/validate/v1/ProductCategoryRecommendValidate.php new file mode 100644 index 00000000..6c0a947b --- /dev/null +++ b/app/admin/validate/v1/ProductCategoryRecommendValidate.php @@ -0,0 +1,63 @@ + ['规则1','规则2'...] + * + * @var array + */ + protected $rule = [ + 'language_id' => 'require|integer', + 'category_id' => 'require|integer', + 'title' => 'require|max:255', + 'image' => 'require|max:255', + 'desc' => 'require|max:255', + 'link' => 'max:500', + 'sort' => 'require|integer', + 'disabled' => 'in:0,1' + ]; + + /** + * 定义错误信息 + * 格式:'字段名.规则名' => '错误信息' + * + * @var array + */ + protected $message = [ + 'id.require' => 'ID不能为空', + 'id.integer' => 'ID必须是整数', + 'language_id.require' => '语言ID不能为空', + 'language_id.integer' => '语言ID必须是整数', + 'category_id.require' => '分类ID不能为空', + 'category_id.integer' => '分类ID必须是整数', + 'title.require' => '标题不能为空', + 'title.max' => '标题长度不能超过:rule个字符', + 'image.require' => '图片不能为空', + 'image.max' => '图片长度不能超过:rule个字符', + 'desc.require' => '描述不能为空', + 'desc.max' => '描述长度不能超过:rule个字符', + 'link.max' => '链接长度不能超过:rule个字符', + 'sort.require' => '排序不能为空', + 'sort.integer' => '排序必须是整数', + 'disabled.in' => '禁用状态只能是0或1', + ]; + + // 新增场景 + protected function sceneCreate() + { + return $this->remove('id', 'require|integer'); + } + + // 更新场景 + protected function sceneUpdate() + { + return $this->append('id', 'require|integer'); + } +} diff --git a/app/admin/validate/v1/ProductValidate.php b/app/admin/validate/v1/ProductValidate.php index 1a1f2d30..0431ba9c 100644 --- a/app/admin/validate/v1/ProductValidate.php +++ b/app/admin/validate/v1/ProductValidate.php @@ -5,8 +5,6 @@ namespace app\admin\validate\v1; use think\Validate; -use function PHPSTORM_META\type; - class ProductValidate extends Validate { /** diff --git a/app/admin/validate/v1/SysBannerValidate.php b/app/admin/validate/v1/SysBannerValidate.php index 9af8c9a8..584134a4 100644 --- a/app/admin/validate/v1/SysBannerValidate.php +++ b/app/admin/validate/v1/SysBannerValidate.php @@ -43,7 +43,6 @@ class SysBannerValidate extends BaseValidate 'at_platform.in' => '显示端口只能是pc或mobile', 'at_page.max' => '页面位置最多255个字符', 'unique_label.max' => '唯一标识最多64个字符', - 'unique_label.mustOmit' => '更新时不能有unique_label字段', 'name.require' => '名称不能为空', 'name.max' => '名称最多64个字符', 'desc.max' => '描述最多255个字符', @@ -61,6 +60,6 @@ class SysBannerValidate extends BaseValidate // 编辑场景 public function sceneEdit() { - return $this->remove('language_id', 'require|integer')->append('unique_label', 'mustOmit'); + return $this->remove('language_id', 'require|integer'); } } diff --git a/app/admin/validate/v1/SysMallStoreEntranceValidate.php b/app/admin/validate/v1/SysMallStoreEntranceValidate.php new file mode 100644 index 00000000..0f2927a6 --- /dev/null +++ b/app/admin/validate/v1/SysMallStoreEntranceValidate.php @@ -0,0 +1,61 @@ + ['规则1','规则2'...] + * + * @var array + */ + protected $rule = [ + 'id' => 'require|integer', + 'language_id' => 'require|integer', + 'name' => 'require|max:255', + 'image' => 'require|max:255', + 'hover_image' => 'max:255', + 'link' => 'url|max:500', + 'sort' => 'require|integer', + 'disabled' => 'in:0,1', + ]; + + /** + * 定义错误信息 + * 格式:'字段名.规则名' => '错误信息' + * + * @var array + */ + protected $message = [ + 'id.require' => 'ID不能为空', + 'id.integer' => 'ID必须是整数', + 'language_id.require' => '语言ID不能为空', + 'language_id.integer' => '语言ID必须是整数', + 'name.require' => '商城名称不能为空', + 'name.max' => '商城名称长度不能超过:rule个字符', + 'image.require' => '图片不能为空', + 'image.max' => '图片长度不能超过:rule个字符', + 'hover_image.max' => '悬浮图长度不能超过:rule个字符', + 'link.url' => '链接地址必须是有效的URL', + 'link.max' => '链接地址长度不能超过:rule个字符', + 'sort.require' => '排序不能为空', + 'sort.integer' => '排序必须是整数', + 'disabled.in' => '禁用状态只能是0或1', + ]; + + // 新增场景 + protected function sceneCreate() + { + return $this->remove('id', 'require|integer'); + } + + // 更新场景 + protected function sceneUpdate() + { + return $this->append('id', 'require|integer'); + } +} diff --git a/app/common/model/ProductCategoryRecommendBaseModel.php b/app/common/model/ProductCategoryRecommendBaseModel.php new file mode 100644 index 00000000..13dff1ac --- /dev/null +++ b/app/common/model/ProductCategoryRecommendBaseModel.php @@ -0,0 +1,33 @@ + 'int', + 'language_id' => 'int', + 'category_id' => 'int', + 'title' => 'string', + 'image' => 'string', + 'desc' => 'string', + 'link' => 'string', + 'sort' => 'int', + 'disabled' => 'int', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + 'deleted_at' => 'datetime' + ]; +} diff --git a/app/common/model/SysMallStoreEntranceBaseModel.php b/app/common/model/SysMallStoreEntranceBaseModel.php new file mode 100644 index 00000000..7e4115c2 --- /dev/null +++ b/app/common/model/SysMallStoreEntranceBaseModel.php @@ -0,0 +1,32 @@ + 'int', + 'language_id' => 'int', + 'name' => 'string', + 'image' => 'string', + 'hover_image' => 'string', + 'link' => 'string', + 'sort' => 'int', + 'disabled' => 'int', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + 'deleted_at' => 'datetime', + ]; +} diff --git a/app/common/model/SysNavigationItemBaseModel.php b/app/common/model/SysNavigationItemBaseModel.php index db3e7597..824604c6 100644 --- a/app/common/model/SysNavigationItemBaseModel.php +++ b/app/common/model/SysNavigationItemBaseModel.php @@ -21,7 +21,9 @@ class SysNavigationItemBaseModel extends BaseModel 'nav_id' => 'int', 'pid' => 'int', 'name' => 'string', + 'desc' => 'string', 'icon' => 'string', + 'image' => 'string', 'link_to' => 'string', 'link' => 'string', 'sort' => 'int', diff --git a/app/index/controller/Common.php b/app/index/controller/Common.php index 703890c8..f5bab337 100644 --- a/app/index/controller/Common.php +++ b/app/index/controller/Common.php @@ -8,6 +8,7 @@ use app\index\model\LanguageModel; use app\index\model\ProductCategoryModel; use app\index\model\ProductModel; use app\index\model\SysConfigModel; +use app\index\model\SysMallStoreEntranceModel; use app\index\model\SysNavigationItemModel; use think\facade\Lang; use think\facade\View; @@ -38,7 +39,7 @@ abstract class Common extends BaseController } // 获取产品分类 - $categorys = $this->getProductCategory($this->lang_id); + $categorys = $this->getProductCategory($this->lang_id, true); // 输出产品分类 View::assign('header_categorys', $categorys); @@ -47,6 +48,9 @@ abstract class Common extends BaseController // 输出热销产品 View::assign('header_hot_products', $hot_products); + // 获取商品购买入口 + View::assign("header_mall_entrance", $this->getMallStoreEntrance($this->lang_id)); + // 输出顶部导航 View::assign('header_navigation', $this->getNavigation('NAV_67f3701f3e831', $this->lang_id)); @@ -78,7 +82,7 @@ abstract class Common extends BaseController } // 获取产品分类 - protected function getProductCategory($language = 1) + protected function getProductCategory($language = 1, $with_recommends = false) { $categorys = ProductCategoryModel::field([ 'id', @@ -87,15 +91,48 @@ abstract class Common extends BaseController 'icon', 'level' ]) + ->when($with_recommends, function($query) { + $query->with(['recommends' => function($query) { + $query->field(['id', 'category_id', 'title', 'image', 'desc', 'link'])->disabled(false) + ->order(['sort' => 'asc', 'id' => 'desc']); + }]); + }) ->language($language) ->displayed() ->order(['sort' => 'asc', 'id' => 'desc']) - ->select(); + ->select() + ->hidden(["recommends.category_id"]); if ($categorys->isEmpty()) { return []; } - return array_to_tree($categorys->toArray(), 0, 'pid', 1, false); + return $this->toTreeAndChunk($categorys->toArray(), 0, 1); + } + private function toTreeAndChunk(array $categorys, int $pid, int|bool $level): array + { + $ret = []; + foreach ($categorys as $item) { + if ($item['pid'] == $pid) { + $lv = $level; + if ($level !== false) { + $item['level'] = $level; + $lv = $level + 1; + } + $children = $this->toTreeAndChunk($categorys, $item['id'], $lv); + if (!empty($children)) { + if ($lv == 1) { + $item['children'] = array_chunk($children, 2); + } else if ($lv == 2) { + $item['children'] = array_chunk($children, 3); + } else { + $item['children'] = $children; + } + } + $ret[] = $item; + } + } + + return $ret; } // 获取顶部导航 @@ -164,6 +201,26 @@ abstract class Common extends BaseController return $languages; } + // 获取商品购买入口 + private function getMallStoreEntrance($language = 1) + { + return SysMallStoreEntranceModel::field([ + 'id', + 'name', + 'image', + 'hover_image', + 'link' + ]) + ->language($language) + ->enabled() + ->order(['sort' => 'asc', 'id' => 'desc']) + ->select() + ?->each(function($item) { + $item->image = thumb($item->image); + return $item; + }); + } + // 获取系统联系方式配置 protected function getSysConfig($language, $group = []) { diff --git a/app/index/controller/Product.php b/app/index/controller/Product.php index fd93d7c3..0d86ca6d 100644 --- a/app/index/controller/Product.php +++ b/app/index/controller/Product.php @@ -149,7 +149,7 @@ class Product extends Common } } View::assign('list', $list); - + return View::fetch('category'); } // 产品分类 - 查看子类 @@ -267,6 +267,59 @@ class Product extends Common return View::fetch('subcategory'); } + + /** + * 单纯分类页 + */ + public function classify() + { + $pid = request()->param('id/d', 0); + + // 获取当前选中的父分类 + $parent = ProductCategoryModel::field(['id', 'name'])->find($pid); + View::assign('parent', $parent); + + // 获取分类及产品信息 + $categorys = ProductCategoryModel::field(['id', 'pid', 'name', 'path', 'level']) + ->language($this->lang_id) + ->displayed(true) + ->children($pid) + ->order(['pid' => 'asc', 'sort' => 'asc', 'id' => 'desc']) + ->select() + ->toArray(); + + // 组装第三级分类所属产品数据 + $lv3_id_arr = array_column(array_filter($categorys, fn($item)=> $item['level'] === 3), 'id'); + $products = ProductModel::field([ + 'id', + 'category_id', + 'name', + 'short_name', + 'cover_image', + 'spu' + ]) + ->byCategory($lv3_id_arr) + ->language($this->lang_id) + ->enabled(true) + ->onSale(true) + ->onShelves(true) + ->select() + ->toArray(); + $product_groups = []; + foreach ($products as $product) { + $product_groups[$product['category_id']][] = $product; + } + foreach ($categorys as $key => $category) { + if ($category['level'] < 3) continue; + $categorys[$key]['products'] = $product_groups[$category['id']] ?? []; + } + + $tree = array_to_tree($categorys, $pid); + View::assign('categorys', $tree); + + return View::fetch('classify'); + } + /** * 产品搜索 */ @@ -329,7 +382,7 @@ class Product extends Common ->bypk($param['id']) ->find(); View::assign('product', $product); - + $product_categorys = []; $product_params = []; $product_skus = []; @@ -352,7 +405,7 @@ class Product extends Common ->order(['id' => 'asc']) ->select() ->toArray(); - + // 获取产品参数信息 $product_params = ProductParamsModel::field(['id', 'name', 'value']) ->byProductId($product['id']) @@ -375,7 +428,7 @@ class Product extends Common $attrs = ProductAttrModel::bypks(array_unique(Arr::pluck($sku_attrs, 'attr_id')))->column(['attr_name'], 'id'); foreach ($sku_attrs as $v) { if (empty($v['attr_value'])) continue; - + $v['attr_name'] = $attrs[$v['attr_id']]?? ''; // 按属性分组 $product_sku_attrs[$v['attr_id']]['attr_id'] = $v['attr_id']; @@ -422,8 +475,8 @@ class Product extends Common // 获取询盘可选国家 $config = $this->basic_config['optional_country_for_product_inquiry']; - View::assign('country_list', explode(',', preg_replace('/\r?\n/', ',', $config['value']?? ''))); - + View::assign('country_list', explode(',', preg_replace('/\r?\n/', ',', $config['value']?? ''))); + return View::fetch('detail'); } @@ -535,7 +588,7 @@ class Product extends Common if ($val['level'] != 2) { continue; } - + foreach ($pro_map as $k => $pro) { if (in_array($val['id'], explode(',', strval($k)))) { $newpros[] = [ @@ -549,7 +602,7 @@ class Product extends Common } } View::assign('newpros', $newpros); - + return View::fetch('newpro'); } } diff --git a/app/index/lang/en-us/mobile.php b/app/index/lang/en-us/mobile.php index 4e0cde9b..977199f5 100644 --- a/app/index/lang/en-us/mobile.php +++ b/app/index/lang/en-us/mobile.php @@ -6,6 +6,9 @@ return [ '产品列表' => 'Products', '搜索' => 'Search', '搜索历史' => 'Search History', + '请输入搜索关键词' => 'Please enter a search keyword', + '搜索记录' => 'Search History', + '清空' => 'Clear', '请择地区' => 'SELECT A REGION', '产品' => 'Product', '联系方式' => 'Contact', diff --git a/app/index/lang/en-us/pc.php b/app/index/lang/en-us/pc.php index 4aa02b3f..73452f7a 100644 --- a/app/index/lang/en-us/pc.php +++ b/app/index/lang/en-us/pc.php @@ -5,10 +5,17 @@ return [ '产品列表' => 'Products', '店铺' => 'Store', '搜索记录' => 'Search History', - '热销产品' => 'Popular Products', '产品' => 'Product', '联系我们' => 'Contact', + // 新导航栏 - 2023-03-31 + '搜索' => 'Search', + '搜索产品、分类...' => 'Search products and categories...', + '最近搜索' => 'Recent Searches', + '清空' => 'Clear', + '热销产品' => 'Popular Products', + '购买' => 'Buy', + // 返回文本 '提交成功' => 'success', '提交失败' => 'fail', diff --git a/app/index/model/ProductCategoryModel.php b/app/index/model/ProductCategoryModel.php index a6ea7e5e..7846820a 100644 --- a/app/index/model/ProductCategoryModel.php +++ b/app/index/model/ProductCategoryModel.php @@ -17,6 +17,12 @@ class ProductCategoryModel extends ProductCategoryBaseModel // 软件删除时间字段 protected $deleteTime = 'deleted_at'; + // 关联产品推荐 + public function recommends() + { + return $this->hasMany(ProductCategoryRecommendModel::class, 'category_id', 'id'); + } + // 所属语言范围查询 public function scopeLanguage($query, $language) { diff --git a/app/index/model/ProductCategoryRecommendModel.php b/app/index/model/ProductCategoryRecommendModel.php new file mode 100644 index 00000000..b8267082 --- /dev/null +++ b/app/index/model/ProductCategoryRecommendModel.php @@ -0,0 +1,30 @@ +where($this->getTable() . '.language_id', '=', $language); + } + + public function scopeDisabled($query, $disabled = true) + { + $query->where($this->getTable() . '.disabled', '=', (int)$disabled); + } +} diff --git a/app/index/model/SysMallStoreEntranceModel.php b/app/index/model/SysMallStoreEntranceModel.php new file mode 100644 index 00000000..df07e042 --- /dev/null +++ b/app/index/model/SysMallStoreEntranceModel.php @@ -0,0 +1,43 @@ +belongsTo(\app\index\model\LanguageModel::class, 'language_id', 'id'); + } + + // 所属语言范围查询 + public function scopeLanguage($query, $language) + { + $query->where($this->getTable() . '.language_id', '=', $language); + } + + // 查询启用状态 + public function scopeEnabled($query) + { + $query->where('disabled', '=', 0); + } + + // 查询禁用状态 + public function scopeDisabled($query) + { + $query->where('disabled', '=', 1); + } +} diff --git a/app/index/route/route.php b/app/index/route/route.php index 68203058..af41b7c0 100644 --- a/app/index/route/route.php +++ b/app/index/route/route.php @@ -18,6 +18,8 @@ Route::group('product', function () { Route::get('category/:id', 'Product/category'); // 产品分类 - 查看子类 Route::get('subcategory/:id', 'Product/subcategory'); + // 单纯分类页 + Route::get('classify/:id', 'Product/classify'); // 产品详情页 Route::get('detail/:id', 'Product/detail'); // 产品询盘 diff --git a/app/index/view/mobile/index/index.html b/app/index/view/mobile/index/index.html index 4260f198..b704c5e9 100644 --- a/app/index/view/mobile/index/index.html +++ b/app/index/view/mobile/index/index.html @@ -9,7 +9,7 @@