From 67cd9501de98997b25aba51d3b8f8c46b2d9f324 Mon Sep 17 00:00:00 2001 From: "jiangdong.cheng" Date: Thu, 12 Feb 2026 13:06:12 +0800 Subject: [PATCH] =?UTF-8?q?=E9=A1=B5=E9=9D=A2=E4=BC=98=E5=8C=96=EF=BC=8C?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/Admin/FeedbackController.php | 47 ++++ .../Admin/SiteSettingController.php | 76 ++++++ .../Controllers/Admin/UploadController.php | 4 +- .../Controllers/Site/FeedbackController.php | 41 ++++ app/Http/Controllers/Site/ToolController.php | 45 +++- app/Models/FeedbackEntry.php | 23 ++ app/Models/SiteSetting.php | 26 ++ app/Support/MarkdownRenderer.php | 10 + ...2_12_120000_create_site_settings_table.php | 25 ++ ...2_120100_create_feedback_entries_table.php | 31 +++ .../views/admin/feedback/index.blade.php | 91 +++++++ .../views/admin/settings/index.blade.php | 49 ++++ .../portal/side-list-section.blade.php | 29 +++ .../components/portal/stat-grid.blade.php | 15 ++ .../components/portal/tool-grid.blade.php | 18 ++ .../views/components/portal/top-nav.blade.php | 27 +++ resources/views/layouts/admin.blade.php | 15 +- resources/views/layouts/site.blade.php | 130 +++++++++- resources/views/public/guides/index.blade.php | 71 +++--- resources/views/public/models/index.blade.php | 71 +++--- resources/views/public/news/index.blade.php | 71 +++--- resources/views/public/tools/index.blade.php | 228 +++++++++++++----- resources/views/public/tools/list.blade.php | 64 ++--- routes/web.php | 10 + 24 files changed, 975 insertions(+), 242 deletions(-) create mode 100644 app/Http/Controllers/Admin/FeedbackController.php create mode 100644 app/Http/Controllers/Admin/SiteSettingController.php create mode 100644 app/Http/Controllers/Site/FeedbackController.php create mode 100644 app/Models/FeedbackEntry.php create mode 100644 app/Models/SiteSetting.php create mode 100644 database/migrations/2026_02_12_120000_create_site_settings_table.php create mode 100644 database/migrations/2026_02_12_120100_create_feedback_entries_table.php create mode 100644 resources/views/admin/feedback/index.blade.php create mode 100644 resources/views/admin/settings/index.blade.php create mode 100644 resources/views/components/portal/side-list-section.blade.php create mode 100644 resources/views/components/portal/stat-grid.blade.php create mode 100644 resources/views/components/portal/tool-grid.blade.php create mode 100644 resources/views/components/portal/top-nav.blade.php diff --git a/app/Http/Controllers/Admin/FeedbackController.php b/app/Http/Controllers/Admin/FeedbackController.php new file mode 100644 index 0000000..bd3e5c3 --- /dev/null +++ b/app/Http/Controllers/Admin/FeedbackController.php @@ -0,0 +1,47 @@ +latest('id'); + + if ($request->filled('type')) { + $builder->where('feedback_type', (string) $request->string('type')); + } + + if ($request->filled('status')) { + $builder->where('status', (string) $request->string('status')); + } + + return view('admin.feedback.index', [ + 'items' => $builder->paginate(30)->withQueryString(), + 'filters' => $request->only(['type', 'status']), + ]); + } + + public function updateStatus(FeedbackEntry $feedback, Request $request): RedirectResponse + { + $status = (string) $request->input('status', 'new'); + if (!in_array($status, ['new', 'reviewing', 'done'], true)) { + $status = 'new'; + } + + $feedback->update(['status' => $status]); + + return redirect() + ->route('admin.feedback.index') + ->with('status', '反馈状态已更新'); + } +} + diff --git a/app/Http/Controllers/Admin/SiteSettingController.php b/app/Http/Controllers/Admin/SiteSettingController.php new file mode 100644 index 0000000..e890b6f --- /dev/null +++ b/app/Http/Controllers/Admin/SiteSettingController.php @@ -0,0 +1,76 @@ +defaultModules(); + $stored = SiteSetting::query()->where('setting_key', 'home_modules')->value('setting_value'); + + $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']); + } + + return $module; + }, $defaults); + } + + return view('admin.settings.index', [ + 'modules' => $modules, + ]); + } + + 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']))), + ]; + } + + SiteSetting::query()->updateOrCreate( + ['setting_key' => 'home_modules'], + ['setting_value' => $payload], + ); + + return redirect() + ->route('admin.settings.index') + ->with('status', '首页模块配置已更新'); + } + + /** + * @return array + */ + private function defaultModules(): 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], + ]; + } +} + diff --git a/app/Http/Controllers/Admin/UploadController.php b/app/Http/Controllers/Admin/UploadController.php index 8c12bfb..30a5ed1 100644 --- a/app/Http/Controllers/Admin/UploadController.php +++ b/app/Http/Controllers/Admin/UploadController.php @@ -71,8 +71,8 @@ class UploadController extends Controller Storage::disk('public')->put($mainPath, $mainBinary); Storage::disk('public')->put($thumbPath, $thumbBinary); - $mainUrl = Storage::disk('public')->url($mainPath); - $thumbUrl = Storage::disk('public')->url($thumbPath); + $mainUrl = '/storage/'.ltrim($mainPath, '/'); + $thumbUrl = '/storage/'.ltrim($thumbPath, '/'); return response()->json([ 'success' => true, diff --git a/app/Http/Controllers/Site/FeedbackController.php b/app/Http/Controllers/Site/FeedbackController.php new file mode 100644 index 0000000..848a428 --- /dev/null +++ b/app/Http/Controllers/Site/FeedbackController.php @@ -0,0 +1,41 @@ +validate([ + 'feedback_type' => ['required', 'in:tool,model,news,guide,other'], + 'title' => ['required', 'string', 'max:180'], + 'description' => ['required', 'string', 'max:4000'], + 'contact' => ['nullable', 'string', 'max:160'], + ], [ + 'feedback_type.required' => '请选择反馈类型', + 'title.required' => '请填写标题', + 'description.required' => '请填写详细说明', + ]); + + FeedbackEntry::query()->create([ + 'feedback_type' => $validated['feedback_type'], + 'title' => $validated['title'], + 'description' => $validated['description'], + 'contact' => $validated['contact'] ?? null, + 'status' => 'new', + 'ip_address' => $request->ip(), + ]); + + return redirect() + ->back() + ->with('status', '反馈提交成功,感谢你的建议'); + } +} + diff --git a/app/Http/Controllers/Site/ToolController.php b/app/Http/Controllers/Site/ToolController.php index 08de3ef..901fd4a 100644 --- a/app/Http/Controllers/Site/ToolController.php +++ b/app/Http/Controllers/Site/ToolController.php @@ -8,6 +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\Tool; use App\Support\MarkdownRenderer; use Illuminate\Contracts\View\View; @@ -59,15 +60,54 @@ 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, - 'hotTools' => $portalTools->take(18), - 'latestTools' => Tool::query()->published()->with('category')->latest('published_at')->limit(18)->get(), + 'hotTools' => $portalTools->take($hotToolsLimit), + 'latestTools' => Tool::query()->published()->with('category')->latest('published_at')->limit($latestToolsLimit)->get(), 'filters' => $request->only(['q', 'pricing', 'api', 'tab']), 'activeTab' => $activeTab, 'tabOptions' => $this->tabOptions(), 'toolStats' => $this->buildToolStats(), + 'modules' => $modules, ]; }); @@ -292,4 +332,3 @@ class ToolController extends Controller return array_values(array_unique(array_slice($tags, 0, 8))); } } - diff --git a/app/Models/FeedbackEntry.php b/app/Models/FeedbackEntry.php new file mode 100644 index 0000000..814bd05 --- /dev/null +++ b/app/Models/FeedbackEntry.php @@ -0,0 +1,23 @@ + 'array', + ]; + } +} + diff --git a/app/Support/MarkdownRenderer.php b/app/Support/MarkdownRenderer.php index a901144..ebaffe4 100644 --- a/app/Support/MarkdownRenderer.php +++ b/app/Support/MarkdownRenderer.php @@ -16,9 +16,19 @@ class MarkdownRenderer return ''; } + $markdown = $this->normalizeStorageLinks($markdown); + return (string) Str::markdown($markdown, [ 'html_input' => 'strip', 'allow_unsafe_links' => false, ]); } + + private function normalizeStorageLinks(string $markdown): string + { + $pattern = '/https?:\/\/(?:localhost|127\.0\.0\.1)(?::\d+)?\/storage\/([^\s)"\'\<\>]+)/i'; + $normalized = preg_replace($pattern, '/storage/$1', $markdown); + + return is_string($normalized) ? $normalized : $markdown; + } } diff --git a/database/migrations/2026_02_12_120000_create_site_settings_table.php b/database/migrations/2026_02_12_120000_create_site_settings_table.php new file mode 100644 index 0000000..3fdf979 --- /dev/null +++ b/database/migrations/2026_02_12_120000_create_site_settings_table.php @@ -0,0 +1,25 @@ +id(); + $table->string('setting_key', 120)->unique(); + $table->json('setting_value')->nullable(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('site_settings'); + } +}; + diff --git a/database/migrations/2026_02_12_120100_create_feedback_entries_table.php b/database/migrations/2026_02_12_120100_create_feedback_entries_table.php new file mode 100644 index 0000000..d639ade --- /dev/null +++ b/database/migrations/2026_02_12_120100_create_feedback_entries_table.php @@ -0,0 +1,31 @@ +id(); + $table->string('feedback_type', 40); + $table->string('title', 180); + $table->text('description'); + $table->string('contact', 160)->nullable(); + $table->string('status', 30)->default('new'); + $table->string('ip_address', 45)->nullable(); + $table->timestamps(); + + $table->index(['feedback_type', 'status']); + }); + } + + public function down(): void + { + Schema::dropIfExists('feedback_entries'); + } +}; + diff --git a/resources/views/admin/feedback/index.blade.php b/resources/views/admin/feedback/index.blade.php new file mode 100644 index 0000000..9181e89 --- /dev/null +++ b/resources/views/admin/feedback/index.blade.php @@ -0,0 +1,91 @@ +@extends('layouts.admin') + +@section('title', '反馈管理') + +@section('content') +
+
+ @if(session('status')) +
{{ session('status') }}
+ @endif + +
+
+ +
+
+ +
+
+ +
+
+
+
+ +
+
+

