WelcomeToast 欢迎弹窗组件实现教程

分钟
WelcomeToast 欢迎弹窗组件实现教程
AI 概括

点击按钮,AI 将为你生成这篇文章的摘要

一、功能介绍

WelcomeToast 是一个轻量级的欢迎弹窗组件,当访客第一次打开页面时,右下角会弹出一个欢迎卡片,支持 IP 地理位置的个性化问候,5 秒后自动消失。

功能特点

  • 首次访问弹出:使用 sessionStorage 去重,每个会话只弹一次
  • IP 地理位置问候:自动获取访客位置,显示个性化问候
  • 自动消失:5 秒后自动关闭,不打扰用户
  • 手动关闭:支持点击关闭按钮提前关闭
  • 响应式设计:适配桌面端和移动端
  • 深色模式支持:自动切换主题样式
  • Swup 兼容:完美适配 SPA 客户端路由

二、效果演示

场景效果
桌面端固定在右下角,显示欢迎卡片
移动端固定在底部居中,宽度 90%
深色模式自动切换深色主题
首次访问显示 IP 地理位置问候
重复访问不再弹出(会话内)

三、涉及的文件

整个组件涉及两个文件:

文件路径
组件本身src/components/WelcomeToast.astro
在布局中引用src/layouts/MainGridLayout.astro

四、组件实现原理

组件特点

组件的 frontmatter 是空的,没有任何服务端逻辑。所有事情都在浏览器端的 <script><style> 里完成。

页面结构

组件本身不输出任何 HTML,所有 DOM 都是 JS 动态生成的。结构大概是这样:

#welcome-toast(固定在右下角,z-50)
└── 一个卡片 div(白色背景、圆角、阴影)
├── 👋 表情
├── 文字区
│ ├── 问候语(一开始显示"正在加载...",后面会变成具体地址)
│ └── 副标题"欢迎来到我的博客"
└── 关闭按钮(一个 ×)

五、关键函数说明

整个逻辑不复杂,就是 5 个函数各管各的事:

1. 会话标识

// 用 sessionStorage 做会话标识,防止重复弹出
const VISIT_SESSION_KEY = 'blog_visit_flag';
let hasShownToast = false;

2. createWelcomeToast()

创建 Toast 的 DOM 结构,插入到页面上。

3. closeWelcomeToast()

关掉 Toast:先滑出去,500ms 后把节点从 DOM 里移掉。

4. fetchLocation()

请求一个免费的 IP API,拿到访客的位置信息。

5. showWelcomeToast()

整个弹出流程的”导演”:创建 → 滑入 → 拿位置 → 5秒后自动关闭。

6. initWelcome()

入口函数:判断是不是第一次访问,是的话才弹。


六、完整代码

组件文件

