[{"data":1,"prerenderedAt":221},["ShallowReactive",2],{"content:\u002Fposts\u002Fwelcome-toast":3,"surround:\u002Fposts\u002Fwelcome-toast":210},{"id":4,"title":5,"body":6,"categories":183,"date":185,"description":186,"draft":187,"extension":188,"image":189,"meta":190,"navigation":192,"path":193,"permalink":194,"pinned":187,"published":194,"readingTime":195,"recommend":194,"references":194,"seo":200,"sitemap":201,"stem":202,"tags":203,"type":207,"updated":208,"__hash__":209},"content\u002Fposts\u002Fposts\u002Fwelcome-toast.md","Firefly 魔改：IP 定位欢迎弹窗组件",{"type":7,"value":8,"toc":170},"minimark",[9,19,22,25,30,37,40,61,65,69,80,83,89,92,100,104,107,115,122,132,136,146,154,158,164],[10,11,15],"alert",{"title":12,"type":13,"icon":14},"AI 迁移提示","info","tabler:robot",[16,17,18],"p",{},"本文由 AI 协助从旧站迁移，尚未完成逐篇人工审校；内容如有疏漏，将在复核后修订。",[10,20],{"title":21,"type":13},"> 本文部分内容由 AI 辅助整理，已结合实际操作进行校验，请根据自身环境谨慎使用。",[16,23,24],{},"当访客第一次打开你的博客时，右下角弹出一个轻巧的欢迎提示，显示\"你好，来自 XX 的朋友\"，5 秒后自动消失——这种小细节能让博客更有人情味。",[26,27,29],"h2",{"id":28},"一功能概览","一、功能概览",[16,31,32],{},[33,34],"img",{"alt":35,"src":36},"欢迎弹框的效果","https:\u002F\u002Fimg.olinl.com\u002Ffile\u002Fpost-img\u002Fwelcome-toast\u002F0001.webp",[16,38,39],{},"核心流程：",[41,42,43,52,55,58],"ol",{},[44,45,46,47,51],"li",{},"检查 ",[48,49,50],"code",{"code":50},"sessionStorage"," 中是否已有访问标记",[44,53,54],{},"首次访问 → 调用 IP 定位 API 获取所在地",[44,56,57],{},"右下角弹出欢迎 Toast，5 秒后自动关闭",[44,59,60],{},"同一会话内不再重复弹出",[26,62,64],{"id":63},"二实现细节","二、实现细节",[66,67,68],"h3",{"id":68},"会话控制",[70,71,77],"pre",{"className":72,"code":74,"language":75,"meta":76},[73],"language-typescript","const VISIT_SESSION_KEY = \"blog_visit_flag\";\n\n\u002F\u002F 检查是否已弹过\nif (sessionStorage.getItem(VISIT_SESSION_KEY)) return;\n\n\u002F\u002F 显示弹窗后标记\nsessionStorage.setItem(VISIT_SESSION_KEY, \"true\");\n","typescript","title=\"src\u002Fcomponents\u002Fwidget\u002FWelcomeToast.astro\"",[48,78,74],{"__ignoreMap":79},"",[66,81,82],{"id":82},"获取访客位置",[70,84,87],{"className":85,"code":86,"language":75,"meta":76},[73],"fetch(\"https:\u002F\u002Fv2.xxapi.cn\u002Fapi\u002Fip\")\n  .then(res => res.json())\n  .then(data => {\n    const address = data.data?.address || \"\";\n    message.textContent = address\n      ? `你好，来自 ${address} 的朋友 👋`\n      : \"你好，欢迎来到我的博客 👋\";\n  })\n  .catch(() => {\n    message.textContent = \"你好，欢迎来到我的博客 👋\";\n  });\n",[48,88,86],{"__ignoreMap":79},[66,90,91],{"id":91},"动画与关闭",[70,93,98],{"className":94,"code":96,"language":97,"meta":76},[95],"language-js","\u002F\u002F 弹入动画\ntoast.classList.remove(\"translate-y-full\", \"opacity-0\");\n\n\u002F\u002F 5 秒后自动关闭\nsetTimeout(() => {\n  toast.classList.add(\"translate-y-full\", \"opacity-0\");\n}, 5000);\n\n\u002F\u002F 手动关闭\nwindow.__closeWelcomeToast = () => {\n  toast.classList.add(\"translate-y-full\", \"opacity-0\");\n};\n","js",[48,99,96],{"__ignoreMap":79},[26,101,103],{"id":102},"三核心代码","三、核心代码",[16,105,106],{},"组件本身是一个自执行的 Astro 脚本组件，直接查看文件内容：",[70,108,113],{"className":109,"code":111,"language":112,"meta":76},[110],"language-astro","---\n\u002F\u002F 欢迎提示组件 —— 首次访问时从右下角弹出，显示访客所在地\n---\n\n\u003Cscript>\n  const VISIT_SESSION_KEY = \"blog_visit_flag\";\n  let hasShownToast = false;\n\n  function createWelcomeToast(): HTMLElement { \u002F* ... DOM 构建 ... *\u002F }\n  function closeWelcomeToast(): void { \u002F* ... 关闭动画 ... *\u002F }\n\n  async function fetchLocation(): Promise\u003Cvoid> {\n    try {\n      const response = await fetch(\"https:\u002F\u002Fv2.xxapi.cn\u002Fapi\u002Fip\");\n      const data = await response.json();\n      if (data.code === 200 && data.data) {\n        const locationText = `你好，来自${data.data.address}的朋友`;\n        document.getElementById(\"welcome-message\")!.textContent = locationText;\n      }\n    } catch {\n      document.getElementById(\"welcome-message\")!.textContent = \"你好，欢迎来到我的博客\";\n    }\n  }\n\n  async function showWelcomeToast(): Promise\u003Cvoid> {\n    if (hasShownToast) return;\n    const toast = createWelcomeToast();\n    requestAnimationFrame(() => {\n      toast.classList.remove(\"translate-y-full\", \"opacity-0\");\n      toast.classList.add(\"translate-y-0\", \"opacity-100\");\n    });\n    hasShownToast = true;\n    await fetchLocation();\n    setTimeout(() => closeWelcomeToast(), 5000);\n  }\n\n  function initWelcome(): void {\n    if (!sessionStorage.getItem(VISIT_SESSION_KEY)) {\n      sessionStorage.setItem(VISIT_SESSION_KEY, \"true\");\n      showWelcomeToast();\n    }\n  }\n\n  window.__closeWelcomeToast = closeWelcomeToast;\n\n  if (document.readyState === \"loading\") {\n    document.addEventListener(\"DOMContentLoaded\", initWelcome);\n  } else {\n    initWelcome();\n  }\n\u003C\u002Fscript>\n","astro",[48,114,111],{"__ignoreMap":79},[16,116,117,118,121],{},"若要启用，在 ",[48,119,120],{"code":120},"src\u002Flayouts\u002FLayout.astro"," 中导入并挂载：",[70,123,130],{"className":124,"code":125,"highlights":126,"language":97,"meta":129},[95],"import FontSetup from \"@components\u002Ffeatures\u002FFontSetup.astro\";\nimport MusicManager from \"@components\u002Ffeatures\u002FMusicManager.astro\";\nimport SakuraEffect from \"@components\u002Ffeatures\u002FSakuraEffect.astro\";\nimport WelcomeToast from \"@components\u002Fwidget\u002FWelcomeToast.astro\";\nimport ConfigCarrier from \"@components\u002Flayout\u002FConfigCarrier.astro\";\n\u002F\u002F ...\n    \u003C!-- Sakura Effect -->\n    \u003CSakuraEffect \u002F>\n    \u003CWelcomeToast \u002F>\n    \u003C!-- Fancybox Manager -->\n",[127,128],4,9,"title=\"src\u002Flayouts\u002FLayout.astro\" ins=",[48,131,125],{"__ignoreMap":79},[26,133,135],{"id":134},"四相关文件","四、相关文件",[16,137,138,139],{},"组件：",[140,141,145],"a",{"href":142,"rel":143},"https:\u002F\u002Fgithub.com\u002Folinll\u002Ffirefly-blog\u002Fblob\u002Fmaster\u002Fsrc\u002Fcomponents\u002Fwidget\u002FWelcomeToast.astro",[144],"nofollow","\u002Fsrc\u002Fcomponents\u002Fwidget\u002FWelcomeToast.astro",[16,147,148,149],{},"相关源码：",[140,150,153],{"href":151,"rel":152},"https:\u002F\u002Fgithub.com\u002Folinll\u002Ffirefly-blog",[144],"olinll\u002Ffirefly-blog",[26,155,157],{"id":156},"最后","🔗 最后",[16,159,160,161,163],{},"通过 IP 定位让每位访客感受到被欢迎，同时 ",[48,162,50],{"code":50}," 保证了不打扰体验。如果你有更好的 IP 定位 API，替换请求地址即可。",[16,165,148,166],{},[140,167,169],{"href":151,"rel":168},[144],"Firefly 博客 - WelcomeToast.astro",{"title":79,"searchDepth":127,"depth":127,"links":171},[172,174,180,181,182],{"id":28,"depth":173,"text":29},2,{"id":63,"depth":173,"text":64,"children":175},[176,178,179],{"id":68,"depth":177,"text":68},3,{"id":82,"depth":177,"text":82},{"id":91,"depth":177,"text":91},{"id":102,"depth":173,"text":103},{"id":134,"depth":173,"text":135},{"id":156,"depth":173,"text":157},[184],"Firefly","2026-07-04 15:51:31","首次访问时右下角弹出欢迎提示，显示访客所在地，5 秒后自动关闭。",false,"md","https:\u002F\u002Fimg.olinl.com\u002Ffile\u002Fpost-img\u002Fwelcome-toast\u002Fcover.webp",{"slots":191},{},true,"\u002Fposts\u002Fwelcome-toast",null,{"text":196,"minutes":197,"time":198,"words":199},"3 min read",3.005,180300,601,{"title":5,"description":186},{"loc":193},"posts\u002Fposts\u002Fwelcome-toast",[184,204,205,206],"博客","二开","交互","tech","2026-07-22 19:30:00","hnmWs23oP704FLTbCKTXaS41MlCZqKXtmpak7Hi3g-o",[211,216],{"title":212,"path":213,"stem":214,"date":215,"type":207,"children":-1},"Firefly 魔改：GitHub 贡献热力图侧边栏","\u002Fposts\u002Fgithub-heatmap","posts\u002Fposts\u002Fgithub-heatmap","2026-07-04 13:10:29",{"title":217,"path":218,"stem":219,"date":220,"type":207,"children":-1},"Firefly 魔改：基于 Astro redirects 的短链接系统","\u002Fposts\u002Fshortlink","posts\u002Fposts\u002Fshortlink","2026-07-04 17:30:50",1788712222690]