用户反馈列表

+
+
+ + + + + + + + + + + + + + @forelse($items as $item) + + + + + + + + + + @empty + + + + @endforelse + +
ID类型标题联系状态提交时间操作
{{ $item->id }}{{ $item->feedback_type }} +
{{ $item->title }}
+
{{ $item->description }}
+
{{ $item->contact ?: '-' }}{{ $item->status }}{{ $item->created_at?->format('Y-m-d H:i') }} +
+ @csrf + @method('put') + + +
+
暂无反馈数据
+
+
+ {{ $items->links() }} +
+
+@endsection + diff --git a/resources/views/admin/settings/index.blade.php b/resources/views/admin/settings/index.blade.php new file mode 100644 index 0000000..d5d0415 --- /dev/null +++ b/resources/views/admin/settings/index.blade.php @@ -0,0 +1,49 @@ +@extends('layouts.admin') + +@section('title', '首页模块配置') + +@section('content') +
+
+

首页模块配置(AI工具集)

+
+
+ @if(session('status')) +
{{ session('status') }}
+ @endif + +
+ @csrf + @method('put') + + @foreach($modules as $module) +
+
+
+
{{ $module['label'] }}
+
key: {{ $module['key'] }}
+
+ +
+ + +
+ + +
+
+
+
+ @endforeach + +
+ +
+
+
+
+@endsection + diff --git a/resources/views/components/portal/side-list-section.blade.php b/resources/views/components/portal/side-list-section.blade.php new file mode 100644 index 0000000..69d046e --- /dev/null +++ b/resources/views/components/portal/side-list-section.blade.php @@ -0,0 +1,29 @@ +@props([ + 'title' => '', + 'moreUrl' => null, + 'moreText' => '查看全部', + 'items' => [], + 'itemUrl' => '#', + 'itemTitle' => null, + 'itemMeta' => null, + 'emptyText' => '暂无数据', +]) + +
+
+

