Просмотр исходного кода

feat(home): 新增首页视图、卡片组件与样式

visuddhinanda 2 дней назад
Родитель
Сommit
b09e53b168

+ 264 - 0
api-v13/resources/css/home.css

@@ -0,0 +1,264 @@
+/* resources/css/home.css
+   WikiPāli 首页(next.wikipali.org)样式入口。
+   只服务于首页:品牌 hero + 四张通栏分流卡片。
+   平面、哑光、克制,无渐变 / 发光 / 阴影 / 圆角卡片。
+*/
+
+/* 1. Tabler(Bootstrap 5 底座)+ Tabler Icons */
+@import '@tabler/core/dist/css/tabler.min.css';
+@import '@tabler/icons-webfont/dist/tabler-icons.min.css';
+
+/* 2. 设计令牌 */
+:root {
+    --home-bg: #F6F1E7;
+    --home-text: #2B2318;
+    --home-text-secondary: #63594A;
+    --home-text-muted: #6F6555;
+    --home-border-muted: #B6AB98;
+    --home-cta-hover-text: #F6F1E7;
+    --home-hero-tint: #8A6A3C;
+}
+
+/* 3. 页面底色 */
+.home-page {
+    margin: 0;
+    background: var(--home-bg);
+    color: var(--home-text);
+}
+
+.home-page .page {
+    background: var(--home-bg);
+}
+
+/* 4. 品牌 Hero */
+.home-hero {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    text-align: center;
+    height: 44vh;
+    min-height: 280px;
+    padding: 2rem 1.5rem;
+    background: color-mix(in srgb, var(--home-hero-tint) 16%, var(--home-bg));
+}
+
+.home-hero__title {
+    margin: 0 0 .75rem;
+    color: var(--home-text);
+    font-size: clamp(2rem, 5vw, 3.5rem);
+    font-weight: 500;
+    line-height: 1.2;
+    letter-spacing: .02em;
+}
+
+.home-hero__lead {
+    margin: 0;
+    color: var(--home-text-secondary);
+    font-size: clamp(.95rem, 1.6vw, 1.125rem);
+    line-height: 1.5;
+}
+
+/* 5. 通栏卡片 */
+.home-card {
+    position: relative;
+    display: flex;
+    align-items: center;
+    height: 40vh;
+    min-height: 320px;
+    max-height: 480px;
+    overflow: hidden;
+    color: var(--home-text);
+}
+
+/* 末卡(二次开发)高度略矮 */
+#card-development {
+    height: 36vh;
+    min-height: 280px;
+    max-height: none;
+}
+
+/* 底图:通栏满铺 */
+.home-card__media-wrap {
+    position: absolute;
+    inset: 0;
+    display: block;
+    margin: 0;
+    padding: 0;
+}
+
+.home-card__media {
+    display: block;
+    width: 100%;
+    height: 100%;
+    object-fit: cover;
+}
+
+/* 文字在左 → 图的视觉重心在右;文字在右 → 视觉重心在左 */
+.home-card--start .home-card__media {
+    object-position: 70% center;
+}
+
+.home-card--end .home-card__media {
+    object-position: 30% center;
+}
+
+/* 蒙版:保证文字对比度 ≥ 4.5:1 */
+.home-card__veil {
+    position: absolute;
+    inset: 0;
+    background: color-mix(in srgb, var(--card-tint) 16%, rgba(246, 241, 231, .8));
+}
+
+/* 文字块:垂直居中,横向贴边 */
+.home-card__body {
+    position: relative;
+    z-index: 1;
+    display: flex;
+    align-items: center;
+    width: 100%;
+    padding: 2rem clamp(1.5rem, 6vw, 5rem);
+}
+
+.home-card--start .home-card__body {
+    justify-content: flex-start;
+    padding-left: calc(clamp(1.5rem, 6vw, 5rem) + 300px);
+}
+
+.home-card--end .home-card__body {
+    justify-content: flex-end;
+    padding-right: calc(clamp(1.5rem, 6vw, 5rem) + 300px);
+}
+
+.home-card__text {
+    width: 100%;
+    max-width: 26rem;
+}
+
+.home-card--start .home-card__text {
+    text-align: left;
+}
+
+.home-card--end .home-card__text {
+    text-align: right;
+}
+
+.home-card__eyebrow {
+    margin: 0 0 .5rem;
+    color: var(--home-text-muted);
+    font-size: 12px;
+    letter-spacing: .22em;
+    text-transform: uppercase;
+}
+
+.home-card__title {
+    margin: 0 0 .75rem;
+    color: var(--home-text);
+    font-size: clamp(1.5rem, 3.2vw, 2.5rem);
+    font-weight: 500;
+    line-height: 1.3;
+}
+
+.home-card__lead {
+    margin: 0 0 1.5rem;
+    color: var(--home-text-secondary);
+    font-size: clamp(.9rem, 1.4vw, 1.0625rem);
+    line-height: 1.5;
+}
+
+/* CTA / 状态角标:胶囊形 */
+.home-card__cta,
+.home-card__badge {
+    display: inline-flex;
+    align-items: center;
+    gap: .5rem;
+    border-radius: 999px;
+    padding: .55rem 1.25rem;
+    font-size: 14px;
+    line-height: 1;
+}
+
+.home-card__cta {
+    border: 1px solid var(--home-text);
+    color: var(--home-text);
+    text-decoration: none;
+    transition: background-color .2s ease, color .2s ease;
+}
+
+.home-card__cta:hover,
+.home-card__cta:focus {
+    background: var(--home-text);
+    color: var(--home-cta-hover-text);
+    text-decoration: none;
+}
+
+.home-card__cta:focus-visible {
+    outline: 2px solid var(--home-text);
+    outline-offset: 3px;
+}
+
+.home-card__badge {
+    border: 1px solid var(--home-border-muted);
+    color: var(--home-text-muted);
+}
+
+/* 6. 响应式:768–991px(高度不变,文字块收窄) */
+@media (max-width: 991.98px) {
+    .home-card__text {
+        max-width: 20rem;
+    }
+}
+
+/* 7. 响应式:<768px(全部左对齐,换移动端高度,蒙版加深) */
+@media (max-width: 767.98px) {
+    .home-hero {
+        height: 40vh;
+        min-height: 240px;
+    }
+
+    .home-card {
+        height: 52vh;
+        min-height: 300px;
+        max-height: none;
+    }
+
+    #card-development {
+        height: 46vh;
+        min-height: 260px;
+    }
+
+    .home-card__body {
+        justify-content: flex-start;
+        padding: 2rem 1.5rem;
+    }
+
+    .home-card--start .home-card__body,
+    .home-card--end .home-card__body {
+        padding-left: 1.5rem;
+        padding-right: 1.5rem;
+    }
+
+    .home-card__text {
+        max-width: 100%;
+        text-align: left;
+    }
+
+    .home-card__media {
+        object-position: 75% center;
+    }
+
+    .home-card__veil {
+        background: color-mix(in srgb, var(--card-tint) 20%, rgba(246, 241, 231, .88));
+    }
+
+    .home-card__eyebrow {
+        font-size: 11px;
+    }
+}
+
+/* 8. 尊重减少动态偏好:关闭过渡 */
+@media (prefers-reduced-motion: reduce) {
+    .home-card__cta {
+        transition: none;
+    }
+}

