WelcomeToast 欢迎弹窗组件实现教程
点击按钮,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()
入口函数:判断是不是第一次访问,是的话才弹。
六、完整代码
组件文件
---// 空的 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 请求失败
- 页面加载时序问题
解决方案:
- 检查浏览器设置,确保 sessionStorage 可用
- 检查网络连接,确保能访问 IP API
- 检查控制台是否有错误信息
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 客户端路由 |
实现原理:
- 组件是纯客户端渲染,不输出静态 HTML
- 使用 sessionStorage 做会话级别的去重
- 通过 IP API 获取访客地理位置
- 支持 Swup 的 astro:page-load 事件
放在 Firefly 主题的 MainGridLayout 里,和 Swup 的 SPA 导航机制配合得很好。🎉
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!