WelcomeToast.astro
---
// 空的 frontmatter,所有逻辑都在浏览器端
---
<script is:inline>
const VISIT_SESSION_KEY = 'blog_visit_flag';
let hasShownToast = false;
function createWelcomeToast() {
const toast = document.createElement('div');
toast.id = 'welcome-toast';
toast.className = 'welcome-toast';
toast.innerHTML = `
<div class="welcome-toast-card">
<div class="welcome-toast-emoji">👋</div>
<div class="welcome-toast-content">
<div class="welcome-toast-greeting">正在加载...</div>
<div class="welcome-toast-subtitle">欢迎来到我的博客</div>
</div>
<button class="welcome-toast-close" onclick="closeWelcomeToast()">×</button>
</div>
`;
document.body.appendChild(toast);
return toast;
}
function closeWelcomeToast() {
const toast = document.getElementById('welcome-toast');
if (toast) {
toast.classList.remove('show');
toast.classList.add('hide');
setTimeout(() => toast.remove(), 500);
}
}
async function fetchLocation() {
try {
const response = await fetch('https://ipapi.co/json/');
const data = await response.json();
return {
city: data.city,
region: data.region,
country: data.country_name
};
} catch (error) {
console.error('获取位置信息失败:', error);
return null;
}
}
async function showWelcomeToast() {
if (hasShownToast) return;
hasShownToast = true;
const toast = createWelcomeToast();
setTimeout(() => toast.classList.add('show'), 100);
const location = await fetchLocation();
const greeting = toast.querySelector('.welcome-toast-greeting');
if (location) {
greeting.textContent = `你好,来自${location.city || location.region || location.country}的朋友!`;
} else {
greeting.textContent = '你好,欢迎访问!';
}
setTimeout(closeWelcomeToast, 5000);
}
function initWelcome() {
if (sessionStorage.getItem(VISIT_SESSION_KEY)) return;
sessionStorage.setItem(VISIT_SESSION_KEY, '1');
showWelcomeToast();
}
// 兼容 Swup 的 SPA 导航
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initWelcome);
document.addEventListener('astro:page-load', initWelcome);
} else {
initWelcome();
}
// 挂载到 window 上供内联 onclick 调用
window.closeWelcomeToast = closeWelcomeToast;
</script>
<style>
.welcome-toast {
position: fixed;
bottom: 1rem;
right: 1rem;
z-index: 50;
transform: translateX(120%);
transition: transform 0.5s ease-out;
}
.welcome-toast.show {
transform: translateX(0);
}
.welcome-toast.hide {
transform: translateX(120%);
}
.welcome-toast-card {
background: var(--card-bg, #fff);
border: 1px solid var(--line-divider, rgba(0, 0, 0, 0.1));
border-radius: 12px;
padding: 1rem 1.25rem;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
gap: 0.75rem;
min-width: 280px;
max-width: 360px;
}
.welcome-toast-emoji {
font-size: 1.5rem;
flex-shrink: 0;
}
.welcome-toast-content {
flex: 1;
min-width: 0;
}
.welcome-toast-greeting {
font-size: 0.875rem;
font-weight: 600;
color: var(--text-primary, #1f2937);
line-height: 1.4;
}
.welcome-toast-subtitle {
font-size: 0.75rem;
color: var(--text-secondary, #6b7280);
margin-top: 0.25rem;
}
.welcome-toast-close {
background: none;
border: none;
font-size: 1.25rem;
color: var(--text-secondary, #6b7280);
cursor: pointer;
padding: 0.25rem;
line-height: 1;
flex-shrink: 0;
transition: color 0.2s;
}
.welcome-toast-close:hover {
color: var(--text-primary, #1f2937);
}
/* 深色模式 */
:root.dark .welcome-toast-card {
background: var(--card-bg, #1f2937);
border-color: var(--line-divider, rgba(255, 255, 255, 0.1));
}
:root.dark .welcome-toast-greeting {
color: var(--text-primary, #f9fafb);
}
:root.dark .welcome-toast-subtitle {
color: var(--text-secondary, #9ca3af);
}
:root.dark .welcome-toast-close {
color: var(--text-secondary, #9ca3af);
}
:root.dark .welcome-toast-close:hover {
color: var(--text-primary, #f9fafb);
}
/* 移动端适配 */
@media (max-width: 640px) {
.welcome-toast {
bottom: 1rem;
left: 50%;
right: auto;
transform: translateX(-50%) translateY(120%);
}
.welcome-toast.show {
transform: translateX(-50%) translateY(0);
}
.welcome-toast.hide {
transform: translateX(-50%) translateY(120%);
}
.welcome-toast-card {
width: 90vw;
max-width: none;
}
}
</style>

七、在布局中使用

导入组件

打开 src/layouts/MainGridLayout.astro,在文件顶部添加导入:

---
import WelcomeToast from "@components/WelcomeToast.astro";
// ... 其他导入
---

使用组件

<main id="swup-container"> 里面、<slot /> 后面添加:

<main id="swup-container" class="transition-main">
<div id="content-wrapper">
<slot></slot>
</div>
<WelcomeToast />
</main>

布局层级关系

Layout.astro
└── MainGridLayout.astro
├── Navbar(导航栏)
├── Wallpaper(背景壁纸)
├── <main id="swup-container"> ← Swup 的容器,导航时会替换这里面的内容
│ ├── <slot> ← 每个页面的具体内容
│ └── <WelcomeToast /> ← 放在内容后面
├── Footer(页脚)
└── FloatingControls(浮动控制按钮)

八、注意事项

1. SPA 导航适配

Firefly 使用了 Swup 做客户端路由,页面切换的时候不会走完整的 DOMContentLoaded。所以组件同时绑了两个事件:

if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initWelcome);
document.addEventListener('astro:page-load', initWelcome); // Swup 专用
} else {
initWelcome();
}

2. 内联 onclick 兼容

关闭按钮用的是内联 onclick,所以函数得挂到 window 上才能调用:

(window as any).closeWelcomeToast = closeWelcomeToast;

3. 会话去重

靠 sessionStorage 去重:每次 Swup 导航都会重建组件,但因为有 blog_visit_flag 标记,所以不会重复弹出。

4. 无 HTML 输出

构建时没有 HTML 输出:组件只有 <script><style>,不产生静态 HTML,全靠 JS 在浏览器里创建。

5. 无 Props 依赖

没传任何 props:组件完全是自包含的,不需要从外面传参数。


九、常见问题

Q1: 弹窗不显示?

可能原因:

  • 浏览器禁用了 sessionStorage
  • IP API 请求失败
  • 页面加载时序问题

解决方案:

  1. 检查浏览器设置,确保 sessionStorage 可用
  2. 检查网络连接,确保能访问 IP API
  3. 检查控制台是否有错误信息

Q2: 地理位置显示不准确?

可能原因:

  • IP API 返回的数据不准确
  • 访客使用了 VPN 或代理

解决方案: 这是正常现象,IP 定位本身就有一定误差,可以显示更宽泛的地区信息。

Q3: 每次刷新都显示?

可能原因:

  • sessionStorage 被清空
  • 浏览器隐私模式

解决方案: 这是正常行为,sessionStorage 在关闭浏览器后会清空。如果需要更持久的去重,可以使用 localStorage。

Q4: 如何修改样式?

解决方案: 修改组件中的 <style> 部分即可,主要样式包括:

.welcome-toast {
/* 容器样式 */
}
.welcome-toast-card {
/* 卡片样式 */
}
.welcome-toast-greeting {
/* 问候语文本样式 */
}

Q5: 如何禁用此功能?

解决方案:MainGridLayout.astro 中注释掉组件导入和使用:

{/* import WelcomeToast from "@components/WelcomeToast.astro"; */}
<main id="swup-container" class="transition-main">
<div id="content-wrapper">
<slot></slot>
</div>
{/* <WelcomeToast /> */}
</main>

十、总结

WelcomeToast 是一个轻量级的欢迎弹窗组件,主要特点:

特点说明
首次访问弹出使用 sessionStorage 去重
IP 地理位置问候自动获取访客位置
自动消失5 秒后自动关闭
手动关闭支持点击关闭按钮
响应式设计适配桌面端和移动端
深色模式支持自动切换主题样式
Swup 兼容完美适配 SPA 客户端路由

实现原理:

  1. 组件是纯客户端渲染,不输出静态 HTML
  2. 使用 sessionStorage 做会话级别的去重
  3. 通过 IP API 获取访客地理位置
  4. 支持 Swup 的 astro:page-load 事件

放在 Firefly 主题的 MainGridLayout 里,和 Swup 的 SPA 导航机制配合得很好。🎉

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!

赞助
WelcomeToast 欢迎弹窗组件实现教程
https://f3f3.top/posts/welcome-toast/
作者
lyf
发布于
2026-06-07
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
lyf
Hello, I'm LyF.
公告
欢迎来到一飞的博客!。
音乐
封面

音乐

暂未播放

0:00 0:00
暂无歌词
我和宝宝在一起已经
---------TSH ❤️ CXY---------
---------TSH
❤️
CXY---------
0 0 0 0 0 00
分类
标签
站点统计
文章
17
分类
5
标签
24
总字数
51,329
运行时长
0
最后活动
0 天前

文章目录

🤖 AI 助手

👋 你好!

我可以帮你解答关于这篇文章的问题