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>