{{ $title }}

+ @if($moreUrl) + {{ $moreText }} + @endif +
+ + @forelse($items as $item) + + @empty +

{{ $emptyText }}

+ @endforelse +
+ diff --git a/resources/views/components/portal/stat-grid.blade.php b/resources/views/components/portal/stat-grid.blade.php new file mode 100644 index 0000000..4991f46 --- /dev/null +++ b/resources/views/components/portal/stat-grid.blade.php @@ -0,0 +1,15 @@ +@props([ + 'stats' => [], + 'gridClass' => 'channel-kpis', + 'itemClass' => 'channel-kpi', +]) + +
+ @foreach($stats as $stat) +
+ {{ $stat['label'] }} + {{ $stat['value'] }} +
+ @endforeach +
+ diff --git a/resources/views/components/portal/tool-grid.blade.php b/resources/views/components/portal/tool-grid.blade.php new file mode 100644 index 0000000..50c31bf --- /dev/null +++ b/resources/views/components/portal/tool-grid.blade.php @@ -0,0 +1,18 @@ +@props([ + 'tools' => [], + 'cardClass' => 'tool-card', + 'nameClass' => 'tool-card-name', + 'metaClass' => 'tool-card-meta', + 'tagClass' => 'tool-card-tag', +]) + + + diff --git a/resources/views/components/portal/top-nav.blade.php b/resources/views/components/portal/top-nav.blade.php new file mode 100644 index 0000000..e6a3332 --- /dev/null +++ b/resources/views/components/portal/top-nav.blade.php @@ -0,0 +1,27 @@ +@props([ + 'active' => 'tools', + 'statusLabel' => '数据总量', + 'statusValue' => 0, + 'wrapperClass' => 'channel-head', + 'tabsClass' => 'channel-tabs', + 'statusClass' => 'channel-status', +]) + +@php + $tabs = [ + ['key' => 'tools', 'label' => 'AI工具集', 'url' => route('tools.index')], + ['key' => 'models', 'label' => 'AI应用集', 'url' => route('models.index')], + ['key' => 'news', 'label' => '每日AI资讯', 'url' => route('news.index')], + ['key' => 'guides', 'label' => 'AI教程资源', 'url' => route('guides.index')], + ['key' => 'tool-list', 'label' => '工具列表', 'url' => route('tools.list')], + ]; +@endphp + +
+ +
{{ $statusLabel }} {{ $statusValue }}
+
diff --git a/resources/views/layouts/admin.blade.php b/resources/views/layouts/admin.blade.php index 813d5f9..3c38122 100644 --- a/resources/views/layouts/admin.blade.php +++ b/resources/views/layouts/admin.blade.php @@ -1,4 +1,4 @@ - + @@ -79,10 +79,12 @@ - - + + + + - + @@ -95,7 +97,7 @@
当前账号:{{ session('admin_username', 'admin') }}
@csrf - +
@endsection +@section('scripts') + +@endsection diff --git a/resources/views/public/tools/list.blade.php b/resources/views/public/tools/list.blade.php index 25ba114..5504086 100644 --- a/resources/views/public/tools/list.blade.php +++ b/resources/views/public/tools/list.blade.php @@ -10,12 +10,20 @@
工具列表

