diff --git a/app/Http/Controllers/Admin/SiteSettingController.php b/app/Http/Controllers/Admin/SiteSettingController.php index e7a2f6f..1025474 100644 --- a/app/Http/Controllers/Admin/SiteSettingController.php +++ b/app/Http/Controllers/Admin/SiteSettingController.php @@ -11,135 +11,79 @@ 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() - ->with(['items' => fn ($query) => $query->orderBy('sort_order')->orderBy('id')]) + ->withCount('items') ->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, + ]); + } + + 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): RedirectResponse + public function update(Request $request, HomeModule $module): RedirectResponse { - $moduleData = $request->input('modules', []); - if (!is_array($moduleData) || $moduleData === []) { - return redirect() - ->route('admin.settings.index') - ->with('status', '未提交有效的模块配置。'); - } + $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'], + ]); - foreach ($moduleData as $moduleId => $moduleInput) { - if (!is_numeric($moduleId) || !is_array($moduleInput)) { - continue; - } + $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'), + ], + ]); - $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->validateModuleLink($module->module_key, $module->more_link_type, $module->more_link_target); + $module->save(); $this->clearHomeCaches(); return redirect() - ->route('admin.settings.index') - ->with('status', '首页模块配置已更新。'); + ->route('admin.settings.show', $module) + ->with('status', '模块配置已更新。'); } public function storeItem(Request $request, HomeModule $module): RedirectResponse @@ -155,7 +99,7 @@ class SiteSettingController extends Controller ]); $this->validateItemRules($module->module_key, $validated); - $this->validateItemLink($validated['link_type'], $validated['link_target'] ?? null); + $this->validateItemLink($module->module_key, $validated['link_type'], $validated['link_target'] ?? null); $module->items()->create([ 'title' => $validated['title'] ?? null, @@ -170,10 +114,60 @@ class SiteSettingController extends Controller $this->clearHomeCaches(); return redirect() - ->route('admin.settings.index') + ->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) { @@ -184,7 +178,7 @@ class SiteSettingController extends Controller $this->clearHomeCaches(); return redirect() - ->route('admin.settings.index') + ->route('admin.settings.show', $module) ->with('status', '模块条目已删除。'); } @@ -193,13 +187,72 @@ class SiteSettingController extends Controller */ 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'], + $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 @@ -231,11 +284,17 @@ class SiteSettingController extends Controller return; } - if ($type === null && $target === null) { + $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, '模块“更多链接”路由不在允许范围内。'); } @@ -243,7 +302,7 @@ class SiteSettingController extends Controller return; } - if ($type === 'url' && $target !== null && ! $this->isValidUrlOrPath($target)) { + if ($type === 'url' && $target !== '' && ! $this->isValidUrlOrPath($target)) { abort(422, '模块“更多链接”URL格式不正确。'); } } @@ -263,17 +322,36 @@ class SiteSettingController extends Controller } } - private function validateItemLink(string $type, ?string $target): void + 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 (! in_array((string) $target, array_column($this->routeOptions(), 'value'), true)) { - abort(422, '条目路由不在允许范围内。'); + if ($target === '') { + if ($isStrictModule) { + abort(422, '频道卡片/横幅推荐条目必须填写链接目标。'); + } + + return; + } + + if (! in_array($target, array_column($this->routeOptions(), 'value'), true)) { + abort(422, '请选择有效的前台路由,或将链接类型改为 URL。'); } return; } - if ($target === null || ! $this->isValidUrlOrPath($target)) { + if ($target === '') { + if ($isStrictModule) { + abort(422, '频道卡片/横幅推荐条目必须填写链接目标。'); + } + + return; + } + + if (! $this->isValidUrlOrPath($target)) { abort(422, '条目URL格式不正确。'); } } @@ -292,4 +370,3 @@ class SiteSettingController extends Controller Cache::flush(); } } - diff --git a/app/Http/Controllers/Admin/UploadController.php b/app/Http/Controllers/Admin/UploadController.php index 6beeaed..81ddfa4 100644 --- a/app/Http/Controllers/Admin/UploadController.php +++ b/app/Http/Controllers/Admin/UploadController.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace App\Http\Controllers\Admin; use App\Http\Controllers\Controller; +use App\Support\MarkdownRenderer; use GdImage; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -26,6 +27,18 @@ class UploadController extends Controller private const THUMB_WEBP_QUALITY = 80; + public function markdownPreview(Request $request, MarkdownRenderer $markdownRenderer): JsonResponse + { + $validated = $request->validate([ + 'markdown' => ['nullable', 'string'], + ]); + + return response()->json([ + 'success' => true, + 'html' => $markdownRenderer->render((string) ($validated['markdown'] ?? '')), + ]); + } + public function markdownImage(Request $request): JsonResponse { $validated = $request->validate([ @@ -177,4 +190,3 @@ class UploadController extends Controller return $binary; } } - diff --git a/resources/views/admin/articles/form.blade.php b/resources/views/admin/articles/form.blade.php index 9b7e866..a78b450 100644 --- a/resources/views/admin/articles/form.blade.php +++ b/resources/views/admin/articles/form.blade.php @@ -2,89 +2,176 @@ @section('title', $item->exists ? '编辑资讯' : '新建资讯') +@section('head') + @include('admin.partials.modern-form-head') +@endsection + @section('content') -
* 为必填项
+| 标题 | @@ -29,16 +36,20 @@ @forelse($items as $item)||||||
|---|---|---|---|---|---|---|
|
- {{ $item->title }}
- {{ $item->excerpt }}
+ {{ $item->title }}
+ {{ $item->excerpt }}
|
- {{ $item->status?->value ?? '-' }} | +@include('admin.partials.status-badge', ['status' => $item->status]) | {{ $item->source_level?->value ?? '-' }} | {{ $item->published_at?->format('Y-m-d H:i') ?: '-' }} | -编辑 | ++ 编辑 + |
| 暂无资讯数据 | ||||||
| 暂无资讯数据,先新增一条内容吧。 | +||||||
* 为必填项
+| 名称 | @@ -36,8 +43,8 @@ @forelse($items as $item)|||||
|---|---|---|---|---|---|
|
- {{ $item->name }}
- {{ $item->description ?: '-' }}
+ {{ $item->name }}
+ {{ $item->description ?: '暂无描述' }}
|
{{ $item->type }} | {{ $item->slug }} |
@@ -49,10 +56,14 @@
@endif
{{ $item->updated_at?->format('Y-m-d H:i') }} | -编辑 | ++ 编辑 + |
| 暂无分类数据 | |||||
| 暂无分类数据,先新增一条内容吧。 | +|||||
| ID | 类型 | -标题 | +标题与描述 | 联系 | 状态 | 提交时间 | @@ -53,20 +60,28 @@|||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| {{ $item->id }} | -{{ $item->feedback_type }} | +#{{ $item->id }} | +{{ $item->feedback_type }} |
- {{ $item->title }}
- {{ $item->description }}
+ {{ $item->title }}
+ {{ $item->description }}
|
{{ $item->contact ?: '-' }} | -{{ $item->status }} | ++ @if($item->status === 'done') + done + @elseif($item->status === 'reviewing') + reviewing + @else + new + @endif + | {{ $item->created_at?->format('Y-m-d H:i') }} | |||||
| 暂无反馈数据 | +暂无反馈数据。 | ||||||||||||
* 为必填项
+| 标题 | @@ -29,16 +36,20 @@ @forelse($items as $item)|||||||
|---|---|---|---|---|---|---|---|
|
- {{ $item->title }}
- {{ $item->excerpt }}
+ {{ $item->title }}
+ {{ $item->excerpt }}
|
- {{ $item->difficulty }} | -{{ $item->status?->value ?? '-' }} | +{{ $item->difficulty ?: '-' }} | +@include('admin.partials.status-badge', ['status' => $item->status]) | {{ $item->updated_at?->format('Y-m-d H:i') }} | -编辑 | ++ 编辑 + |
| 暂无教程数据 | |||||||
| 暂无教程数据,先新增一条内容吧。 | +|||||||
* 为必填项
+