ensureDefaultModules(); $modules = HomeModule::query() ->withCount('items') ->orderBy('sort_order') ->orderBy('id') ->get(); return view('admin.settings.index', [ 'modules' => $modules, ]); } public function show(HomeModule $module): View { $this->ensureDefaultModules(); $module->load([ 'items' => fn ($query) => $query->orderBy('sort_order')->orderBy('id'), ]); return view('admin.settings.show', [ 'module' => $module, 'moduleNav' => $this->moduleNavigation(), 'routeOptions' => $this->routeOptions(), ]); } public function update(Request $request, HomeModule $module): RedirectResponse { $validated = $request->validate([ 'name' => ['required', 'string', 'max:120'], 'title' => ['nullable', 'string', 'max:160'], 'subtitle' => ['nullable', 'string', 'max:255'], 'enabled' => ['nullable', 'boolean'], 'sort_order' => ['nullable', 'integer', 'min:0', 'max:9999'], 'limit' => ['nullable', 'integer', 'min:1', 'max:30'], 'more_link_type' => ['nullable', Rule::in(['route', 'url'])], 'more_link_target' => ['nullable', 'string', 'max:255'], 'extra.side_title' => ['nullable', 'string', 'max:120'], 'extra.side_subtitle' => ['nullable', 'string', 'max:255'], ]); $module->fill([ 'name' => $validated['name'], 'title' => $validated['title'] ?? null, 'subtitle' => $validated['subtitle'] ?? null, 'enabled' => (bool) ($validated['enabled'] ?? false), 'sort_order' => (int) ($validated['sort_order'] ?? $module->sort_order), 'limit' => (int) ($validated['limit'] ?? $module->limit), 'more_link_type' => $validated['more_link_type'] ?? null, 'more_link_target' => $validated['more_link_target'] ?? null, 'extra' => [ 'side_title' => data_get($validated, 'extra.side_title'), 'side_subtitle' => data_get($validated, 'extra.side_subtitle'), ], ]); $this->validateModuleLink($module->module_key, $module->more_link_type, $module->more_link_target); $module->save(); $this->clearHomeCaches(); return redirect() ->route('admin.settings.show', $module) ->with('status', '模块配置已更新。'); } public function storeItem(Request $request, HomeModule $module): RedirectResponse { $validated = $request->validate([ 'title' => ['nullable', 'string', 'max:160'], 'subtitle' => ['nullable', 'string', 'max:255'], 'image_path' => ['nullable', 'string', 'max:255'], 'link_type' => ['required', Rule::in(['route', 'url'])], 'link_target' => ['nullable', 'string', 'max:255'], 'sort_order' => ['nullable', 'integer', 'min:0', 'max:9999'], 'enabled' => ['nullable', 'boolean'], ]); $this->validateItemRules($module->module_key, $validated); $this->validateItemLink($module->module_key, $validated['link_type'], $validated['link_target'] ?? null); $module->items()->create([ 'title' => $validated['title'] ?? null, 'subtitle' => $validated['subtitle'] ?? null, 'image_path' => $validated['image_path'] ?? null, 'link_type' => $validated['link_type'], 'link_target' => $validated['link_target'] ?? null, 'sort_order' => (int) ($validated['sort_order'] ?? (($module->items()->max('sort_order') ?? 0) + 10)), 'enabled' => (bool) ($validated['enabled'] ?? true), ]); $this->clearHomeCaches(); return redirect() ->route('admin.settings.show', $module) ->with('status', '模块条目已新增。'); } public function editItem(HomeModule $module, HomeModuleItem $item): View { if ($item->home_module_id !== $module->id) { abort(404); } return view('admin.settings.item-form', [ 'module' => $module, 'item' => $item, 'moduleNav' => $this->moduleNavigation(), 'routeOptions' => $this->routeOptions(), ]); } public function updateItem(Request $request, HomeModule $module, HomeModuleItem $item): RedirectResponse { if ($item->home_module_id !== $module->id) { abort(404); } $validated = $request->validate([ 'title' => ['nullable', 'string', 'max:160'], 'subtitle' => ['nullable', 'string', 'max:255'], 'image_path' => ['nullable', 'string', 'max:255'], 'link_type' => ['required', Rule::in(['route', 'url'])], 'link_target' => ['nullable', 'string', 'max:255'], 'sort_order' => ['nullable', 'integer', 'min:0', 'max:9999'], 'enabled' => ['nullable', 'boolean'], ]); $this->validateItemRules($module->module_key, $validated); $this->validateItemLink($module->module_key, $validated['link_type'], $validated['link_target'] ?? null); $item->fill([ 'title' => $validated['title'] ?? null, 'subtitle' => $validated['subtitle'] ?? null, 'image_path' => $validated['image_path'] ?? null, 'link_type' => $validated['link_type'], 'link_target' => $validated['link_target'] ?? null, 'sort_order' => (int) ($validated['sort_order'] ?? $item->sort_order), 'enabled' => (bool) ($validated['enabled'] ?? false), ])->save(); $this->clearHomeCaches(); return redirect() ->route('admin.settings.show', $module) ->with('status', '模块条目已更新。'); } public function destroyItem(HomeModule $module, HomeModuleItem $item): RedirectResponse { if ($item->home_module_id !== $module->id) { abort(404); } $item->delete(); $this->clearHomeCaches(); return redirect() ->route('admin.settings.show', $module) ->with('status', '模块条目已删除。'); } /** * @return array */ private function routeOptions(): array { $allRouteNames = collect(RouteFacade::getRoutes()->getRoutesByName()) ->filter(function ($route, $name): bool { if (!is_string($name) || $name === '') { return false; } if (! method_exists($route, 'parameterNames')) { return false; } return count($route->parameterNames()) === 0; }) ->keys() ->map(fn ($name) => (string) $name) ->filter(fn (string $name) => ! str_starts_with($name, 'admin.')) ->filter(fn (string $name) => ! str_starts_with($name, 'debugbar.')) ->filter(fn (string $name) => ! str_starts_with($name, 'ignition.')) ->filter(fn (string $name) => ! str_starts_with($name, '_')) ->values(); $priority = [ 'home', 'tools.index', 'tools.list', 'models.index', 'news.index', 'guides.index', ]; $priorityRoutes = collect($priority) ->filter(fn (string $name) => $allRouteNames->contains($name)); $remainingRoutes = $allRouteNames ->reject(fn (string $name) => in_array($name, $priority, true)) ->sort() ->values(); return $priorityRoutes ->concat($remainingRoutes) ->map(fn (string $name) => [ 'name' => $this->routeLabel($name), 'value' => $name, ]) ->values() ->all(); } private function routeLabel(string $name): string { return match ($name) { 'home' => '首页', 'tools.index' => '工具首页', 'tools.list' => '工具列表', 'models.index' => '模型推荐', 'news.index' => '文章资讯', 'guides.index' => '教程学习', default => $name, }; } private function moduleNavigation() { return HomeModule::query() ->orderBy('sort_order') ->orderBy('id') ->get(['id', 'name', 'module_key']); } private function ensureDefaultModules(): void { $defaults = [ ['key' => 'channel_cards', 'name' => '频道卡片', 'title' => '频道入口', 'sort' => 20, 'limit' => 5], ['key' => 'promo_banners', 'name' => '横幅推荐', 'title' => '横幅推荐', 'sort' => 30, 'limit' => 2], ['key' => 'hot_tools', 'name' => '热门工具', 'title' => '热门工具', 'sort' => 40, 'limit' => 18], ['key' => 'latest_tools', 'name' => '最新收录', 'title' => '最新收录', 'sort' => 50, 'limit' => 18], ['key' => 'category_sections', 'name' => '分类分块', 'title' => '分类分块', 'sort' => 60, 'limit' => 18], ]; foreach ($defaults as $default) { HomeModule::query()->firstOrCreate([ 'module_key' => $default['key'], ], [ 'name' => $default['name'], 'title' => $default['title'], 'enabled' => true, 'sort_order' => $default['sort'], 'limit' => $default['limit'], ]); } } private function validateModuleLink(string $moduleKey, ?string $type, ?string $target): void { if (!in_array($moduleKey, ['hot_tools', 'latest_tools', 'category_sections'], true)) { return; } $target = trim((string) $target); if ($type === null && $target === '') { return; } if ($type === 'route') { if ($target === '') { abort(422, '请选择有效的“更多链接”路由。'); } if (! in_array($target, array_column($this->routeOptions(), 'value'), true)) { abort(422, '模块“更多链接”路由不在允许范围内。'); } return; } if ($type === 'url' && $target !== '' && ! $this->isValidUrlOrPath($target)) { abort(422, '模块“更多链接”URL格式不正确。'); } } private function validateItemRules(string $moduleKey, array $item): void { if (! in_array($moduleKey, ['channel_cards', 'promo_banners'], true)) { return; } $title = trim((string) ($item['title'] ?? '')); $imagePath = trim((string) ($item['image_path'] ?? '')); $linkTarget = trim((string) ($item['link_target'] ?? '')); if ($title === '' || $imagePath === '' || $linkTarget === '') { abort(422, '频道卡片/横幅推荐条目必须填写标题、图片、链接。'); } } private function validateItemLink(string $moduleKey, string $type, ?string $target): void { $target = trim((string) $target); $isStrictModule = in_array($moduleKey, ['channel_cards', 'promo_banners'], true); if ($type === 'route') { if ($target === '') { if ($isStrictModule) { abort(422, '频道卡片/横幅推荐条目必须填写链接目标。'); } return; } if (! in_array($target, array_column($this->routeOptions(), 'value'), true)) { abort(422, '请选择有效的前台路由,或将链接类型改为 URL。'); } return; } if ($target === '') { if ($isStrictModule) { abort(422, '频道卡片/横幅推荐条目必须填写链接目标。'); } return; } if (! $this->isValidUrlOrPath($target)) { abort(422, '条目URL格式不正确。'); } } private function isValidUrlOrPath(string $value): bool { if (str_starts_with($value, '/')) { return true; } return (bool) filter_var($value, FILTER_VALIDATE_URL); } private function clearHomeCaches(): void { Cache::flush(); } }