配置功能完善
This commit is contained in:
@@ -5,72 +5,291 @@ declare(strict_types=1);
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\SiteSetting;
|
||||
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\Validation\Rule;
|
||||
|
||||
class SiteSettingController extends Controller
|
||||
{
|
||||
public function index(): View
|
||||
{
|
||||
$defaults = $this->defaultModules();
|
||||
$stored = SiteSetting::query()->where('setting_key', 'home_modules')->value('setting_value');
|
||||
$modules = HomeModule::query()
|
||||
->with(['items' => fn ($query) => $query->orderBy('sort_order')->orderBy('id')])
|
||||
->orderBy('sort_order')
|
||||
->orderBy('id')
|
||||
->get();
|
||||
|
||||
$modules = $defaults;
|
||||
if (is_array($stored)) {
|
||||
$storedByKey = collect($stored)->keyBy('key');
|
||||
$modules = array_map(function (array $module) use ($storedByKey): array {
|
||||
$saved = $storedByKey->get($module['key']);
|
||||
if (is_array($saved)) {
|
||||
$module['enabled'] = (bool) ($saved['enabled'] ?? true);
|
||||
$module['limit'] = (int) ($saved['limit'] ?? $module['limit']);
|
||||
}
|
||||
if ($modules->isEmpty()) {
|
||||
$this->ensureDefaultModules();
|
||||
|
||||
return $module;
|
||||
}, $defaults);
|
||||
$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
|
||||
{
|
||||
$payload = [];
|
||||
foreach ($this->defaultModules() as $module) {
|
||||
$key = $module['key'];
|
||||
$payload[] = [
|
||||
'key' => $key,
|
||||
'label' => $module['label'],
|
||||
'enabled' => $request->boolean("modules.{$key}.enabled", true),
|
||||
'limit' => max(1, min(30, (int) $request->input("modules.{$key}.limit", $module['limit']))),
|
||||
];
|
||||
$moduleData = $request->input('modules', []);
|
||||
if (!is_array($moduleData) || $moduleData === []) {
|
||||
return redirect()
|
||||
->route('admin.settings.index')
|
||||
->with('status', '未提交有效的模块配置。');
|
||||
}
|
||||
|
||||
SiteSetting::query()->updateOrCreate(
|
||||
['setting_key' => 'home_modules'],
|
||||
['setting_value' => $payload],
|
||||
);
|
||||
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', '首页模块配置已更新');
|
||||
->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<int, array{key:string,label:string,enabled:bool,limit:int}>
|
||||
* @return array<int, array{name:string,value:string}>
|
||||
*/
|
||||
private function defaultModules(): array
|
||||
private function routeOptions(): array
|
||||
{
|
||||
return [
|
||||
['key' => 'hot_tools', 'label' => '热门工具', 'enabled' => true, 'limit' => 18],
|
||||
['key' => 'latest_tools', 'label' => '最新收录', 'enabled' => true, 'limit' => 18],
|
||||
['key' => 'category_sections', 'label' => '分类分块', 'enabled' => true, 'limit' => 18],
|
||||
['key' => 'channel_cards', 'label' => '频道卡片区', 'enabled' => true, 'limit' => 1],
|
||||
['key' => 'promo_banners', 'label' => '横幅推荐区', 'enabled' => true, 'limit' => 1],
|
||||
['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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -177,3 +177,4 @@ class UploadController extends Controller
|
||||
return $binary;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ use App\Http\Controllers\Controller;
|
||||
use App\Models\AiModel;
|
||||
use App\Models\Article;
|
||||
use App\Models\Category;
|
||||
use App\Models\SiteSetting;
|
||||
use App\Models\HomeModule;
|
||||
use App\Models\Tool;
|
||||
use App\Support\MarkdownRenderer;
|
||||
use Illuminate\Contracts\View\View;
|
||||
@@ -28,7 +28,7 @@ class ToolController extends Controller
|
||||
$querySignature = sha1((string) $request->getQueryString());
|
||||
$activeTab = $this->resolveTab($request);
|
||||
|
||||
$payload = Cache::remember("tools_portal_v2_{$querySignature}", now()->addMinutes(10), function () use ($request, $activeTab): array {
|
||||
$payload = Cache::remember("tools_portal_v3_home_{$querySignature}", now()->addMinutes(10), function () use ($request, $activeTab): array {
|
||||
$builder = Tool::query()
|
||||
->published()
|
||||
->with('category');
|
||||
@@ -49,8 +49,15 @@ class ToolController extends Controller
|
||||
$portalTools = $builder->limit(360)->get();
|
||||
$toolsByCategory = $portalTools->groupBy(fn (Tool $tool): string => $tool->category?->slug ?? 'uncategorized');
|
||||
|
||||
$categorySections = $categories->map(function (Category $category) use ($toolsByCategory): array {
|
||||
$sectionTools = $toolsByCategory->get($category->slug, collect())->take(18);
|
||||
$modules = $this->loadHomeModules();
|
||||
$moduleMap = collect($modules)->keyBy('module_key');
|
||||
|
||||
$hotToolsLimit = (int) data_get($moduleMap, 'hot_tools.limit', 18);
|
||||
$latestToolsLimit = (int) data_get($moduleMap, 'latest_tools.limit', 18);
|
||||
$sectionLimit = (int) data_get($moduleMap, 'category_sections.limit', 18);
|
||||
|
||||
$categorySections = $categories->map(function (Category $category) use ($toolsByCategory, $sectionLimit): array {
|
||||
$sectionTools = $toolsByCategory->get($category->slug, collect())->take($sectionLimit);
|
||||
|
||||
return [
|
||||
'slug' => $category->slug,
|
||||
@@ -60,44 +67,6 @@ class ToolController extends Controller
|
||||
];
|
||||
})->values();
|
||||
|
||||
$moduleConfig = SiteSetting::query()
|
||||
->where('setting_key', 'home_modules')
|
||||
->value('setting_value');
|
||||
|
||||
$modules = collect([
|
||||
'hot_tools' => ['enabled' => true, 'limit' => 18],
|
||||
'latest_tools' => ['enabled' => true, 'limit' => 18],
|
||||
'category_sections' => ['enabled' => true, 'limit' => 18],
|
||||
'channel_cards' => ['enabled' => true, 'limit' => 1],
|
||||
'promo_banners' => ['enabled' => true, 'limit' => 1],
|
||||
]);
|
||||
|
||||
if (is_array($moduleConfig)) {
|
||||
foreach ($moduleConfig as $module) {
|
||||
if (!is_array($module) || empty($module['key'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$key = (string) $module['key'];
|
||||
if ($modules->has($key)) {
|
||||
$modules[$key] = [
|
||||
'enabled' => (bool) ($module['enabled'] ?? true),
|
||||
'limit' => max(1, min(30, (int) ($module['limit'] ?? 18))),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$hotToolsLimit = (int) ($modules['hot_tools']['limit'] ?? 18);
|
||||
$latestToolsLimit = (int) ($modules['latest_tools']['limit'] ?? 18);
|
||||
$sectionLimit = (int) ($modules['category_sections']['limit'] ?? 18);
|
||||
|
||||
$categorySections = $categorySections->map(function (array $section) use ($sectionLimit): array {
|
||||
$section['tools'] = $section['tools']->take($sectionLimit);
|
||||
|
||||
return $section;
|
||||
});
|
||||
|
||||
return [
|
||||
'categories' => $categories,
|
||||
'categorySections' => $categorySections,
|
||||
@@ -107,7 +76,7 @@ class ToolController extends Controller
|
||||
'activeTab' => $activeTab,
|
||||
'tabOptions' => $this->tabOptions(),
|
||||
'toolStats' => $this->buildToolStats(),
|
||||
'modules' => $modules,
|
||||
'modules' => $moduleMap,
|
||||
];
|
||||
});
|
||||
|
||||
@@ -299,7 +268,7 @@ class ToolController extends Controller
|
||||
'搜索' => '知识检索',
|
||||
'自动化' => '流程自动化',
|
||||
'agent' => 'Agent 工作流',
|
||||
'api' => '开放 API',
|
||||
'api' => '开放API',
|
||||
];
|
||||
|
||||
$tags = [];
|
||||
@@ -331,4 +300,244 @@ class ToolController extends Controller
|
||||
|
||||
return array_values(array_unique(array_slice($tags, 0, 8)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private function loadHomeModules(): array
|
||||
{
|
||||
$defaults = $this->defaultHomeModules();
|
||||
|
||||
$records = HomeModule::query()
|
||||
->with(['items' => fn ($query) => $query->where('enabled', true)->orderBy('sort_order')->orderBy('id')])
|
||||
->orderBy('sort_order')
|
||||
->orderBy('id')
|
||||
->get();
|
||||
|
||||
if ($records->isEmpty()) {
|
||||
return $defaults;
|
||||
}
|
||||
|
||||
$filled = [];
|
||||
foreach ($records as $record) {
|
||||
$default = collect($defaults)->firstWhere('module_key', $record->module_key);
|
||||
if (!is_array($default)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$items = $record->items->map(function ($item): array {
|
||||
return [
|
||||
'id' => $item->id,
|
||||
'title' => (string) ($item->title ?? ''),
|
||||
'subtitle' => (string) ($item->subtitle ?? ''),
|
||||
'image_path' => (string) ($item->image_path ?? ''),
|
||||
'link_type' => (string) ($item->link_type ?? 'route'),
|
||||
'link_target' => (string) ($item->link_target ?? ''),
|
||||
'url' => $this->resolveLink((string) ($item->link_type ?? 'route'), (string) ($item->link_target ?? '')),
|
||||
];
|
||||
})->values()->all();
|
||||
|
||||
$filled[] = [
|
||||
'module_key' => $record->module_key,
|
||||
'name' => $record->name,
|
||||
'title' => $record->title ?: $default['title'],
|
||||
'subtitle' => $record->subtitle ?: $default['subtitle'],
|
||||
'enabled' => (bool) $record->enabled,
|
||||
'sort_order' => (int) $record->sort_order,
|
||||
'limit' => max(1, min(30, (int) $record->limit)),
|
||||
'more_link_type' => $record->more_link_type,
|
||||
'more_link_target' => $record->more_link_target,
|
||||
'more_url' => $this->resolveLink((string) $record->more_link_type, (string) $record->more_link_target),
|
||||
'extra' => is_array($record->extra) ? $record->extra : [],
|
||||
'items' => $items,
|
||||
];
|
||||
}
|
||||
|
||||
if ($filled === []) {
|
||||
return $defaults;
|
||||
}
|
||||
|
||||
return $this->ensureModuleDefaults($filled, $defaults);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private function defaultHomeModules(): array
|
||||
{
|
||||
return [
|
||||
[
|
||||
'module_key' => 'channel_cards',
|
||||
'name' => '频道卡片',
|
||||
'title' => '频道入口',
|
||||
'subtitle' => '模型、资讯、教程等入口合集',
|
||||
'enabled' => true,
|
||||
'sort_order' => 20,
|
||||
'limit' => 5,
|
||||
'more_link_type' => null,
|
||||
'more_link_target' => null,
|
||||
'more_url' => null,
|
||||
'extra' => [
|
||||
'side_title' => '频道导航',
|
||||
'side_subtitle' => '快速直达',
|
||||
],
|
||||
'items' => [
|
||||
[
|
||||
'title' => '每日快讯',
|
||||
'subtitle' => 'AI 资讯',
|
||||
'image_path' => '',
|
||||
'link_type' => 'route',
|
||||
'link_target' => 'news.index',
|
||||
'url' => route('news.index'),
|
||||
],
|
||||
[
|
||||
'title' => '最新模型',
|
||||
'subtitle' => '模型推荐',
|
||||
'image_path' => '',
|
||||
'link_type' => 'route',
|
||||
'link_target' => 'models.index',
|
||||
'url' => route('models.index'),
|
||||
],
|
||||
[
|
||||
'title' => '热门教程',
|
||||
'subtitle' => '教程学习',
|
||||
'image_path' => '',
|
||||
'link_type' => 'route',
|
||||
'link_target' => 'guides.index',
|
||||
'url' => route('guides.index'),
|
||||
],
|
||||
[
|
||||
'title' => '工具列表',
|
||||
'subtitle' => '查看全部',
|
||||
'image_path' => '',
|
||||
'link_type' => 'route',
|
||||
'link_target' => 'tools.list',
|
||||
'url' => route('tools.list'),
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'module_key' => 'promo_banners',
|
||||
'name' => '横幅推荐',
|
||||
'title' => '精选推荐',
|
||||
'subtitle' => '运营位横幅可配置',
|
||||
'enabled' => true,
|
||||
'sort_order' => 30,
|
||||
'limit' => 2,
|
||||
'more_link_type' => null,
|
||||
'more_link_target' => null,
|
||||
'more_url' => null,
|
||||
'extra' => [],
|
||||
'items' => [
|
||||
[
|
||||
'title' => '超全图像视频模板一键复制',
|
||||
'subtitle' => '全球顶尖模型',
|
||||
'image_path' => '',
|
||||
'link_type' => 'route',
|
||||
'link_target' => 'tools.list',
|
||||
'url' => route('tools.list'),
|
||||
],
|
||||
[
|
||||
'title' => '一站式 AI 创作平台',
|
||||
'subtitle' => '免费试用',
|
||||
'image_path' => '',
|
||||
'link_type' => 'route',
|
||||
'link_target' => 'tools.list',
|
||||
'url' => route('tools.list'),
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'module_key' => 'hot_tools',
|
||||
'name' => '热门工具',
|
||||
'title' => '热门工具',
|
||||
'subtitle' => '根据推荐排序筛选',
|
||||
'enabled' => true,
|
||||
'sort_order' => 40,
|
||||
'limit' => 18,
|
||||
'more_link_type' => 'route',
|
||||
'more_link_target' => 'tools.list',
|
||||
'more_url' => route('tools.list', ['tab' => 'recommended']),
|
||||
'extra' => ['side_title' => '热门工具'],
|
||||
'items' => [],
|
||||
],
|
||||
[
|
||||
'module_key' => 'latest_tools',
|
||||
'name' => '最新收录',
|
||||
'title' => '最新收录',
|
||||
'subtitle' => '按发布时间倒序',
|
||||
'enabled' => true,
|
||||
'sort_order' => 50,
|
||||
'limit' => 18,
|
||||
'more_link_type' => 'route',
|
||||
'more_link_target' => 'tools.list',
|
||||
'more_url' => route('tools.list', ['tab' => 'latest']),
|
||||
'extra' => ['side_title' => '最新收录'],
|
||||
'items' => [],
|
||||
],
|
||||
[
|
||||
'module_key' => 'category_sections',
|
||||
'name' => '分类分块',
|
||||
'title' => '分类分块',
|
||||
'subtitle' => '按分类浏览工具',
|
||||
'enabled' => true,
|
||||
'sort_order' => 60,
|
||||
'limit' => 18,
|
||||
'more_link_type' => null,
|
||||
'more_link_target' => null,
|
||||
'more_url' => null,
|
||||
'extra' => ['side_title' => '分类导航'],
|
||||
'items' => [],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $loaded
|
||||
* @param array<int, array<string, mixed>> $defaults
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private function ensureModuleDefaults(array $loaded, array $defaults): array
|
||||
{
|
||||
$loadedByKey = collect($loaded)->keyBy('module_key');
|
||||
$merged = [];
|
||||
|
||||
foreach ($defaults as $default) {
|
||||
$existing = $loadedByKey->get($default['module_key']);
|
||||
if (!is_array($existing)) {
|
||||
$merged[] = $default;
|
||||
continue;
|
||||
}
|
||||
|
||||
$merged[] = array_merge($default, $existing, [
|
||||
'items' => $existing['items'] ?? $default['items'],
|
||||
'extra' => array_merge($default['extra'] ?? [], $existing['extra'] ?? []),
|
||||
]);
|
||||
}
|
||||
|
||||
usort($merged, fn (array $a, array $b): int => (int) $a['sort_order'] <=> (int) $b['sort_order']);
|
||||
|
||||
return $merged;
|
||||
}
|
||||
|
||||
private function resolveLink(string $type, string $target): ?string
|
||||
{
|
||||
if ($type === 'route') {
|
||||
if ($target === '' || !\Illuminate\Support\Facades\Route::has($target)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return route($target);
|
||||
}
|
||||
|
||||
if ($target === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (str_starts_with($target, '/')) {
|
||||
return $target;
|
||||
}
|
||||
|
||||
return filter_var($target, FILTER_VALIDATE_URL) ? $target : null;
|
||||
}
|
||||
}
|
||||
|
||||
45
app/Models/HomeModule.php
Normal file
45
app/Models/HomeModule.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class HomeModule extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'module_key',
|
||||
'name',
|
||||
'title',
|
||||
'subtitle',
|
||||
'enabled',
|
||||
'sort_order',
|
||||
'limit',
|
||||
'more_link_type',
|
||||
'more_link_target',
|
||||
'extra',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'enabled' => 'boolean',
|
||||
'sort_order' => 'integer',
|
||||
'limit' => 'integer',
|
||||
'extra' => 'array',
|
||||
];
|
||||
}
|
||||
|
||||
public function items(): HasMany
|
||||
{
|
||||
return $this->hasMany(HomeModuleItem::class)
|
||||
->orderBy('sort_order')
|
||||
->orderBy('id');
|
||||
}
|
||||
}
|
||||
|
||||
42
app/Models/HomeModuleItem.php
Normal file
42
app/Models/HomeModuleItem.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class HomeModuleItem extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'home_module_id',
|
||||
'item_key',
|
||||
'title',
|
||||
'subtitle',
|
||||
'image_path',
|
||||
'link_type',
|
||||
'link_target',
|
||||
'sort_order',
|
||||
'enabled',
|
||||
'extra',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'enabled' => 'boolean',
|
||||
'sort_order' => 'integer',
|
||||
'extra' => 'array',
|
||||
];
|
||||
}
|
||||
|
||||
public function module(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(HomeModule::class, 'home_module_id');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user