with(['items' => fn ($query) => $query->orderBy('sort_order')->orderBy('id')]) ->orderBy('sort_order') ->orderBy('id') ->get(); if ($modules->isEmpty()) { $this->ensureDefaultModules(); $modules = HomeModule::query() ->with(['items' => fn ($query) => $query->orderBy('sort_order')->orderBy('id')]) ->orderBy('sort_order') ->orderBy('id') ->get(); } return view('admin.settings.index', [ 'modules' => $modules, 'routeOptions' => $this->routeOptions(), ]); } public function update(Request $request): RedirectResponse { $moduleData = $request->input('modules', []); if (!is_array($moduleData) || $moduleData === []) { return redirect() ->route('admin.settings.index') ->with('status', '未提交有效的模块配置。'); } foreach ($moduleData as $moduleId => $moduleInput) { if (!is_numeric($moduleId) || !is_array($moduleInput)) { continue; } $module = HomeModule::query()->find((int) $moduleId); if (! $module instanceof HomeModule) { continue; } $validatedModule = validator($moduleInput, [ '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'], ])->validate(); $module->fill([ 'name' => $validatedModule['name'], 'title' => $validatedModule['title'] ?? null, 'subtitle' => $validatedModule['subtitle'] ?? null, 'enabled' => (bool) ($validatedModule['enabled'] ?? false), 'sort_order' => (int) ($validatedModule['sort_order'] ?? $module->sort_order), 'limit' => (int) ($validatedModule['limit'] ?? $module->limit), 'more_link_type' => $validatedModule['more_link_type'] ?? null, 'more_link_target' => $validatedModule['more_link_target'] ?? null, 'extra' => [ 'side_title' => data_get($validatedModule, 'extra.side_title'), 'side_subtitle' => data_get($validatedModule, 'extra.side_subtitle'), ], ]); $this->validateModuleLink($module->module_key, $module->more_link_type, $module->more_link_target); $module->save(); $itemsInput = $moduleInput['items'] ?? []; if (!is_array($itemsInput)) { continue; } foreach ($itemsInput as $itemId => $itemInput) { if (!is_numeric($itemId) || !is_array($itemInput)) { continue; } $item = HomeModuleItem::query() ->where('home_module_id', $module->id) ->find((int) $itemId); if (! $item instanceof HomeModuleItem) { continue; } $validatedItem = validator($itemInput, [ 'title' => ['nullable', 'string', 'max:160'], 'subtitle' => ['nullable', 'string', 'max:255'], 'image_path' => ['nullable', 'string', 'max:255'], 'link_type' => ['nullable', Rule::in(['route', 'url'])], 'link_target' => ['nullable', 'string', 'max:255'], 'sort_order' => ['nullable', 'integer', 'min:0', 'max:9999'], 'enabled' => ['nullable', 'boolean'], ])->validate(); $linkType = $validatedItem['link_type'] ?? 'route'; $linkTarget = $validatedItem['link_target'] ?? null; $this->validateItemRules($module->module_key, $validatedItem); $this->validateItemLink($linkType, $linkTarget); $item->fill([ 'title' => $validatedItem['title'] ?? null, 'subtitle' => $validatedItem['subtitle'] ?? null, 'image_path' => $validatedItem['image_path'] ?? null, 'link_type' => $linkType, 'link_target' => $linkTarget, 'sort_order' => (int) ($validatedItem['sort_order'] ?? $item->sort_order), 'enabled' => (bool) ($validatedItem['enabled'] ?? false), ])->save(); } } $this->clearHomeCaches(); return redirect() ->route('admin.settings.index') ->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($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.index') ->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.index') ->with('status', '模块条目已删除。'); } /** * @return array */ private function routeOptions(): array { return [ ['name' => '工具集首页', 'value' => 'tools.index'], ['name' => '工具列表页', 'value' => 'tools.list'], ['name' => '模型推荐', 'value' => 'models.index'], ['name' => '文章资讯', 'value' => 'news.index'], ['name' => '教程学习', 'value' => 'guides.index'], ]; } 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; } if ($type === null && $target === null) { return; } if ($type === 'route') { if (! in_array($target, array_column($this->routeOptions(), 'value'), true)) { abort(422, '模块“更多链接”路由不在允许范围内。'); } return; } if ($type === 'url' && $target !== null && ! $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 $type, ?string $target): void { if ($type === 'route') { if (! in_array((string) $target, array_column($this->routeOptions(), 'value'), true)) { abort(422, '条目路由不在允许范围内。'); } return; } if ($target === null || ! $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(); } }