Files
ai-web/resources/views/public/tools/index.blade.php
2026-02-12 13:06:12 +08:00

723 lines
22 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@extends('layouts.site')
@section('page_class', 'page-tools')
@section('title', 'AI工具集 - AIWeb')
@section('meta_description', 'AI工具集首页按分类分块浏览工具左侧菜单可定位到对应区块。')
@section('canonical', route('tools.index'))
@section('head')
<style>
html {
scroll-behavior: smooth;
}
.tool-home {
display: grid;
grid-template-columns: 186px minmax(0, 1fr);
gap: .68rem;
align-items: start;
}
.tool-side {
position: sticky;
top: .72rem;
border: 1px solid #d8e1f1;
border-radius: 12px;
background: #fff;
box-shadow: 0 8px 18px rgba(40, 63, 120, .08);
padding: .6rem .48rem;
max-height: calc(100vh - 1.44rem);
overflow: auto;
}
.tool-side-logo {
display: flex;
align-items: center;
gap: .38rem;
padding: .22rem .3rem .56rem;
border-bottom: 1px solid #e7edf8;
margin-bottom: .36rem;
font-family: "Outfit", "Noto Sans SC", sans-serif;
color: #131e44;
font-size: 1.12rem;
font-weight: 800;
}
.tool-side-logo-dot {
width: 1.35rem;
height: 1.35rem;
border-radius: .38rem;
background: linear-gradient(135deg, #a777ff, #6e77ff);
color: #fff;
font-size: .68rem;
font-weight: 700;
display: inline-flex;
align-items: center;
justify-content: center;
}
.tool-side-links {
display: grid;
gap: .16rem;
}
.tool-side-link {
display: grid;
grid-template-columns: 1rem minmax(0, 1fr) auto;
align-items: center;
gap: .42rem;
border: 1px solid transparent;
border-radius: .58rem;
text-decoration: none;
color: #4e6189;
padding: .38rem .42rem;
font-size: .82rem;
transition: .16s ease;
}
.tool-side-link:hover,
.tool-side-link.active {
border-color: #d4ddf2;
background: #f4f7ff;
color: #21345f;
box-shadow: inset 2px 0 0 #5f72ff;
}
.tool-side-link.is-active {
border-color: #d4ddf2;
background: #eef3ff;
color: #1f3157;
box-shadow: inset 3px 0 0 #536eff;
}
.tool-side-link i {
color: #687ba2;
text-align: center;
}
.tool-side-link small {
color: #97a9c8;
font-size: .7rem;
}
.tool-main {
min-width: 0;
display: grid;
gap: .62rem;
}
.tool-top {
display: flex;
align-items: center;
justify-content: space-between;
gap: .5rem;
border: 1px solid #d8e1f1;
border-radius: 12px;
background: #fff;
box-shadow: 0 8px 18px rgba(40, 63, 120, .08);
padding: .5rem .62rem;
}
.tool-top .channel-tab {
padding: .16rem .52rem;
font-size: .78rem;
}
.tool-status {
color: #7387ad;
font-size: .76rem;
white-space: nowrap;
}
.tool-status b {
color: #2c4172;
font-family: "Outfit", "Noto Sans SC", sans-serif;
font-size: .9rem;
}
.tool-hero {
border: 1px solid #d8e1f1;
border-radius: 12px;
background: linear-gradient(180deg, #f5f8ff 0, #eef3fb 100%);
box-shadow: 0 8px 18px rgba(40, 63, 120, .08);
padding: .95rem .95rem .82rem;
text-align: center;
}
.tool-chip {
display: inline-flex;
border: 1px solid #d8e0f0;
border-radius: 999px;
background: #fff;
color: #6f81a7;
font-size: .68rem;
font-weight: 700;
padding: .14rem .5rem;
}
.tool-title {
margin: .34rem 0 .18rem;
font-family: "Outfit", "Noto Sans SC", sans-serif;
font-size: clamp(1.6rem, 2.8vw, 2.45rem);
font-weight: 800;
color: #141f47;
}
.tool-sub {
margin: 0;
color: #66799f;
font-size: .84rem;
}
.tool-search {
margin: .62rem auto .38rem;
width: min(860px, 100%);
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: .38rem;
border: 1px solid #d4deef;
border-radius: 999px;
background: #fff;
padding: .3rem;
}
.tool-search input {
border: 0;
box-shadow: none;
height: 2.16rem;
padding: 0 .82rem;
background: transparent;
}
.tool-search button {
border-radius: 999px;
min-width: 90px;
height: 2.16rem;
font-size: .82rem;
}
.tool-kpis {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: .48rem;
}
.tool-kpi {
border: 1px solid #d7e0f1;
border-radius: 10px;
background: #fff;
padding: .56rem .6rem;
text-align: left;
}
.tool-kpi span {
font-size: .74rem;
color: #6d80a5;
}
.tool-kpi b {
display: block;
margin-top: .12rem;
font-family: "Outfit", "Noto Sans SC", sans-serif;
font-size: 1.08rem;
font-weight: 800;
color: #1c2d55;
}
.tool-channel {
border: 1px solid #d8e1f1;
border-radius: 12px;
background: #fff;
box-shadow: 0 8px 18px rgba(40, 63, 120, .08);
padding: .52rem;
display: grid;
grid-template-columns: 62px repeat(5, minmax(0, 1fr));
gap: .46rem;
}
.tool-channel-mini {
border: 1px solid #dee6f5;
border-radius: 10px;
background: #f8faff;
display: grid;
gap: .34rem;
padding: .44rem .3rem;
}
.tool-channel-mini a {
text-decoration: none;
color: #5f7399;
font-size: .72rem;
text-align: center;
}
.tool-channel-card {
border: 1px solid #dce4f3;
border-radius: 10px;
min-height: 94px;
display: flex;
align-items: center;
justify-content: center;
text-decoration: none;
font-family: "Outfit", "Noto Sans SC", sans-serif;
font-size: 1.8rem;
font-weight: 800;
color: #273a63;
}
.tool-channel-card.news { background: #dce8ff; }
.tool-channel-card.community { background: #dff3da; }
.tool-channel-card.project { background: #ede0fa; }
.tool-channel-card.tutorial { background: #d8f0ff; }
.tool-channel-card.ad { background: #f4f6ff; }
.tool-banner-row {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: .52rem;
}
.tool-banner {
border: 1px solid #d8e1f1;
border-radius: 10px;
background: linear-gradient(90deg, #e8f3ff, #e4f7ef);
min-height: 66px;
display: flex;
align-items: center;
justify-content: space-between;
gap: .52rem;
padding: .58rem .72rem;
}
.tool-banner.alt {
background: linear-gradient(90deg, #e6f6ff, #dff8dd);
}
.tool-banner b {
color: #1d3058;
font-size: 1.06rem;
font-family: "Outfit", "Noto Sans SC", sans-serif;
}
.tool-banner small {
border: 1px solid #d6def0;
border-radius: 999px;
background: #fff;
color: #5f749a;
font-size: .72rem;
padding: .14rem .48rem;
}
.tool-section {
border: 1px solid #d8e1f1;
border-radius: 12px;
background: #fff;
box-shadow: 0 8px 18px rgba(40, 63, 120, .08);
padding: .62rem;
scroll-margin-top: 1rem;
}
.tool-section-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: .48rem;
}
.tool-section-head h2 {
margin: 0;
color: #20335d;
font-size: 1rem;
font-family: "Outfit", "Noto Sans SC", sans-serif;
font-weight: 700;
}
.tool-section-head a {
color: #4d63a4;
text-decoration: none;
font-size: .76rem;
font-weight: 600;
}
.tool-grid {
display: grid;
grid-template-columns: repeat(6, minmax(0, 1fr));
gap: .52rem;
}
.tool-card {
border: 1px solid #dbe3f2;
border-radius: 10px;
background: #fff;
padding: .52rem .56rem;
text-decoration: none;
color: #1f3157;
transition: .16s ease;
min-height: 88px;
display: grid;
align-content: start;
gap: .25rem;
}
.tool-card:hover {
border-color: #cdd9f0;
box-shadow: 0 12px 24px rgba(45, 66, 122, .12);
transform: translateY(-2px);
}
.tool-card-name {
font-size: .85rem;
font-weight: 700;
line-height: 1.3;
}
.tool-card-meta {
font-size: .72rem;
color: #7084a8;
line-height: 1.25;
}
.tool-card-tag {
display: inline-flex;
border: 1px solid #d6def0;
border-radius: 999px;
background: #f6f8ff;
color: #4f639b;
font-size: .68rem;
padding: .06rem .4rem;
}
.tool-mobile-cats {
display: none;
gap: .28rem;
flex-wrap: wrap;
border: 1px solid #d8e1f1;
border-radius: 12px;
background: #fff;
box-shadow: 0 8px 18px rgba(40, 63, 120, .08);
padding: .42rem;
}
.tool-mobile-cats .tool-side-link {
flex: 1 1 160px;
min-width: 144px;
}
@media (max-width: 1399.98px) {
.tool-grid {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
}
@media (max-width: 991.98px) {
.tool-home {
grid-template-columns: 1fr;
}
.tool-side {
display: none;
}
.tool-mobile-cats {
display: flex;
}
.tool-top {
flex-direction: column;
align-items: stretch;
}
.tool-channel {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.tool-channel-mini {
grid-column: 1 / -1;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: .22rem;
}
.tool-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.tool-banner-row {
grid-template-columns: 1fr;
}
.tool-kpis {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 575.98px) {
.tool-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.tool-kpis {
grid-template-columns: 1fr;
}
.tool-search {
grid-template-columns: 1fr;
border-radius: 12px;
}
.tool-search button {
width: 100%;
border-radius: 10px;
}
}
</style>
@endsection
@section('content')
@php
$icons = ['bi-stars', 'bi-pencil', 'bi-image', 'bi-camera-video', 'bi-briefcase', 'bi-cpu', 'bi-chat-dots', 'bi-code-slash', 'bi-kanban'];
@endphp
<div class="tool-home">
<aside class="tool-side" aria-label="工具分类侧边栏">
<div class="tool-side-logo"><span class="tool-side-logo-dot">AI</span>AI工具集</div>
<nav class="tool-side-links">
@if(($modules['hot_tools']['enabled'] ?? true) === true)
<a class="tool-side-link" href="#section-hot">
<i class="bi bi-fire"></i>
<span>热门工具</span>
<small>{{ $hotTools->count() }}</small>
</a>
@endif
@if(($modules['latest_tools']['enabled'] ?? true) === true)
<a class="tool-side-link" href="#section-latest">
<i class="bi bi-clock-history"></i>
<span>最新收录</span>
<small>{{ $latestTools->count() }}</small>
</a>
@endif
@if(($modules['category_sections']['enabled'] ?? true) === true)
@foreach($categorySections as $index => $section)
<a class="tool-side-link" href="#section-{{ $section['slug'] }}">
<i class="bi {{ $icons[$index % count($icons)] }}"></i>
<span>{{ $section['name'] }}</span>
<small>{{ $section['count'] }}</small>
</a>
@endforeach
@endif
</nav>
</aside>
<section class="tool-main">
<x-portal.top-nav active="tools" status-label="收录工具" :status-value="$toolStats['total'] ?? 0" wrapper-class="tool-top" />
<nav class="tool-mobile-cats" aria-label="移动分类导航">
@if(($modules['hot_tools']['enabled'] ?? true) === true)
<a class="tool-side-link" href="#section-hot"><i class="bi bi-fire"></i><span>热门</span><small>{{ $hotTools->count() }}</small></a>
@endif
@if(($modules['latest_tools']['enabled'] ?? true) === true)
<a class="tool-side-link" href="#section-latest"><i class="bi bi-clock-history"></i><span>最新</span><small>{{ $latestTools->count() }}</small></a>
@endif
@if(($modules['category_sections']['enabled'] ?? true) === true)
@foreach($categorySections as $index => $section)
<a class="tool-side-link" href="#section-{{ $section['slug'] }}">
<i class="bi {{ $icons[$index % count($icons)] }}"></i>
<span>{{ $section['name'] }}</span>
<small>{{ $section['count'] }}</small>
</a>
@endforeach
@endif
</nav>
<section class="tool-hero">
<span class="tool-chip">AI-BOT.CN</span>
<h1 class="tool-title">AI工具集</h1>
<p class="tool-sub">左侧菜单点击后直接定位到对应工具区块,符合你要的交互方式。</p>
<form class="tool-search" method="get" action="{{ route('tools.list') }}" role="search" aria-label="站内 AI 工具搜索">
<input type="hidden" name="tab" value="{{ $activeTab }}">
<input type="search" name="q" value="{{ $filters['q'] ?? '' }}" placeholder="站内AI工具搜索写作、图像、办公自动化" autocomplete="off" spellcheck="false">
<button class="btn btn-primary" type="submit"><i class="bi bi-search"></i> 搜索</button>
</form>
<nav class="channel-tabs justify-content-center" aria-label="筛选模式">
@foreach($tabOptions as $tab)
<a class="channel-tab @if($activeTab === $tab['key']) active @endif" href="{{ route('tools.index', array_filter(array_merge($filters, ['tab' => $tab['key']]))) }}">{{ $tab['label'] }}</a>
@endforeach
</nav>
<x-portal.stat-grid
grid-class="tool-kpis"
item-class="tool-kpi"
:stats="[
['label' => '收录工具', 'value' => $toolStats['total'] ?? 0],
['label' => '支持 API', 'value' => $toolStats['api'] ?? 0],
['label' => '免费可用', 'value' => $toolStats['free'] ?? 0],
['label' => '7天更新', 'value' => $toolStats['updated_7d'] ?? 0],
]"
/>
</section>
@if(($modules['channel_cards']['enabled'] ?? true) === true)
<section class="tool-channel" aria-label="频道入口">
<aside class="tool-channel-mini">
<a href="{{ route('news.index') }}"><i class="bi bi-newspaper"></i>AI资讯</a>
<a href="{{ route('models.index') }}"><i class="bi bi-box"></i>AI项目</a>
<a href="{{ route('guides.index') }}"><i class="bi bi-journal"></i>AI百科</a>
<a href="{{ route('home') }}"><i class="bi bi-house"></i>首页</a>
</aside>
<a class="tool-channel-card news" href="{{ route('news.index') }}">每日快讯</a>
<a class="tool-channel-card community" href="{{ route('guides.index') }}">免费社群</a>
<a class="tool-channel-card project" href="{{ route('models.index') }}">最新项目</a>
<a class="tool-channel-card tutorial" href="{{ route('guides.index') }}">热门教程</a>
<a class="tool-channel-card ad" href="{{ route('tools.list') }}">工具列表</a>
</section>
@endif
@if(($modules['promo_banners']['enabled'] ?? true) === true)
<section class="tool-banner-row" aria-label="横幅推荐">
<article class="tool-banner">
<b>超全图片视频模板一键复制</b>
<small>全球顶尖模型</small>
</article>
<article class="tool-banner alt">
<b>一站式 AI 创作平台</b>
<small>免费试用</small>
</article>
</section>
@endif
@if(($modules['hot_tools']['enabled'] ?? true) === true)
<section id="section-hot" class="tool-section" aria-label="热门工具">
<header class="tool-section-head">
<h2><i class="bi bi-fire text-danger"></i> 热门工具</h2>
<a href="{{ route('tools.list', ['tab' => 'recommended']) }}">查看更多</a>
</header>
<x-portal.tool-grid :tools="$hotTools" />
</section>
@endif
@if(($modules['latest_tools']['enabled'] ?? true) === true)
<section id="section-latest" class="tool-section" aria-label="最新收录">
<header class="tool-section-head">
<h2><i class="bi bi-clock-history text-primary"></i> 最新收录</h2>
<a href="{{ route('tools.list', ['tab' => 'latest']) }}">查看更多</a>
</header>
<x-portal.tool-grid :tools="$latestTools" />
</section>
@endif
@if(($modules['category_sections']['enabled'] ?? true) === true)
@foreach($categorySections as $section)
<section id="section-{{ $section['slug'] }}" class="tool-section" aria-label="{{ $section['name'] }}工具">
<header class="tool-section-head">
<h2><i class="bi bi-grid-3x3-gap text-primary"></i> {{ $section['name'] }}</h2>
<a href="{{ route('tools.list', ['category' => $section['slug']]) }}">查看更多</a>
</header>
@if($section['tools']->isNotEmpty())
<x-portal.tool-grid :tools="$section['tools']" />
@else
<p class="text-muted-soft mb-0">该分类暂未收录工具</p>
@endif
</section>
@endforeach
@endif
</section>
</div>
@endsection
@section('scripts')
<script>
(() => {
const links = Array.from(document.querySelectorAll('.tool-side-link[href^="#section-"]'));
const sections = Array.from(document.querySelectorAll('.tool-section[id^="section-"]'));
if (!links.length || !sections.length || !('IntersectionObserver' in window)) {
return;
}
const linksById = new Map();
links.forEach((link) => {
const id = link.getAttribute('href')?.replace('#', '');
if (id) {
const group = linksById.get(id) || [];
group.push(link);
linksById.set(id, group);
}
});
const setActive = (activeId) => {
links.forEach((link) => {
const id = link.getAttribute('href')?.replace('#', '') || '';
link.classList.toggle('is-active', id === activeId);
});
};
const syncHash = (id) => {
const hash = `#${id}`;
if (window.location.hash !== hash) {
history.replaceState(null, '', hash);
}
};
links.forEach((link) => {
link.addEventListener('click', (event) => {
const id = link.getAttribute('href')?.replace('#', '');
if (!id) {
return;
}
const target = document.getElementById(id);
if (!target) {
return;
}
event.preventDefault();
setActive(id);
syncHash(id);
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
});
});
const observer = new IntersectionObserver((entries) => {
const visible = entries
.filter((entry) => entry.isIntersecting)
.sort((a, b) => b.intersectionRatio - a.intersectionRatio);
if (!visible.length) {
return;
}
const top = visible[0].target;
if (top instanceof HTMLElement) {
setActive(top.id);
syncHash(top.id);
}
}, {
rootMargin: '-18% 0px -64% 0px',
threshold: [0.2, 0.45, 0.7],
});
sections.forEach((section) => observer.observe(section));
if (window.location.hash) {
const hashId = window.location.hash.replace('#', '');
const target = document.getElementById(hashId);
if (target) {
setActive(hashId);
window.requestAnimationFrame(() => {
target.scrollIntoView({ behavior: 'auto', block: 'start' });
});
} else {
setActive(sections[0].id);
syncHash(sections[0].id);
}
} else {
setActive(sections[0].id);
syncHash(sections[0].id);
}
})();
</script>
@endsection