with(['children' => fn ($q) => $q->where('status', 'published')]) ->whereNull('parent_id') ->where('status', 'published') ->where('show_in_nav', true) ->orderByDesc('sort') ->orderBy('id') ->get(); return $this->ok([ 'site_name' => Settings::get('site_name', '轻舟'), 'description' => Settings::get('site_description', ''), 'icp' => Settings::get('icp', ''), 'mail_enabled' => Settings::bool('mail_enabled'), 'nav' => $this->nav('header'), 'footer' => $this->nav('footer'), 'categories' => $categories->map(fn (Category $category) => $this->categoryNode($category)), ]); } public function home() { $sections = HomeSection::query() ->with(['tools' => fn ($q) => $q->published()->with('tags')]) ->where('status', 'published') ->orderByDesc('sort') ->orderBy('id') ->get(); $banners = Banner::query()->where('status', 'published') ->where(function ($q) { $q->whereNull('starts_at')->orWhere('starts_at', '<=', now()); }) ->where(function ($q) { $q->whereNull('ends_at')->orWhere('ends_at', '>=', now()); }) ->orderByDesc('sort')->get(); $announcement = Article::query()->published()->where('type', 'announcement')->latest('published_at')->first(); return $this->ok([ 'sections' => $sections->map(fn (HomeSection $section) => [ 'id' => $section->id, 'title' => $section->title, 'slug' => $section->slug, 'tools' => $section->tools->take($section->display_limit)->map(fn (Tool $tool) => Presenter::toolCard($tool))->values(), ]), 'banners' => $banners->map(fn (Banner $banner) => [ 'id' => $banner->id, 'title' => $banner->title, 'image' => $banner->image, 'link' => $banner->link, ]), 'announcement' => $announcement ? [ 'title' => $announcement->title, 'slug' => $announcement->slug, 'summary' => $announcement->summary, ] : null, ]); } public function category(Request $request, string $slug) { $category = Category::query()->with('children')->where('slug', $slug)->where('status', 'published')->firstOrFail(); $ids = $category->children->pluck('id')->push($category->id); $query = Tool::query()->published()->with('tags')->whereHas('categories', fn ($q) => $q->whereIn('categories.id', $ids)); $this->applySort($query, (string) $request->query('sort', 'recommend')); $page = $query->paginate(24); return $this->ok([ 'category' => $this->categoryNode($category), 'items' => collect($page->items())->map(fn (Tool $tool) => Presenter::toolCard($tool))->values(), 'total' => $page->total(), 'page' => $page->currentPage(), 'per_page' => $page->perPage(), ]); } public function tool(string $slug) { $tool = Tool::query()->published()->with('tags', 'categories')->where('slug', $slug)->firstOrFail(); $related = Tool::query()->published()->with('tags') ->whereKeyNot($tool->id) ->whereHas('categories', fn ($q) => $q->whereIn('categories.id', $tool->categories->pluck('id'))) ->limit(4) ->get(); return $this->ok(Presenter::toolDetail($tool) + [ 'related' => $related->map(fn (Tool $item) => Presenter::toolCard($item))->values(), ]); } public function go(string $slug) { $tool = Tool::query()->published()->where('slug', $slug)->where('type', 'link')->firstOrFail(); $tool->increment('clicks'); return redirect()->away($tool->open_url); } public function search(Request $request) { $keyword = trim((string) $request->query('q', '')); $scope = (string) $request->query('scope', 'tools'); $like = '%'.addcslashes($keyword, '%_\\').'%'; if ($scope === 'help') { $query = Article::query()->published()->where('type', 'help'); if ($keyword !== '') { $query->where(fn ($q) => $q->where('title', 'like', $like)->orWhere('summary', 'like', $like)); } $page = $query->latest('published_at')->paginate(24); return $this->ok([ 'items' => collect($page->items())->map(fn (Article $article) => [ 'title' => $article->title, 'slug' => $article->slug, 'summary' => $article->summary, ])->values(), 'total' => $page->total(), 'page' => $page->currentPage(), 'per_page' => $page->perPage(), ]); } if ($scope === 'recent') { $user = $request->user(); if (! $user) { return response()->json(['message' => '请先登录', 'code' => 'unauthenticated'], 401); } $opens = UserToolOpen::query()->with('tool.tags')->where('user_id', $user->id)->orderByDesc('last_opened_at')->get(); $items = $opens->filter(function (UserToolOpen $open) use ($keyword) { return $open->tool && $open->tool->status === 'published' && ($keyword === '' || str_contains($open->tool->name, $keyword) || str_contains($open->tool->summary, $keyword)); })->map(fn (UserToolOpen $open) => Presenter::toolCard($open->tool))->values(); return $this->ok(['items' => $items, 'total' => $items->count(), 'page' => 1, 'per_page' => $items->count()]); } $query = Tool::query()->published()->with('tags'); if ($keyword !== '') { $query->where(function ($q) use ($like) { $q->where('name', 'like', $like) ->orWhere('summary', 'like', $like) ->orWhereHas('tags', fn ($tag) => $tag->where('name', 'like', $like)); }); } $page = $query->orderByDesc('is_recommended')->orderByDesc('sort')->paginate(24); $hot = $page->total() === 0 ? Tool::query()->published()->with('tags')->orderByDesc('is_recommended')->limit(8)->get() : collect(); return $this->ok([ 'items' => collect($page->items())->map(fn (Tool $tool) => Presenter::toolCard($tool))->values(), 'hot' => $hot->map(fn (Tool $tool) => Presenter::toolCard($tool))->values(), 'total' => $page->total(), 'page' => $page->currentPage(), 'per_page' => $page->perPage(), ]); } public function plans() { $plans = Plan::query()->with('entitlements')->where('status', 'published')->orderByDesc('sort')->orderBy('id')->get(); return $this->ok($plans->map(fn (Plan $plan) => Presenter::plan($plan))->values()); } public function articles(Request $request) { $type = (string) $request->query('type', 'help'); $items = Article::query()->published()->where('type', $type)->latest('published_at')->get(); return $this->ok($items->map(fn (Article $article) => [ 'title' => $article->title, 'slug' => $article->slug, 'summary' => $article->summary, 'published_at' => $article->published_at?->toIso8601String(), ])->values()); } public function article(string $slug) { $article = Article::query()->published()->where('slug', $slug)->firstOrFail(); return $this->ok([ 'title' => $article->title, 'slug' => $article->slug, 'summary' => $article->summary, 'body' => $article->body, 'type' => $article->type, 'published_at' => $article->published_at?->toIso8601String(), ]); } public function agreement(string $type) { $agreement = Agreement::query()->where('type', $type)->firstOrFail(); return $this->ok([ 'type' => $agreement->type, 'title' => $agreement->title, 'body' => $agreement->body, ]); } private function nav(string $group): array { return Navigation::query()->where('group_name', $group)->where('status', 'published')->orderByDesc('sort')->get() ->map(fn (Navigation $item) => ['title' => $item->title, 'link' => $item->link])->all(); } private function categoryNode(Category $category): array { return [ 'id' => $category->id, 'name' => $category->name, 'slug' => $category->slug, 'icon' => $category->icon, 'children' => $category->children->map(fn (Category $child) => [ 'id' => $child->id, 'name' => $child->name, 'slug' => $child->slug, 'icon' => $child->icon, ])->values(), ]; } private function applySort($query, string $sort): void { match ($sort) { 'latest' => $query->orderByDesc('published_at'), 'name' => $query->orderBy('name'), default => $query->orderByDesc('is_recommended')->orderByDesc('sort'), }; } }