搭建轻舟一期门户、后台和统一登录。
让分类、会员和示例工具共用同一账号,本地即可跑通登录与授权。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,255 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Agreement;
|
||||
use App\Models\Article;
|
||||
use App\Models\Banner;
|
||||
use App\Models\Category;
|
||||
use App\Models\HomeSection;
|
||||
use App\Models\Navigation;
|
||||
use App\Models\Plan;
|
||||
use App\Models\Tool;
|
||||
use App\Models\UserToolOpen;
|
||||
use App\Support\Presenter;
|
||||
use App\Support\Settings;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class PortalController extends Controller
|
||||
{
|
||||
public function meta()
|
||||
{
|
||||
$categories = Category::query()
|
||||
->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'),
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user