[{"data":1,"prerenderedAt":256},["ShallowReactive",2],{"content:\u002Fposts\u002Fcopy-toast":3,"surround:\u002Fposts\u002Fcopy-toast":246},{"id":4,"title":5,"body":6,"categories":219,"date":221,"description":222,"draft":223,"extension":224,"image":225,"meta":226,"navigation":228,"path":229,"permalink":230,"pinned":223,"published":230,"readingTime":231,"recommend":230,"references":230,"seo":236,"sitemap":237,"stem":238,"tags":239,"type":243,"updated":244,"__hash__":245},"content\u002Fposts\u002Fposts\u002Fcopy-toast.md","Firefly 魔改：全局复制事件监听与提示浮层",{"type":7,"value":8,"toc":208},"minimark",[9,19,22,25,30,37,58,62,67,78],[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],{},"当访客复制博客内容时，顶部自动弹出\"复制成功，转载请标注本文地址\"的提示浮层，并带有一个进度条动画——既友好地提醒了版权，又提供了优雅的视觉反馈。",[26,27,29],"h2",{"id":28},"一功能概览","一、功能概览",[16,31,32],{},[33,34],"img",{"alt":35,"src":36},"复制提示浮层弹出的效果截图","https:\u002F\u002Fimg.olinl.com\u002Ffile\u002Fpost-img\u002Fcopy-toast\u002F0001.webp",[38,39,40,49,52,55],"ul",{},[41,42,43,44,48],"li",{},"监听全局 ",[45,46,47],"code",{"code":47},"copy"," 事件",[41,50,51],{},"顶部滑入提示浮层，3 秒后自动消失",[41,53,54],{},"进度条从左到右动画，模拟倒计时",[41,56,57],{},"支持浅色\u002F深色模式",[26,59,61],{"id":60},"二实现细节","二、实现细节",[63,64,66],"h3",{"id":65},"svelte-组件","Svelte 组件",[68,69,75],"pre",{"className":70,"code":72,"language":73,"meta":74},[71],"language-svelte","\u003Cscript lang=\"ts\">\nimport { onMount } from \"svelte\";\n\nconst SHOW_DURATION = 3000;\nlet isVisible = $state(false);\nlet hideTimeout: ReturnType\u003Ctypeof setTimeout> | null = null;\n\nconst showCopyMessage = () => {\n  if (hideTimeout) {\n    clearTimeout(hideTimeout);\n    hideTimeout = null;\n  }\n  isVisible = false;\n  setTimeout(() => {\n    isVisible = true;\n    hideTimeout = setTimeout(() => {\n      isVisible = false;\n    }, SHOW_DURATION);\n  }, 10); \u002F\u002F 短暂延迟触发重排\n};\n\nconst handleCopy = () => {\n  const selection = window.getSelection();\n  if (selection?.toString().trim()) showCopyMessage();\n};\n\nonMount(() => {\n  document.addEventListener(\"copy\", handleCopy);\n  return () => {\n    document.removeEventListener(\"copy\", handleCopy);\n    if (hideTimeout) clearTimeout(hideTimeout);\n  };\n});\n\u003C\u002Fscript>\n\n\u003Cdiv class=\"copy-message\" class:show={isVisible}>\n  \u003Cdiv class=\"message-background\">\n    \u003Cdiv class=\"progress-bar\" class:active={isVisible}>\u003C\u002Fdiv>\n  \u003C\u002Fdiv>\n  \u003Cdiv class=\"message-content\">\n    \u003Cspan class=\"text\">✨️ 复制成功，转载请标注本文地址\u003C\u002Fspan>\n  \u003C\u002Fdiv>\n\u003C\u002Fdiv>\n\n\u003Cstyle>\n  .copy-message {\n    --duration: 3000ms;\n    --height: 50px;\n    --transition: transform 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);\n    --bg-gradient: linear-gradient(90deg, var(--primary) 0%, var(--primary) 25%, var(--primary) 50%, var(--primary) 75%, var(--primary) 100%);\n    position: fixed;\n    top: 0;\n    left: 0;\n    width: 100%;\n    height: var(--height);\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    z-index: 9999;\n    transform: translateY(-100%);\n    transition: var(--transition), opacity 0.4s ease;\n    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);\n    overflow: hidden;\n    opacity: 0;\n  }\n\n  .copy-message.show { transform: translateY(0); opacity: 1; }\n\n  .copy-message .message-background {\n    position: absolute;\n    inset: 0;\n    background: var(--bg-gradient);\n    z-index: 1;\n  }\n\n  .copy-message .progress-bar {\n    position: absolute;\n    top: 0;\n    left: -100%;\n    width: 100%;\n    height: 100%;\n    background: linear-gradient(\n      90deg,\n      rgba(255, 255, 255, 0) 0%,\n      rgba(255, 255, 255, 0.3) 50%,\n      rgba(255, 255, 255, 0) 100%\n    );\n    z-index: 2;\n    transform: translateX(0);\n    transition: transform 0ms;\n  }\n\n  .copy-message .progress-bar.active {\n    transform: translateX(200%);\n    transition: transform var(--duration) linear;\n  }\n\n  .copy-message .message-content {\n    display: flex;\n    align-items: center;\n    font-size: 16px;\n    font-weight: 600;\n    color: white;\n    position: relative;\n    z-index: 3;\n    text-shadow: 0 1px 2px rgba(0, 0, 0, 0.15);\n  }\n\n  @media (max-width: 768px) {\n    .copy-message { --height: 44px; }\n    .copy-message .message-content {\n      font-size: 14px;\n      padding: 0 16px;\n      text-align: center;\n    }\n  }\n\n  @media (prefers-color-scheme: dark) {\n    .copy-message {\n      --bg-gradient: linear-gradient(135deg, #1e40af 0%, #1e3a8a 100%);\n    }\n  }\n\u003C\u002Fstyle>\n","svelte","title=\"src\u002Fcomponents\u002Fcommon\u002FCopyMessage.svelte\"",[45,76,72],{"__ignoreMap":77},"",[10,79,82,93,99,105,114,119,126,131,141,145,160,171,175,185,193,201,205],{"title":80,"type":81},"重排技巧","tip",[16,83,84,85,88,89,92],{},"先设为 ",[45,86,87],{"code":87},"false"," 再延迟设为 ",[45,90,91],{"code":91},"true","，是为了触发浏览器重排让进度条动画能重新播放。",[16,94,95],{},[33,96],{"alt":97,"src":98},"进度条动画过程","https:\u002F\u002Fimg.olinl.com\u002Ffile\u002Fpost-img\u002Fcopy-toast\u002F0002.webp",[16,100,101,104],{},[45,102,103],{"code":103},"src\u002Fcomponents\u002Fcommon\u002FCopyMessage.svelte"," — showCopyMessage 函数变更：",[68,106,112],{"className":107,"code":108,"highlights":109,"language":73,"meta":111},[71],"  if (hideTimeout) clearTimeout(hideTimeout);\n  if (hideTimeout) {\n    clearTimeout(hideTimeout);\n    hideTimeout = null;\n  }\n",[110],1,"title=\"src\u002Fcomponents\u002Fcommon\u002FCopyMessage.svelte\" del= ins={2-5}",[45,113,108],{"__ignoreMap":77},[16,115,116,118],{},[45,117,103],{"code":103}," — onMount 清理变更：",[68,120,124],{"className":121,"code":122,"highlights":123,"language":73,"meta":111},[71],"  return () => document.removeEventListener(\"copy\", handleCopy);\n  return () => {\n    document.removeEventListener(\"copy\", handleCopy);\n    if (hideTimeout) clearTimeout(hideTimeout);\n  };\n",[110],[45,125,122],{"__ignoreMap":77},[16,127,128,130],{},[45,129,103],{"code":103}," — CSS 分层重构：",[68,132,139],{"className":133,"code":135,"highlights":136,"language":137,"meta":138},[134],"language-css","  .copy-message .progress-bar.active { transform: translateX(200%); transition: transform var(--duration) linear; }\n  .copy-message .message-background { position: absolute; inset: 0; background: var(--bg-gradient); z-index: 1; }\n  .copy-message .progress-bar { position: absolute; top: 0; left: -100%; width: 100%; height: 100%; background: linear-gradient(90deg, rgba(255,255,255,0) 0%, rgba(255,255,255,0.3) 50%, rgba(255,255,255,0) 100%); z-index: 2; transform: translateX(0); transition: transform 0ms; }\n  .copy-message .progress-bar.active { transform: translateX(200%); transition: transform var(--duration) linear; }\n  .copy-message .message-content { display: flex; align-items: center; font-size: 16px; font-weight: 600; color: white; position: relative; z-index: 3; text-shadow: 0 1px 2px rgba(0,0,0,0.15); }\n  @media (max-width: 768px) { .copy-message { --height: 44px; } .copy-message .message-content { font-size: 14px; padding: 0 16px; text-align: center; } }\n  @media (prefers-color-scheme: dark) { .copy-message { --bg-gradient: linear-gradient(135deg, #1e40af 0%, #1e3a8a 100%); } }\n",[110],"css","title=\"src\u002Fcomponents\u002Fcommon\u002FCopyMessage.svelte\" del= ins={2-10}",[45,140,135],{"__ignoreMap":77},[26,142,144],{"id":143},"三使用方式","三、使用方式",[16,146,147,148,151,152,155,156,159],{},"在 ",[45,149,150],{"code":150},"Layout.astro"," 中全局挂载（位于 ",[45,153,154],{"code":154},"\u003CMusicManager \u002F>"," 之后、",[45,157,158],{"code":158},"\u003Cslot \u002F>"," 之前）：",[68,161,169],{"className":162,"code":164,"highlights":165,"language":167,"meta":168},[163],"language-astro","\u003CConfigCarrier \u002F>\n\u003CMusicManager \u002F>\n\u003CCopyMessage client:load \u002F>\n\u003Cslot \u002F>\n",[166],3,"astro","title=\"src\u002Flayouts\u002FLayout.astro\" ins=",[45,170,164],{"__ignoreMap":77},[26,172,174],{"id":173},"四相关文件","四、相关文件",[16,176,177,178],{},"组件：",[179,180,184],"a",{"href":181,"rel":182},"https:\u002F\u002Fgithub.com\u002Folinll\u002Ffirefly-blog\u002Fblob\u002Fmaster\u002Fsrc\u002Fcomponents\u002Fcommon\u002FCopyMessage.svelte",[183],"nofollow","\u002Fsrc\u002Fcomponents\u002Fcommon\u002FCopyMessage.svelte",[16,186,187,188],{},"布局：",[179,189,192],{"href":190,"rel":191},"https:\u002F\u002Fgithub.com\u002Folinll\u002Ffirefly-blog\u002Fblob\u002Fmaster\u002Fsrc\u002Flayouts\u002FLayout.astro",[183],"\u002Fsrc\u002Flayouts\u002FLayout.astro",[16,194,195,196],{},"相关源码：",[179,197,200],{"href":198,"rel":199},"https:\u002F\u002Fgithub.com\u002Folinll\u002Ffirefly-blog",[183],"olinll\u002Ffirefly-blog",[26,202,204],{"id":203},"最后","🔗 最后",[16,206,207],{},"一个 Svelte 文件，监听一个事件，3 秒自动消失——实现简单但用户体验加分。想自定义提示文字，直接在源码中修改即可。",{"title":77,"searchDepth":209,"depth":209,"links":210},4,[211,213,216,217,218],{"id":28,"depth":212,"text":29},2,{"id":60,"depth":212,"text":61,"children":214},[215],{"id":65,"depth":166,"text":66},{"id":143,"depth":212,"text":144},{"id":173,"depth":212,"text":174},{"id":203,"depth":212,"text":204},[220],"Firefly","2026-07-21 19:41:32","全局监听复制事件，复制成功后底部弹出提示浮层，附带动画进度条。",false,"md","https:\u002F\u002Fimg.olinl.com\u002Ffile\u002Fpost-img\u002Fcopy-toast\u002Fcover.webp",{"slots":227},{},true,"\u002Fposts\u002Fcopy-toast",null,{"text":232,"minutes":233,"time":234,"words":235},"4 min read",3.805,228300,761,{"title":5,"description":222},{"loc":229},"posts\u002Fposts\u002Fcopy-toast",[220,240,241,242],"博客","二开","交互","tech","2026-07-22 19:30:00","EJt4xvFLNwig1TBAbBc4I8HV3OhAwREEBDkIinRwRBY",[247,252],{"title":248,"path":249,"stem":250,"date":251,"type":243,"children":-1},"Firefly 魔改：时间进度与节假日倒计时组件","\u002Fposts\u002Fschedule-progress","posts\u002Fposts\u002Fschedule-progress","2026-07-21 19:41:29",{"title":253,"path":254,"stem":255,"date":221,"type":243,"children":-1},"Firefly 魔改：自定义鼠标指针样式","\u002Fposts\u002Fcustom-cursor","posts\u002Fposts\u002Fcustom-cursor",1788712221314]