From a63323423915d47c2f6f38ea489a6cac3f255b33 Mon Sep 17 00:00:00 2001 From: "jiangdong.cheng" Date: Thu, 12 Feb 2026 10:31:53 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E7=95=8C=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 41 ++ app/Http/Controllers/Site/GuideController.php | 25 +- app/Http/Controllers/Site/ModelController.php | 24 +- app/Http/Controllers/Site/NewsController.php | 23 +- resources/views/layouts/site.blade.php | 474 +++++++++++-- resources/views/public/guides/index.blade.php | 216 ++++-- resources/views/public/guides/show.blade.php | 28 +- resources/views/public/home.blade.php | 571 ++++----------- resources/views/public/models/index.blade.php | 241 ++++--- resources/views/public/news/index.blade.php | 223 +++--- resources/views/public/news/show.blade.php | 56 +- resources/views/public/tools/index.blade.php | 671 ++++++++++++++++++ resources/views/public/tools/show.blade.php | 12 +- 13 files changed, 1856 insertions(+), 749 deletions(-) create mode 100644 AGENTS.md create mode 100644 resources/views/public/tools/index.blade.php diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..4c9956e --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,41 @@ +# Repository Guidelines + +## Project Structure & Module Organization +- `app/` contains core backend code (`Http/Controllers`, `Services`, `Models`, `Support`, `Enums`). +- `routes/web.php` defines HTTP routes; `routes/console.php` holds CLI routes. +- `resources/views` stores Blade templates, while `resources/js` and `resources/css` are Vite frontend entry points. +- `database/migrations`, `database/seeders`, and `database/factories` manage schema and seed data. +- `tests/Feature` covers HTTP/integration behavior; `tests/Unit` covers isolated domain logic. +- `public/` is web root; generated assets are bundled by Vite. + +## Build, Test, and Development Commands +- `composer install` — install PHP dependencies. +- `npm install` — install frontend build tooling. +- `composer setup` — one-shot project bootstrap (`.env`, key, migration, frontend build). +- `composer dev` — run local development stack (Laravel server, queue listener, logs, Vite). +- `php artisan serve` — run backend only. +- `npm run dev` / `npm run build` — start Vite dev server or build production assets. +- `composer test` (or `php artisan test`) — clear config and run test suites. + +## Coding Style & Naming Conventions +- Follow `.editorconfig`: UTF-8, LF, 4-space indent (YAML: 2 spaces), trim trailing whitespace. +- PHP follows PSR-12 and Laravel conventions; format with `./vendor/bin/pint` before PR. +- Use PSR-4 namespaces under `App\` with clear suffixes (`*Controller`, `*Request`, `*Service`). +- Use `StudlyCase` for classes, `camelCase` for methods/variables, and `snake_case` for DB columns/migration names. + +## Testing Guidelines +- Framework: PHPUnit 11 via Laravel test runner. +- Place tests in `tests/Feature` or `tests/Unit`; filename pattern: `*Test.php`. +- Add/adjust tests for every behavioral change; prioritize Feature tests for route/controller updates. +- Keep tests deterministic; use factories/seed data and avoid external service dependencies. + +## Commit & Pull Request Guidelines +- Current history uses short summaries (e.g., `init`, `添加 1`); keep commits concise, imperative, and scoped. +- Prefer format like `feat: add admin article filter` or `fix: validate model score bounds`. +- PRs should include: purpose, key changes, test evidence (`composer test` output), and related issue IDs. +- Include screenshots/GIFs for UI changes under `resources/views` or frontend assets. + +## Security & Configuration Tips +- Never commit secrets from `.env`; keep `.env.example` updated for new config keys. +- Use least-privilege credentials for local/dev databases. +- Validate and authorize all admin-side inputs using Form Requests and middleware. diff --git a/app/Http/Controllers/Site/GuideController.php b/app/Http/Controllers/Site/GuideController.php index e1b8c95..1833ad2 100644 --- a/app/Http/Controllers/Site/GuideController.php +++ b/app/Http/Controllers/Site/GuideController.php @@ -6,7 +6,6 @@ namespace App\Http\Controllers\Site; use App\Http\Controllers\Controller; use App\Models\AiModel; -use App\Models\Category; use App\Models\Guide; use App\Models\Tool; use App\Support\MarkdownRenderer; @@ -32,9 +31,29 @@ class GuideController extends Controller $builder->where('difficulty', (string) $request->string('difficulty')); } + $difficultyCounts = Guide::query() + ->published() + ->selectRaw('difficulty, COUNT(*) as aggregate') + ->groupBy('difficulty') + ->pluck('aggregate', 'difficulty'); + + $difficultyOptions = [ + ['value' => 'beginner', 'label' => '入门', 'count' => (int) ($difficultyCounts['beginner'] ?? 0)], + ['value' => 'intermediate', 'label' => '进阶', 'count' => (int) ($difficultyCounts['intermediate'] ?? 0)], + ['value' => 'advanced', 'label' => '高级', 'count' => (int) ($difficultyCounts['advanced'] ?? 0)], + ]; + + $guideStats = [ + 'total' => Guide::query()->published()->count(), + 'beginner' => Guide::query()->published()->where('difficulty', 'beginner')->count(), + 'advanced' => Guide::query()->published()->where('difficulty', 'advanced')->count(), + 'updated_7d' => Guide::query()->published()->where('updated_at', '>=', now()->subDays(7))->count(), + ]; + return view('public.guides.index', [ 'items' => $builder->latest('published_at')->paginate(15)->withQueryString(), - 'categories' => Category::query()->where('type', 'guide')->where('is_active', true)->orderBy('name')->get(), + 'difficultyOptions' => $difficultyOptions, + 'guideStats' => $guideStats, 'filters' => $request->only(['q', 'difficulty']), 'sidebarTools' => Tool::published()->latest('published_at')->limit(6)->get(), 'sidebarModels' => AiModel::published()->orderByDesc('total_score')->limit(6)->get(), @@ -60,6 +79,8 @@ class GuideController extends Controller return view('public.guides.show', [ 'item' => $guide, 'bodyHtml' => $this->markdownRenderer->render($guide->body), + 'sidebarTools' => Tool::published()->latest('published_at')->limit(6)->get(), + 'sidebarModels' => AiModel::published()->orderByDesc('total_score')->limit(6)->get(), ]); } } diff --git a/app/Http/Controllers/Site/ModelController.php b/app/Http/Controllers/Site/ModelController.php index d032cdb..08a12f8 100644 --- a/app/Http/Controllers/Site/ModelController.php +++ b/app/Http/Controllers/Site/ModelController.php @@ -7,7 +7,6 @@ namespace App\Http\Controllers\Site; use App\Http\Controllers\Controller; use App\Models\AiModel; use App\Models\Article; -use App\Models\Category; use Illuminate\Contracts\View\View; use Illuminate\Database\Eloquent\Builder; use Illuminate\Http\Request; @@ -30,9 +29,30 @@ class ModelController extends Controller $builder->where('deployment_mode', (string) $request->string('deployment')); } + $modalityCounts = AiModel::query() + ->published() + ->selectRaw('modality, COUNT(*) as aggregate') + ->groupBy('modality') + ->pluck('aggregate', 'modality'); + + $modalityOptions = [ + ['value' => 'text', 'label' => '文本', 'count' => (int) ($modalityCounts['text'] ?? 0)], + ['value' => 'multimodal', 'label' => '多模态', 'count' => (int) ($modalityCounts['multimodal'] ?? 0)], + ['value' => 'image', 'label' => '图像', 'count' => (int) ($modalityCounts['image'] ?? 0)], + ['value' => 'audio', 'label' => '音频', 'count' => (int) ($modalityCounts['audio'] ?? 0)], + ]; + + $modelStats = [ + 'total' => AiModel::query()->published()->count(), + 'multimodal' => AiModel::query()->published()->where('modality', 'multimodal')->count(), + 'api' => AiModel::query()->published()->where('deployment_mode', 'api')->count(), + 'updated_7d' => AiModel::query()->published()->where('updated_at', '>=', now()->subDays(7))->count(), + ]; + return view('public.models.index', [ 'items' => $builder->orderByDesc('total_score')->paginate(18)->withQueryString(), - 'categories' => Category::query()->where('type', 'model')->where('is_active', true)->orderBy('name')->get(), + 'modalityOptions' => $modalityOptions, + 'modelStats' => $modelStats, 'filters' => $request->only(['q', 'modality', 'deployment']), 'sidebarTools' => \App\Models\Tool::published()->latest('published_at')->limit(6)->get(), 'sidebarNews' => Article::published()->latest('published_at')->limit(6)->get(), diff --git a/app/Http/Controllers/Site/NewsController.php b/app/Http/Controllers/Site/NewsController.php index bada0c0..9fb38f6 100644 --- a/app/Http/Controllers/Site/NewsController.php +++ b/app/Http/Controllers/Site/NewsController.php @@ -11,6 +11,7 @@ use App\Models\Category; use App\Models\Guide; use App\Support\MarkdownRenderer; use Illuminate\Contracts\View\View; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Http\Request; class NewsController extends Controller @@ -34,9 +35,27 @@ class NewsController extends Controller }); } + $categories = Category::query() + ->where('type', 'news') + ->where('is_active', true) + ->withCount([ + 'articles as published_articles_count' => fn (Builder $query): Builder => $query->published(), + ]) + ->orderByDesc('published_articles_count') + ->orderBy('name') + ->get(); + + $newsStats = [ + 'total' => Article::query()->published()->count(), + 'today' => Article::query()->published()->whereDate('published_at', today())->count(), + 'high_source' => Article::query()->published()->where('source_level', 'high')->count(), + 'updated_7d' => Article::query()->published()->where('updated_at', '>=', now()->subDays(7))->count(), + ]; + return view('public.news.index', [ 'items' => $builder->latest('published_at')->paginate(15)->withQueryString(), - 'categories' => Category::query()->where('type', 'news')->where('is_active', true)->orderBy('name')->get(), + 'categories' => $categories, + 'newsStats' => $newsStats, 'filters' => $request->only(['q', 'category']), 'sidebarModels' => AiModel::published()->orderByDesc('total_score')->limit(6)->get(), 'sidebarGuides' => Guide::published()->latest('published_at')->limit(6)->get(), @@ -56,6 +75,8 @@ class NewsController extends Controller 'item' => $article, 'bodyHtml' => $this->markdownRenderer->render($article->body), 'showRiskNotice' => $this->containsRiskKeyword($article->title.' '.$article->body), + 'sidebarModels' => AiModel::published()->orderByDesc('total_score')->limit(6)->get(), + 'sidebarGuides' => Guide::published()->latest('published_at')->limit(6)->get(), ]); } diff --git a/resources/views/layouts/site.blade.php b/resources/views/layouts/site.blade.php index b6ea242..02f40cf 100644 --- a/resources/views/layouts/site.blade.php +++ b/resources/views/layouts/site.blade.php @@ -1,4 +1,4 @@ - + @@ -32,6 +32,8 @@ --radius-md: 10px; --shadow-sm: 0 8px 20px rgba(29, 53, 116, .07); --shadow-md: 0 14px 30px rgba(29, 53, 116, .12); + --page-max-width: 1480px; + --page-section-gap: .9rem; } html, @@ -49,6 +51,10 @@ var(--bg-page); } + .container { + max-width: var(--page-max-width); + } + a { color: var(--brand-strong); text-decoration: none; @@ -147,18 +153,370 @@ } .page-main { - padding: 1rem 0 1.2rem; + padding: .85rem 0 1.1rem; + } + + .channel-layout { + display: grid; + grid-template-columns: 196px minmax(0, 1fr); + gap: .9rem; + align-items: start; + } + + .channel-sidebar { + position: sticky; + top: .85rem; + max-height: calc(100vh - 1.7rem); + border: 1px solid var(--line); + border-radius: 12px; + background: #fff; + box-shadow: var(--shadow-sm); + overflow: auto; + padding: .58rem .5rem; + scrollbar-width: thin; + } + + .channel-brand { + display: flex; + align-items: center; + gap: .42rem; + padding: .2rem .38rem .58rem; + border-bottom: 1px solid #e6ebf5; + margin-bottom: .42rem; + font-family: "Outfit", "Noto Sans SC", sans-serif; + font-size: 1.02rem; + font-weight: 800; + color: #1f2f53; + } + + .channel-brand-mark { + width: 1.45rem; + height: 1.45rem; + border-radius: .45rem; + display: inline-flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, #8f8bff, #6471ff 45%, #4a58ed); + color: #fff; + font-size: .7rem; + font-family: "Outfit", "Noto Sans SC", sans-serif; + font-weight: 700; + } + + .channel-links { + display: grid; + gap: .12rem; + } + + .channel-link { + display: grid; + grid-template-columns: 1rem minmax(0, 1fr) auto; + align-items: center; + gap: .44rem; + border: 1px solid transparent; + border-radius: .52rem; + padding: .38rem .44rem; + text-decoration: none; + color: #4f5f80; + font-size: .83rem; + line-height: 1.15; + transition: .16s ease; + } + + .channel-link span { + font-weight: 500; + } + + .channel-link i { + color: #65789f; + font-size: .84rem; + text-align: center; + } + + .channel-link small { + color: #9aabc8; + font-size: .7rem; + } + + .channel-link:hover, + .channel-link.active { + border-color: #d6def0; + background: #f4f7ff; + color: #243861; + box-shadow: inset 2px 0 0 #5b6fff; + } + + .channel-main { + min-width: 0; + display: grid; + gap: .7rem; + } + + .channel-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: .65rem; + border: 1px solid var(--line); + border-radius: 12px; + background: #fff; + box-shadow: var(--shadow-sm); + padding: .54rem .66rem; + } + + .channel-tabs { + display: flex; + align-items: center; + gap: .35rem; + flex-wrap: wrap; + } + + .channel-tab { + border: 1px solid transparent; + border-radius: 999px; + padding: .2rem .56rem; + color: #56698d; + text-decoration: none; + font-size: .79rem; + white-space: nowrap; + transition: .16s ease; + } + + .channel-tab.active, + .channel-tab:hover { + border-color: #d7dff1; + background: #f4f7ff; + color: #27406f; + } + + .channel-status { + display: inline-flex; + align-items: center; + gap: .4rem; + color: #7486a8; + font-size: .76rem; + white-space: nowrap; + } + + .channel-status b { + color: #2e4270; + font-family: "Outfit", "Noto Sans SC", sans-serif; + font-size: .92rem; + } + + .channel-mobile-nav { + display: none; + gap: .3rem; + flex-wrap: wrap; + border: 1px solid var(--line); + border-radius: 12px; + padding: .44rem; + background: #fff; + box-shadow: var(--shadow-sm); + } + + .channel-mobile-nav .channel-link { + min-width: 150px; + flex: 1 1 180px; + } + + .channel-hero { + border: 1px solid var(--line); + border-radius: 12px; + background: linear-gradient(180deg, #f7f9fd 0, #f3f6fb 100%); + box-shadow: var(--shadow-sm); + padding: .9rem .9rem .82rem; + } + + .channel-chip { + display: inline-flex; + border: 1px solid #d8e0f0; + border-radius: 999px; + background: #fff; + color: #7182a6; + font-size: .68rem; + font-weight: 700; + padding: .14rem .48rem; + margin-bottom: .2rem; + } + + .channel-title { + margin: 0; + font-family: "Outfit", "Noto Sans SC", sans-serif; + color: #202f4f; + font-weight: 800; + font-size: clamp(1.45rem, 2.4vw, 2.05rem); + letter-spacing: .01em; + } + + .channel-subtitle { + margin: .28rem 0 .55rem; + color: #5f6f8b; + font-size: .87rem; + } + + .channel-search { + display: grid; + grid-template-columns: minmax(0, 1fr) auto auto auto auto; + gap: .42rem; + } + + .channel-search input, + .channel-search select { + height: 2.24rem; + border: 1px solid #d6def0; + border-radius: 999px; + background: #fff; + color: #33496f; + font-size: .83rem; + padding: 0 .78rem; + min-width: 0; + } + + .channel-search .btn { + border-radius: 999px; + min-width: 92px; + height: 2.24rem; + padding: 0 .95rem; + font-size: .82rem; + font-weight: 600; + } + + .channel-kpis { + margin-top: .56rem; + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: .5rem; + } + + .channel-kpi { + border: 1px solid var(--line); + border-radius: 10px; + background: #fff; + box-shadow: var(--shadow-sm); + padding: .58rem .62rem; + } + + .channel-kpi span { + color: var(--text-muted); + font-size: .76rem; + } + + .channel-kpi b { + display: block; + margin-top: .14rem; + font-family: "Outfit", "Noto Sans SC", sans-serif; + font-size: 1.12rem; + font-weight: 800; + color: var(--text-main); + line-height: 1.1; + } + + .channel-body { + display: grid; + grid-template-columns: minmax(0, 1fr) 320px; + gap: var(--page-section-gap); + } + + .channel-panel { + border: 1px solid var(--line); + border-radius: 12px; + background: #fff; + box-shadow: var(--shadow-sm); + padding: .8rem; + } + + .channel-panel-title { + margin: 0; + font-family: "Outfit", "Noto Sans SC", sans-serif; + font-size: 1rem; + font-weight: 700; + color: #22365c; + } + + .channel-panel-head { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: .62rem; + } + + .channel-list { + display: grid; + gap: .5rem; + } + + .entity-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: .58rem; + } + + .entity-card { + border: 1px solid var(--line); + border-radius: 10px; + background: #fff; + padding: .66rem .7rem; + transition: .18s ease; + } + + .entity-card:hover { + transform: translateY(-2px); + border-color: color-mix(in srgb, var(--brand) 35%, var(--line)); + box-shadow: 0 12px 25px rgba(36, 65, 138, .12); + } + + .entity-title { + display: block; + color: #24385f; + text-decoration: none; + font-weight: 700; + line-height: 1.25; + } + + .entity-meta { + margin-top: .2rem; + color: var(--text-muted); + font-size: .79rem; + } + + .entity-desc { + margin: .42rem 0 .5rem; + color: var(--text-muted); + font-size: .82rem; + } + + .side-list-item { + border: 1px solid var(--line); + border-radius: 10px; + background: #fff; + padding: .52rem .58rem; + } + + .side-list-item a { + display: block; + color: #24385f; + text-decoration: none; + font-weight: 600; + line-height: 1.25; + } + + .side-list-item small { + display: block; + margin-top: .2rem; + color: var(--text-muted); + font-size: .76rem; } .module-hero { position: relative; overflow: hidden; - border-radius: var(--radius-xl); + border-radius: 12px; border: 1px solid var(--line-strong); background: linear-gradient(140deg, #f2f6ff, #ebf2ff 58%, #f6f3ff); box-shadow: var(--shadow-md); - padding: 1.2rem 1.15rem; - margin-bottom: 1rem; + padding: 1rem .95rem; + margin-bottom: var(--page-section-gap); } .module-hero::after { @@ -208,8 +566,8 @@ .portal-grid, .module-grid { display: grid; - grid-template-columns: minmax(0, 1fr) 300px; - gap: 1rem; + grid-template-columns: minmax(0, 1fr) 320px; + gap: var(--page-section-gap); } .portal-card, @@ -217,14 +575,14 @@ .block-card { background: var(--bg-surface); border: 1px solid var(--line); - border-radius: var(--radius-xl); + border-radius: 12px; box-shadow: var(--shadow-sm); } .entry-item { border: 1px solid var(--line); - border-radius: var(--radius-lg); - padding: .9rem; + border-radius: 10px; + padding: .8rem; background: #fff; transition: transform .18s ease, box-shadow .18s ease, border-color .18s ease; } @@ -480,6 +838,48 @@ } @media (max-width: 991.98px) { + .channel-layout { + grid-template-columns: 1fr; + } + + .channel-sidebar { + display: none; + } + + .channel-mobile-nav { + display: flex; + } + + .channel-head { + flex-direction: column; + align-items: stretch; + } + + .channel-status { + justify-content: space-between; + } + + .channel-body { + grid-template-columns: 1fr; + } + + .channel-kpis { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .entity-grid { + grid-template-columns: 1fr; + } + + .channel-search { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .channel-search .btn { + width: 100%; + grid-column: 1 / -1; + } + .portal-grid, .module-grid { grid-template-columns: 1fr; @@ -493,11 +893,23 @@ } @media (max-width: 575.98px) { + .channel-kpis { + grid-template-columns: 1fr; + } + + .channel-search { + grid-template-columns: 1fr; + } + + .channel-mobile-nav .channel-link { + flex: 1 1 100%; + } + .module-hero, .portal-card, .surface-card, .block-card { - border-radius: 14px; + border-radius: 10px; } .module-hero { @@ -530,41 +942,6 @@ - -
@yield('content') @@ -573,7 +950,7 @@