|
|
@@ -0,0 +1,271 @@
|
|
|
+<?php
|
|
|
+
|
|
|
+namespace Webkul\Shop\Http\Controllers\API;
|
|
|
+
|
|
|
+use Illuminate\Http\JsonResponse;
|
|
|
+use Webkul\Category\Repositories\CategoryRepository;
|
|
|
+use Webkul\Product\Repositories\ProductRepository;
|
|
|
+use Webkul\Theme\Repositories\ThemeCustomizationRepository;
|
|
|
+
|
|
|
+class HomeController extends APIController
|
|
|
+{
|
|
|
+ /**
|
|
|
+ * Create a new controller instance.
|
|
|
+ */
|
|
|
+ public function __construct(
|
|
|
+ protected ThemeCustomizationRepository $themeCustomizationRepository,
|
|
|
+ protected CategoryRepository $categoryRepository,
|
|
|
+ protected ProductRepository $productRepository,
|
|
|
+ ) {}
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取首页完整数据
|
|
|
+ *
|
|
|
+ * GET /api/home
|
|
|
+ *
|
|
|
+ * @return JsonResponse
|
|
|
+ */
|
|
|
+ public function index(): JsonResponse
|
|
|
+ {
|
|
|
+ $channel = core()->getCurrentChannel();
|
|
|
+ $locale = core()->getRequestedLocaleCode();
|
|
|
+
|
|
|
+ // 1. 获取主题自定义数据(banner、分类轮播、产品轮播等)
|
|
|
+ $customizations = $this->themeCustomizationRepository
|
|
|
+ ->orderBy('sort_order')
|
|
|
+ ->findWhere([
|
|
|
+ 'status' => 1,
|
|
|
+ 'channel_id' => $channel->id,
|
|
|
+ 'theme_code' => $channel->theme,
|
|
|
+ ]);
|
|
|
+
|
|
|
+ $sections = [];
|
|
|
+ foreach ($customizations as $item) {
|
|
|
+ $section = $this->formatSection($item, $locale);
|
|
|
+ if ($section !== null) {
|
|
|
+ $sections[] = $section;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 获取分类树(供导航使用)
|
|
|
+ $categories = $this->categoryRepository->getVisibleCategoryTree(
|
|
|
+ $channel->root_category_id
|
|
|
+ );
|
|
|
+
|
|
|
+ return response()->json([
|
|
|
+ 'success' => true,
|
|
|
+ 'data' => [
|
|
|
+ 'sections' => $sections,
|
|
|
+ 'categories' => $this->formatCategoryTree($categories),
|
|
|
+ ],
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 格式化单个主题区块
|
|
|
+ */
|
|
|
+ protected function formatSection($item, string $locale): ?array
|
|
|
+ {
|
|
|
+ $base = [
|
|
|
+ 'id' => $item->id,
|
|
|
+ 'type' => $item->type,
|
|
|
+ 'name' => $item->name,
|
|
|
+ 'sort_order'=> (int) $item->sort_order,
|
|
|
+ ];
|
|
|
+
|
|
|
+ $options = $item->translate($locale)?->options ?? [];
|
|
|
+
|
|
|
+ switch ($item->type) {
|
|
|
+ case 'image_carousel':
|
|
|
+ return array_merge($base, [
|
|
|
+ 'images' => $options['images'] ?? [],
|
|
|
+ ]);
|
|
|
+
|
|
|
+ case 'static_content':
|
|
|
+ return array_merge($base, [
|
|
|
+ 'html' => $options['html'] ?? '',
|
|
|
+ 'css' => $options['css'] ?? '',
|
|
|
+ ]);
|
|
|
+
|
|
|
+ case 'category_carousel':
|
|
|
+ $filters = $options['filters'] ?? [];
|
|
|
+ return array_merge($base, [
|
|
|
+ 'title' => $options['title'] ?? '',
|
|
|
+ 'categories' => $this->getCategoryCarouselData($filters),
|
|
|
+ ]);
|
|
|
+
|
|
|
+ case 'product_carousel':
|
|
|
+ $filters = $options['filters'] ?? [];
|
|
|
+ return array_merge($base, [
|
|
|
+ 'title' => $options['title'] ?? '',
|
|
|
+ 'products' => $this->getProductCarouselData($filters),
|
|
|
+ ]);
|
|
|
+
|
|
|
+ case 'services_content':
|
|
|
+ return array_merge($base, [
|
|
|
+ 'services' => $options['services'] ?? [],
|
|
|
+ ]);
|
|
|
+
|
|
|
+ default:
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取分类轮播数据
|
|
|
+ */
|
|
|
+ protected function getCategoryCarouselData(array $filters): array
|
|
|
+ {
|
|
|
+ // 1. 收集所有 parent_id(兼容字符串单值、字符串数组两种格式)
|
|
|
+ $parentIds = $this->extractParentIds($filters);
|
|
|
+
|
|
|
+ $sort = $filters['sort'] ?? 'asc';
|
|
|
+ $limit = (int) ($filters['limit'] ?? 10);
|
|
|
+
|
|
|
+ // 2. 无 parent_id:按 limit 查询
|
|
|
+ if (empty($parentIds)) {
|
|
|
+ $categories = $this->categoryRepository->getAll([
|
|
|
+ 'status' => 1,
|
|
|
+ 'sort' => $sort,
|
|
|
+ 'limit' => $limit,
|
|
|
+ 'locale' => core()->getRequestedLocaleCode(),
|
|
|
+ ]);
|
|
|
+
|
|
|
+ return $this->formatCategories($categories);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 多个 parent_id:按每个 parent 平均分配 limit 后合并去重
|
|
|
+ $perParent = max((int) ceil($limit / count($parentIds)), 1);
|
|
|
+ $result = [];
|
|
|
+ $seenIds = [];
|
|
|
+
|
|
|
+ foreach ($parentIds as $parentId) {
|
|
|
+ $categories = $this->categoryRepository->getAll([
|
|
|
+ 'status' => 1,
|
|
|
+ 'sort' => $sort,
|
|
|
+ 'limit' => $perParent,
|
|
|
+ 'parent_id' => $parentId,
|
|
|
+ 'locale' => core()->getRequestedLocaleCode(),
|
|
|
+ ]);
|
|
|
+
|
|
|
+ foreach ($categories as $cat) {
|
|
|
+ if (isset($seenIds[$cat->id])) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ $seenIds[$cat->id] = true;
|
|
|
+ $result[] = $cat;
|
|
|
+ if (count($result) >= $limit) {
|
|
|
+ break 2;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return $this->formatCategories(collect($result));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 从 filters 中提取所有 parent_id(支持单值或多值数组)
|
|
|
+ */
|
|
|
+ protected function extractParentIds(array $filters): array
|
|
|
+ {
|
|
|
+ if (empty($filters['parent_id'])) {
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+
|
|
|
+ $values = $filters['parent_id'];
|
|
|
+
|
|
|
+ // 已是数组
|
|
|
+ if (is_array($values)) {
|
|
|
+ return array_values(array_filter(array_map('intval', $values), fn ($v) => $v > 0));
|
|
|
+ }
|
|
|
+
|
|
|
+ // 单值字符串,去掉多余空白
|
|
|
+ return array_values(array_filter(array_map('intval', explode(',', (string) $values)),
|
|
|
+ fn ($v) => $v > 0));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 格式化分类集合为数组
|
|
|
+ */
|
|
|
+ protected function formatCategories($categories): array
|
|
|
+ {
|
|
|
+ return $categories->map(fn ($cat) => [
|
|
|
+ 'id' => $cat->id,
|
|
|
+ 'name' => $cat->name,
|
|
|
+ 'slug' => $cat->slug,
|
|
|
+ 'description' => $cat->description,
|
|
|
+ 'image_url' => $cat->image_url,
|
|
|
+ 'banner_url' => $cat->banner_url,
|
|
|
+ 'product_count' => (int) ($cat->products_count ?? 0),
|
|
|
+ ])->values()->toArray();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取产品轮播数据
|
|
|
+ */
|
|
|
+ protected function getProductCarouselData(array $filters): array
|
|
|
+ {
|
|
|
+ $params = [
|
|
|
+ 'status' => 1,
|
|
|
+ 'visible_individually'=> 1,
|
|
|
+ 'limit' => $filters['limit'] ?? 12,
|
|
|
+ 'sort' => $filters['sort'] ?? 'name-asc',
|
|
|
+ ];
|
|
|
+
|
|
|
+ if (! empty($filters['new'])) {
|
|
|
+ $params['new'] = 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (! empty($filters['featured'])) {
|
|
|
+ $params['featured'] = 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ $products = $this->productRepository->getAll($params);
|
|
|
+
|
|
|
+ return $products->map(function ($product) {
|
|
|
+ $image = $product->images->first()
|
|
|
+ ?? $product->base_image;
|
|
|
+
|
|
|
+ return [
|
|
|
+ 'id' => $product->id,
|
|
|
+ 'sku' => $product->sku,
|
|
|
+ 'name' => $product->name,
|
|
|
+ 'slug' => $product->slug,
|
|
|
+ 'type' => $product->type,
|
|
|
+ 'price' => $product->getTypeInstance()->getMinimalPrice(),
|
|
|
+ 'special_price' => $product->special_price,
|
|
|
+ 'image_url' => $image->url ?? null,
|
|
|
+ 'is_new' => (bool) ($product->new ?? false),
|
|
|
+ 'is_featured' => (bool) ($product->featured ?? false),
|
|
|
+ 'short_description'=> $product->short_description,
|
|
|
+ ];
|
|
|
+ })->values()->toArray();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 格式化分类树
|
|
|
+ */
|
|
|
+ protected function formatCategoryTree($categories): array
|
|
|
+ {
|
|
|
+ $result = [];
|
|
|
+
|
|
|
+ foreach ($categories as $category) {
|
|
|
+ $node = [
|
|
|
+ 'id' => $category->id,
|
|
|
+ 'name' => $category->name,
|
|
|
+ 'slug' => $category->slug,
|
|
|
+ 'description' => $category->description,
|
|
|
+ 'image_url' => $category->image_url,
|
|
|
+ 'banner_url' => $category->banner_url,
|
|
|
+ ];
|
|
|
+
|
|
|
+ if ($category->children && $category->children->count() > 0) {
|
|
|
+ $node['children'] = $this->formatCategoryTree($category->children);
|
|
|
+ }
|
|
|
+
|
|
|
+ $result[] = $node;
|
|
|
+ }
|
|
|
+
|
|
|
+ return $result;
|
|
|
+ }
|
|
|
+}
|