373 lines
13 KiB
PHP
373 lines
13 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Http\Controllers\Admin;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\HomeModule;
|
|
use App\Models\HomeModuleItem;
|
|
use Illuminate\Contracts\View\View;
|
|
use Illuminate\Http\RedirectResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Facades\Route as RouteFacade;
|
|
use Illuminate\Validation\Rule;
|
|
|
|
class SiteSettingController extends Controller
|
|
{
|
|
public function index(): View
|
|
{
|
|
$this->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<int, array{name:string,value:string}>
|
|
*/
|
|
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();
|
|
}
|
|
}
|