AI工具独立列表页

-

从工具集首页点击“更多”进入,支持完整筛选与分页浏览。

+

从工具集首页点击“查看更多”进入,支持完整筛选与分页浏览。

+ +
@endsection diff --git a/routes/web.php b/routes/web.php index d8a60d4..b04174c 100644 --- a/routes/web.php +++ b/routes/web.php @@ -6,11 +6,14 @@ use App\Http\Controllers\Admin\AiModelController as AdminAiModelController; use App\Http\Controllers\Admin\ArticleController as AdminArticleController; use App\Http\Controllers\Admin\AuthController as AdminAuthController; use App\Http\Controllers\Admin\DashboardController; +use App\Http\Controllers\Admin\FeedbackController as AdminFeedbackController; use App\Http\Controllers\Admin\GuideController as AdminGuideController; use App\Http\Controllers\Admin\CategoryController as AdminCategoryController; +use App\Http\Controllers\Admin\SiteSettingController as AdminSiteSettingController; use App\Http\Controllers\Admin\UploadController as AdminUploadController; use App\Http\Controllers\Admin\SourceController as AdminSourceController; use App\Http\Controllers\Admin\ToolController as AdminToolController; +use App\Http\Controllers\Site\FeedbackController; use App\Http\Controllers\Site\GuideController; use App\Http\Controllers\Site\ModelController; use App\Http\Controllers\Site\NewsController; @@ -35,6 +38,7 @@ Route::get('/news/{slug}', [NewsController::class, 'show'])->name('news.show'); Route::get('/guides', [GuideController::class, 'index'])->name('guides.index'); Route::get('/guides/topic/{slug}', [GuideController::class, 'byTopic'])->name('guides.by-topic'); Route::get('/guides/{slug}', [GuideController::class, 'show'])->name('guides.show'); +Route::post('/feedback', [FeedbackController::class, 'store'])->name('feedback.store'); Route::get('/robots.txt', [SeoController::class, 'robots'])->name('seo.robots'); Route::get('/sitemap.xml', [SeoController::class, 'sitemap'])->name('seo.sitemap'); @@ -95,5 +99,11 @@ Route::prefix('admin')->name('admin.')->group(function (): void { Route::post('/sources', [AdminSourceController::class, 'store'])->name('sources.store'); Route::get('/sources/{source}/edit', [AdminSourceController::class, 'edit'])->name('sources.edit'); Route::put('/sources/{source}', [AdminSourceController::class, 'update'])->name('sources.update'); + + Route::get('/settings', [AdminSiteSettingController::class, 'index'])->name('settings.index'); + Route::put('/settings', [AdminSiteSettingController::class, 'update'])->name('settings.update'); + + Route::get('/feedback', [AdminFeedbackController::class, 'index'])->name('feedback.index'); + Route::put('/feedback/{feedback}', [AdminFeedbackController::class, 'updateStatus'])->name('feedback.status'); }); });