+ 115 - 0
api-v13/resources/views/components/home/card.blade.php

@@ -0,0 +1,115 @@
+{{-- resources/views/components/home/card.blade.php
+     WikiPāli 首页通栏卡片。底图满铺,文字压在图上。
+     available && href 非空 → 渲染链接按钮;否则渲染不可点击的状态角标。
+
+     底图按「存在的文件」动态输出:优先 AVIF → WebP → JPEG 兜底。
+     有底图时直接显示底图(不加颜色混合蒙版);素材未就位时才用蒙版纯色块占位,不引用任何不存在的文件,
+     避免浏览器选中 AVIF 源却 404 导致整张图不显示。
+--}}
+@props([
+    'slug',
+    'eyebrow',
+    'title',
+    'lead',
+    'image',
+    'tint',
+    'align',
+    'href' => null,
+    'cta',
+    'available' => false,
+    'eager' => false,
+])
+
+@php
+    $sizes = [640, 1280, 2560];
+    $formats = ['avif' => 'image/avif', 'webp' => 'image/webp', 'jpg' => 'image/jpeg'];
+
+    // 收集磁盘上实际存在的底图文件
+    $byFormat = []; // ext => [ ['url'=>..., 'width'=>...], ... ]
+    foreach ($formats as $ext => $mime) {
+        $found = [];
+        foreach ($sizes as $width) {
+            $url = "/img/home/{$image}-{$width}.{$ext}";
+            if (is_file(public_path("img/home/{$image}-{$width}.{$ext}"))) {
+                $found[] = ['url' => $url, 'width' => $width];
+            }
+        }
+        if ($found !== []) {
+            $byFormat[$ext] = $found;
+        }
+    }
+
+    // 每种存在格式的 srcset 串
+    $sets = [];
+    foreach ($byFormat as $ext => $entries) {
+        $parts = [];
+        foreach ($entries as $e) {
+            $parts[] = $e['url'].' '.$e['width'].'w';
+        }
+        $sets[$ext] = implode(', ', $parts);
+    }
+
+    // <img> 兜底:优先 jpg,其次 webp,再次 avif;尺寸优先 1280
+    $fallbackFormat = null;
+    foreach (['jpg', 'webp', 'avif'] as $ext) {
+        if (isset($byFormat[$ext])) {
+            $fallbackFormat = $ext;
+            break;
+        }
+    }
+
+    $fallbackSrc = null;
+    if ($fallbackFormat !== null) {
+        foreach ($byFormat[$fallbackFormat] as $e) {
+            if ($e['width'] === 1280) {
+                $fallbackSrc = $e['url'];
+                break;
+            }
+        }
+        $fallbackSrc ??= $byFormat[$fallbackFormat][0]['url'];
+    }
+
+    $titleId = "{$slug}-title";
+@endphp
+
+<section id="card-{{ $slug }}"
+    class="home-card home-card--{{ $align }}"
+    style="--card-tint: {{ $tint }}"
+    aria-labelledby="{{ $titleId }}">
+
+    @if ($fallbackSrc !== null)
+    <picture class="home-card__media-wrap">
+        @if (isset($sets['avif']))
+        <source type="image/avif" srcset="{{ $sets['avif'] }}" sizes="100vw">
+        @endif
+        @if (isset($sets['webp']))
+        <source type="image/webp" srcset="{{ $sets['webp'] }}" sizes="100vw">
+        @endif
+        <img class="home-card__media"
+            src="{{ $fallbackSrc }}"
+            @if (isset($sets[$fallbackFormat])) srcset="{{ $sets[$fallbackFormat] }}" sizes="100vw" @endif
+            alt=""
+            decoding="async"
+            @if($eager) loading="eager" fetchpriority="high" @else loading="lazy" @endif>
+    </picture>
+    @else
+    <div class="home-card__veil" aria-hidden="true"></div>
+    @endif
+
+    <div class="home-card__body">
+        <div class="home-card__text">
+            <p class="home-card__eyebrow">{{ $eyebrow }}</p>
+            <h2 class="home-card__title" id="{{ $titleId }}">{{ $title }}</h2>
+            <p class="home-card__lead">{{ $lead }}</p>
+
+            @if ($available && filled($href))
+                <a class="home-card__cta" href="{{ $href }}">{{ $cta }}</a>
+            @else
+                <span class="home-card__badge">
+                    <i class="ti ti-clock" aria-hidden="true"></i>
+                    {{ $cta }}
+                </span>
+            @endif
+        </div>
+    </div>
+</section>

