master
  1---
  2import { siteConfig } from '@/config';
  3import { pathMatch, pathsEqual, url } from '@utils/url-utils';
  4import { Icon } from 'astro-icon/components';
  5import ImageWrapper from './utils/ImageWrapper.astro';
  6
  7interface Props {
  8  src?: string;
  9  basePath?: string;
 10}
 11
 12const siteBanner = siteConfig.banner;
 13if (siteBanner === false) throw Error('Should not show this error');
 14
 15const src = Astro.props.src || siteBanner.src;
 16
 17function getBannerHeight(path: string) {
 18  if (siteConfig.banner === false) {
 19    console.error('Banner is disabled. Should not show this error, must be a bug');
 20    return null;
 21  }
 22  if (pathsEqual(url('/'), path)) {
 23    return siteConfig.banner.homepageHeight;
 24  }
 25  if (pathMatch(/\/posts\/.*/, path)) {
 26    return siteConfig.banner.postHeight;
 27  }
 28  for (const { pagePathRegex, height } of siteConfig.banner.pagesHeight) {
 29    if (pathMatch(pagePathRegex, path)) {
 30      return height;
 31    }
 32  }
 33  return siteConfig.banner.defaultHeight;
 34}
 35const path = Astro.url;
 36const bannerHeight = getBannerHeight(path.pathname);
 37---
 38
 39<div
 40  id="banner"
 41  class="relative max-h-screen overflow-hidden duration-1000"
 42  style={{
 43    height: bannerHeight,
 44    scale: '105%',
 45    'transition-property': 'height transform',
 46  }}
 47>
 48  <div class="h-full w-full">
 49    <div id="banner-text">
 50      {
 51        Astro.url.pathname === '/' && siteConfig.banner && siteConfig.banner.text !== null && (
 52          <h1 class="absolute z-10 grid h-full w-full place-items-center text-4xl font-bold text-shadow-md md:px-12">
 53            <span class="text-center">{siteConfig.banner.text || siteConfig.subtitle}</span>
 54          </h1>
 55        )
 56      }
 57    </div>
 58    {
 59      (() => {
 60        if (typeof src === 'string')
 61          return (
 62            <ImageWrapper
 63              id="banner-img"
 64              src={src}
 65              class="swup-transition-parallel-slide absolute! h-full w-full overflow-hidden"
 66              basePath={Astro.props.basePath}
 67            />
 68          );
 69        else
 70          return (
 71            <ImageWrapper
 72              id="banner-img-light"
 73              src={src.light}
 74              class="swup-transition-parallel-slide absolute! h-full w-full overflow-hidden dark:hidden"
 75            />
 76            <ImageWrapper
 77              id="banner-img-dark"
 78              src={src.dark}
 79              class="swup-transition-parallel-slide absolute! h-full w-full overflow-hidden not-dark:hidden"
 80            />
 81          );
 82      })()
 83    }
 84  </div>
 85  <div
 86    id="banner-mask"
 87    class="from-base-100 to-base-100/0 absolute bottom-0 grid h-3/5 min-h-48 w-full bg-linear-0"
 88  >
 89    <Icon
 90      name="material-symbols:keyboard-arrow-down-rounded"
 91      class="text-base-content/60 relative mx-auto animate-bounce place-self-end duration-300"
 92      width="4.5rem"
 93      height="4rem"
 94    />
 95  </div>
 96</div>
 97
 98<script>
 99  import { siteConfig } from '@/config';
100  import { pathMatch, pathsEqual, url } from '@utils/url-utils';
101
102  function getBannerHeight(path: string) {
103    if (siteConfig.banner === false) {
104      console.error('Banner is disabled. Should not show this error, must be a bug');
105      return null;
106    }
107    if (pathsEqual(url('/'), path)) {
108      return siteConfig.banner.homepageHeight;
109    }
110    if (pathMatch(/\/posts\/.*/, path)) {
111      return siteConfig.banner.postHeight;
112    }
113    for (const { pagePathRegex, height } of siteConfig.banner.pagesHeight) {
114      if (pathMatch(pagePathRegex, path)) {
115        return height;
116      }
117    }
118    return siteConfig.banner.defaultHeight;
119  }
120
121  function setupBanner() {
122    const banner = document.getElementById('banner');
123    banner!.style.scale = '100%';
124    banner!.style.height = `calc((${getBannerHeight(window.location.pathname) as string}) + 4rem)`;
125    const bannerMask = banner?.querySelector('#banner-mask');
126    const icon = bannerMask?.querySelector('svg');
127    if (pathsEqual(url('/'), window.location.pathname)) {
128      icon?.classList.remove('hidden');
129      icon?.classList.remove('opacity-0');
130      window.addEventListener('scroll', () => {
131        const bannerHeight = banner!.clientHeight;
132        if (window.scrollY > bannerHeight / 2) {
133          icon?.classList.add('opacity-0');
134        } else {
135          icon?.classList.remove('opacity-0');
136        }
137      });
138    } else {
139      icon?.classList.add('hidden');
140    }
141  }
142
143  document.addEventListener('astro:after-swap', setupBanner);
144  setupBanner();
145
146  function swupSetupBanner() {
147    // 为页面添加额外高度,避免页面切换时出现跳跃式滚动
148    window.swup?.hooks.before('visit:start', () => {
149      const heightExtend = document.getElementById('page-height-extend');
150      if (!heightExtend) {
151        console.error('Height extend not found');
152        return;
153      }
154      heightExtend.classList.remove('hidden');
155    });
156
157    window.swup?.hooks.before('content:replace', () => {
158      const heightExtend = document.getElementById('page-height-extend');
159      if (!heightExtend) {
160        console.error('Height extend not found');
161        return;
162      }
163      heightExtend.classList.remove('hidden');
164    });
165
166    window.swup?.hooks.on('visit:end', () => {
167      const heightExtend = document.getElementById('page-height-extend');
168      heightExtend?.classList.add('hidden');
169    });
170
171    // 处理 Banner 图片切换
172    window.swup?.hooks.before('content:insert', (_, { containers }) => {
173      for (const container of containers) {
174        // 支持单模式和双模式 Banner
175        const isBannerImg = container.selector === '#banner-img' ||
176                          container.selector === '#banner-img-light' ||
177                          container.selector === '#banner-img-dark';
178        if (!isBannerImg) continue;
179
180        const prevWrapper = container.previous;
181        const nextWrapper = container.next;
182        const prevImg = prevWrapper.querySelector('img') as HTMLImageElement;
183        const nextImg = nextWrapper.querySelector('img') as HTMLImageElement;
184        if (prevImg.src !== nextImg.src) continue;
185        prevWrapper.classList.add('hidden');
186        prevWrapper.classList.remove('swup-transition-parallel-slide');
187        nextWrapper.classList.remove('swup-transition-parallel-slide');
188      }
189    });
190  }
191
192  if (window.swup) {
193    swupSetupBanner();
194  } else {
195    document.addEventListener('swup:enable', swupSetupBanner);
196  }
197</script>