+ 42 - 0
api-v13/resources/views/pages/home.blade.php

@@ -0,0 +1,42 @@
+{{-- resources/views/pages/home.blade.php
+     WikiPāli 首页:品牌 hero + 四张通栏分流卡片。
+     卡片数据全部来自 PageIndexController 传入的 $cards,模板不硬编码栏目文案。
+--}}
+@extends('layouts.base')
+
+@section('title', 'WikiPāli · 巴利佛典百科')
+
+@push('styles')
+@vite(['resources/css/home.css'])
+@endpush
+
+@section('body-class', 'home-page')
+
+@section('page')
+<main class="home">
+
+    {{-- 品牌 Hero:站名 + 一行说明,居中 --}}
+    <header class="home-hero">
+        <h1 class="home-hero__title">WikiPāli</h1>
+        <p class="home-hero__lead">巴利佛典百科 —— 由浅入深,进入巴利圣典的世界</p>
+    </header>
+
+    {{-- 四张通栏卡片,纵向排列 --}}
+    @foreach ($cards as $card)
+        <x-home.card
+            :slug="$card['slug']"
+            :eyebrow="$card['eyebrow']"
+            :title="$card['title']"
+            :lead="$card['lead']"
+            :image="$card['image']"
+            :tint="$card['tint']"
+            :align="$card['align']"
+            :href="$card['href']"
+            :cta="$card['cta']"
+            :available="$card['available']"
+            :eager="$loop->first"
+        />
+    @endforeach
+
+</main>
+@endsection