[{"data":1,"prerenderedAt":1108},["ShallowReactive",2],{"post-/2026/20260314-1":3,"surround-/2026/20260314-1":1097},{"id":4,"title":5,"body":6,"categories":1070,"date":1072,"description":1073,"draft":1074,"extension":1075,"image":1076,"meta":1077,"navigation":1079,"path":1080,"permalink":1081,"podcastAudio":1082,"podcastLyric":1083,"podcastTitle":1084,"published":1081,"readingTime":1085,"recommend":1038,"references":1081,"seo":1090,"sitemap":1091,"stem":1092,"tags":1093,"type":1095,"updated":1072,"__hash__":1096},"content/posts/2026/20260314-1.md","侧边栏地区欢迎卡片教程",{"type":7,"value":8,"toc":1037},"minimark",[9,18,29,33,36,39,58,61,75,84,87,90,102,105,122,131,134,139,142,145,177,202,205,208,252,255,288,292,295,303,306,317,327,334,338,341,344,347,356,359,381,384,390,393,396,403,406,410,413,422,425,448,451,454,471,489,499,503,506,513,516,530,533,537,543,550,556,582,586,589,596,602,606,610,613,616,627,630,637,640,657,666,669,672,675,697,700,707,710,714,717,724,727,744,750,781,785,788,795,798,812,815,819,822,829,832,835,846,849,852,858,866,869,885,888,914,917,928,931,934,981,984,987,1009,1012,1015,1018,1021,1032],[10,11,14],"alert",{"type":12,"title":13},"info","撰写与发布说明",[15,16,17],"p",{},"本文由 ChatGPT 5.4 参与内容协同撰写与整理，经由 OpenClaw 自动化构建发布流程生成并发布。全文已通过火山大模型内容安全与合规性双重审核。",[10,19,22],{"type":20,"title":21},"warning","AI 内容告示",[15,23,24,25,28],{},"本文包含 AI 辅助生成与整理内容，示例代码与说明已经过人工校对，但仍可能因主题版本、接口返回格式或使用环境不同而产生差异。",[26,27],"br",{},"\n正式使用前，请务必结合自己的站点环境自行测试与调整。",[30,31,32],"h2",{"id":32},"前言",[15,34,35],{},"很多博客侧边栏都会放一句“欢迎来访”，但如果只是静态文字，其实很快就看腻了。",[15,37,38],{},"这次我们来做一个稍微有意思一点的版本：",[40,41,42,46,49,52,55],"ul",{},[43,44,45],"li",{},"顶部先放一组固定欢迎信息。",[43,47,48],{},"下方根据访客 IP 获取地区信息。",[43,50,51],{},"自动显示“你来自哪里”“离博主多远”。",[43,53,54],{},"再根据当前时间和地区，给出一句更有温度的问候语。",[43,56,57],{},"还可以点击展开，查看 IP、地址和运营商。",[15,59,60],{},"如果你喜欢那种“看起来像是在认真接待访客”的侧边栏，这种模块会很合适。",[10,62,64],{"type":12,"title":63},"这篇文章适合谁",[40,65,66,69,72],{},[43,67,68],{},"想给博客侧边栏加一点互动感的人。",[43,70,71],{},"想直接复制一份代码然后按自己需求改的人。",[43,73,74],{},"想顺手学一下原生 HTML、CSS、JavaScript 小模块写法的人。",[10,76,78],{"type":20,"title":77},"先说明一下",[15,79,80,81,83],{},"下面示例里的昵称、邮箱、API key 和坐标都已经换成了可公开发布的占位写法。",[26,82],{},"\n复制后只要把这几项改成你自己的信息，就可以直接使用。",[30,85,86],{"id":86},"最终能实现什么",[15,88,89],{},"写完以后，这个侧边栏模块会有三层体验：",[91,92,93,96,99],"ol",{},[43,94,95],{},"页面一加载，就先显示一组固定欢迎信息。",[43,97,98],{},"动态区域先转圈加载，再请求访客地区数据。",[43,100,101],{},"成功后渲染出欢迎卡片，失败时也不会整块空白。",[15,103,104],{},"卡片里会展示：",[40,106,107,110,113,116,119],{},[43,108,109],{},"来访地区",[43,111,112],{},"与博主的大致距离",[43,114,115],{},"当前时间段问候语",[43,117,118],{},"一句按地区生成的提示文案",[43,120,121],{},"可展开的 IP、地址、运营商详情",[123,124,126],"folding",{"title":125},"效果图",[127,128],"pic",{"caption":129,"src":130},"anheyu-app欢迎栏示例图","https://dev.jiugg.top/i/10b39e60-1565-4097-9d7a-ffa16ad1b76e.webp",[30,132,133],{"id":133},"安装方法",[135,136,138],"h3",{"id":137},"方法一安和鱼博客系统适配","方法一：安和鱼博客系统适配",[15,140,141],{},"如果你使用的是安和鱼（AnHeYu）博客系统，可以将本文提供的完整代码直接添加到后台侧边栏配置中。",[135,143,144],{"id":144},"配置步骤",[91,146,147,154,171,174],{},[43,148,149,150],{},"登录安和鱼博客系统后台，通常地址为 ",[151,152,153],"code",{"code":153},"你的域名/admin",[43,155,156,157,161,162,161,165,161,168],{},"进入 ",[158,159,160],"strong",{},"系统管理"," → ",[158,163,164],{},"系统设置",[158,166,167],{},"外观配置",[158,169,170],{},"侧边栏",[43,172,173],{},"将本文后面的“完整可复制版本”代码粘贴到侧边栏配置中",[43,175,176],{},"保存配置并刷新前台页面",[10,178,180],{"type":12,"title":179},"使用前记得先改这几项",[40,181,182,185,188,193],{},[43,183,184],{},"顶部昵称",[43,186,187],{},"邮箱链接",[43,189,190],{},[151,191,192],{"code":192},"API_KEY",[43,194,195,198,199],{},[151,196,197],{"code":197},"BLOG_LAT"," 和 ",[151,200,201],{"code":201},"BLOG_LNG",[30,203,204],{"id":204},"先看整体思路",[15,206,207],{},"这个模块其实并不复杂，本质上就是三部分拼起来：",[209,210,211,224],"table",{},[212,213,214],"thead",{},[215,216,217,221],"tr",{},[218,219,220],"th",{},"部分",[218,222,223],{},"作用",[225,226,227,236,244],"tbody",{},[215,228,229,233],{},[230,231,232],"td",{},"CSS",[230,234,235],{},"控制欢迎区、加载动画、按钮和详情面板样式",[215,237,238,241],{},[230,239,240],{},"HTML",[230,242,243],{},"放固定欢迎内容和动态挂载区域",[215,245,246,249],{},[230,247,248],{},"JavaScript",[230,250,251],{},"请求接口、生成问候语、计算距离、渲染卡片",[15,253,254],{},"你可以把它理解成一个“小型单文件组件”。",[256,257,258,261,264,267,270,273,276,279,282,285],"timeline",{},[15,259,260],{},"{页面加载}",[15,262,263],{},"先显示顶部固定欢迎内容，同时在动态区域放一个加载动画。",[15,265,266],{},"{请求数据}",[15,268,269],{},"向 IP 接口发起请求，拿到国家、省份、城市、经纬度和运营商。",[15,271,272],{},"{处理文案}",[15,274,275],{},"根据时间和地区，生成对应的欢迎语。",[15,277,278],{},"{渲染卡片}",[15,280,281],{},"把地区、距离、提示文案和详情按钮一起输出到页面。",[15,283,284],{},"{用户交互}",[15,286,287],{},"点击按钮后展开或收起来访详情。",[30,289,291],{"id":290},"第一步写静态欢迎区","第一步：写静态欢迎区",[15,293,294],{},"侧边栏里最好先有一块不依赖接口的内容。这样做有两个好处：",[40,296,297,300],{},[43,298,299],{},"页面刚打开时不会显得空。",[43,301,302],{},"即使接口请求失败，顶部区域也还能正常显示。",[15,304,305],{},"先写这样一段 HTML：",[307,308,315],"pre",{"className":309,"code":311,"filename":312,"language":313,"meta":314},[310],"language-html","\u003Cdiv class=\"welcome-section\">\n  \u003Cdiv class=\"section-title\">\n    \u003Cspan class=\"icon\">👤\u003C/span>\n    \u003Cspan>欢迎来访者\u003C/span>\n  \u003C/div>\n\n  \u003Cdiv class=\"welcome-list\">\n    \u003Cdiv class=\"welcome-list-item\">\n      \u003Cspan class=\"emoji\">👋🏻\u003C/span>\n      \u003Cspan>Hi，我是站长，欢迎你！\u003C/span>\n    \u003C/div>\n    \u003Cdiv class=\"welcome-list-item\">\n      \u003Cspan class=\"emoji\">❓\u003C/span>\n      \u003Cspan>如有问题欢迎评论区交流！\u003C/span>\n    \u003C/div>\n    \u003Cdiv class=\"welcome-list-item\">\n      \u003Cspan class=\"emoji\">😫\u003C/span>\n      \u003Cspan>页面异常？试试 \u003Cspan class=\"kbd\">Ctrl\u003C/span>+\u003Cspan class=\"kbd\">F5\u003C/span>\u003C/span>\n    \u003C/div>\n    \u003Cdiv class=\"welcome-list-item\">\n      \u003Cspan class=\"emoji\">📧\u003C/span>\n      \u003Cspan>联系我：\u003Ca href=\"mailto:your-email@example.com\">发送邮件 🚀\u003C/a>\u003C/span>\n    \u003C/div>\n  \u003C/div>\n\u003C/div>\n\n\u003Cdiv class=\"section-divider\">\u003C/div>\n\n\u003Cdiv id=\"welcome-info\">\n  \u003Cdiv class=\"loading-spinner\">\u003C/div>\n\u003C/div>\n","静态欢迎区.html","html","",[151,316,311],{"__ignoreMap":314},[15,318,319,320,323,324,326],{},"这里最关键的是 ",[151,321,322],{"code":322},"#welcome-info","。",[26,325],{},"\n它就是后面 JavaScript 动态渲染内容的挂载点。",[328,329,331,333],"tip",{"tip":330},"请求成功后，脚本会把欢迎卡片整块塞进这里。",[151,332,322],{"code":322}," 可以理解成一个“预留出来的插槽”。",[30,335,337],{"id":336},"第二步把基础样式铺好","第二步：把基础样式铺好",[15,339,340],{},"这个模块的样式重点不是炫技，而是“看起来清楚、舒服、有层次”。",[135,342,343],{"id":343},"欢迎区样式",[15,345,346],{},"欢迎区负责展示顶部固定内容，所以样式要轻一点：",[307,348,354],{"className":349,"code":351,"filename":352,"language":353,"meta":314},[350],"language-css",".welcome-section {\n  margin-bottom: 16px;\n}\n\n.welcome-section .section-title {\n  display: flex;\n  align-items: center;\n  gap: 8px;\n  font-size: 15px;\n  font-weight: 600;\n  color: var(--anzhiyu-fontcolor);\n  margin-bottom: 12px;\n}\n\n.welcome-list {\n  display: flex;\n  flex-direction: column;\n  gap: 2px;\n}\n\n.welcome-list-item {\n  display: flex;\n  align-items: center;\n  gap: 10px;\n  font-size: 13px;\n  color: var(--anzhiyu-fontcolor);\n  line-height: 1.5;\n}\n","欢迎区样式.css","css",[151,355,351],{"__ignoreMap":314},[15,357,358],{},"这里用了主题变量：",[40,360,361,366,371,376],{},[43,362,363],{},[151,364,365],{"code":365},"--anzhiyu-fontcolor",[43,367,368],{},[151,369,370],{"code":370},"--anzhiyu-theme",[43,372,373],{},[151,374,375],{"code":375},"--anzhiyu-secondbg",[43,377,378],{},[151,379,380],{"code":380},"--anzhiyu-card-border",[15,382,383],{},"如果你不是在同一套主题里使用，就需要自己替换成固定颜色或你自己的变量。",[10,385,387],{"type":20,"title":386},"移植时最容易忽略的一点",[15,388,389],{},"很多人复制代码后，发现结构正常但颜色全不对，原因通常不是 HTML 或 JS 出错，而是主题变量对不上。",[135,391,392],{"id":392},"加载动画和卡片样式",[15,394,395],{},"动态区域里最先出现的是转圈动画，接口回来后才会被真正内容替换：",[307,397,401],{"className":398,"code":399,"filename":400,"language":353,"meta":314},[350],"#welcome-info {\n  user-select: none;\n}\n\n.loading-spinner {\n  width: 32px;\n  height: 32px;\n  margin: 30px auto;\n  border: 3px solid var(--anzhiyu-secondbg);\n  border-top-color: var(--anzhiyu-theme);\n  border-radius: 50%;\n  animation: spin 0.8s linear infinite;\n}\n\n@keyframes spin {\n  to {\n    transform: rotate(360deg);\n  }\n}\n\n.detail-panel {\n  background: var(--anzhiyu-secondbg);\n  border-radius: 8px;\n  padding: 10px 12px;\n  margin-bottom: 10px;\n}\n","动态区域样式.css",[151,402,399],{"__ignoreMap":314},[15,404,405],{},"按钮、详情面板、提示文字，也都是围绕“侧边栏小卡片”这个定位来写的，所以尺寸控制得比较克制。",[30,407,409],{"id":408},"第三步准备配置项","第三步：准备配置项",[15,411,412],{},"脚本部分先不要急着写逻辑，第一步先把配置项列出来：",[307,414,420],{"className":415,"code":417,"filename":418,"language":419,"meta":314},[416],"language-js","const CONFIG = {\n  API_KEY: 'YOUR_API_KEY',\n  API_URL: 'https://v1.nsuuu.com/api/ipip',\n  BLOG_LAT: 39.9042,\n  BLOG_LNG: 116.4074\n};\n","配置项.js","js",[151,421,417],{"__ignoreMap":314},[15,423,424],{},"每一项分别代表：",[40,426,427,432,438,443],{},[43,428,429,431],{},[151,430,192],{"code":192},"：IP 接口的访问密钥",[43,433,434,437],{},[151,435,436],{"code":436},"API_URL","：IP 查询接口地址",[43,439,440,442],{},[151,441,197],{"code":197},"：博主所在地纬度",[43,444,445,447],{},[151,446,201],{"code":201},"：博主所在地经度",[15,449,450],{},"其中前两项决定“能不能查到访客数据”，后两项决定“能不能算出和博主的距离”。",[15,452,453],{},"这四项里：",[40,455,456,461,468],{},[43,457,458,460],{},[151,459,192],{"code":192}," 一定要换成你自己的",[43,462,463,198,465,467],{},[151,464,197],{"code":197},[151,466,201],{"code":201}," 最好改成你自己的坐标",[43,469,470],{},"顶部欢迎区昵称和邮箱也建议一起改掉",[10,472,474],{"type":12,"title":473},"API 密钥获取说明",[15,475,476,477,479,481,482,488],{},"本文示例使用的是鸭梨 API。",[26,478],{},[151,480,192],{"code":192}," 可前往 ",[483,484,485],"a",{"href":485,"rel":486},"https://api.nsuuu.com/",[487],"nofollow"," 申请，申请完成后填入配置项即可使用。",[10,490,493],{"type":491,"title":492},"question","为什么这里还要写博主坐标？",[15,494,495,496,498],{},"因为卡片里会显示“你目前距博主约 xx 公里”。",[26,497],{},"\n这个距离不是接口直接给的，而是用访客经纬度和你的坐标算出来的。",[30,500,502],{"id":501},"第四步先处理状态显示","第四步：先处理状态显示",[15,504,505],{},"在写主逻辑之前，最好先把“加载中”和“错误态”准备好。",[307,507,511],{"className":508,"code":509,"filename":510,"language":419,"meta":314},[416],"const welcomeEl = document.getElementById('welcome-info');\nif (!welcomeEl) return;\n\nconst showLoading = () => {\n  welcomeEl.innerHTML = '\u003Cdiv class=\"loading-spinner\">\u003C/div>';\n};\n\nconst showError = (msg) => {\n  welcomeEl.innerHTML = `\n    \u003Cdiv class=\"error-message\">\n      \u003Cdiv class=\"error-icon\">😕\u003C/div>\n      \u003Cp>${msg}\u003C/p>\n      \u003Cp>点击 \u003Ci id=\"retry-button\">↻ 重试\u003C/i>\u003C/p>\n    \u003C/div>\n  `;\n\n  document.getElementById('retry-button')?.addEventListener('click', () => {\n    showLoading();\n    fetchAndShow();\n  });\n};\n","状态函数.js",[151,512,509],{"__ignoreMap":314},[15,514,515],{},"这两个函数的意义很简单：",[40,517,518,524],{},[43,519,520,523],{},[151,521,522],{"code":522},"showLoading()","：请求时显示转圈动画",[43,525,526,529],{},[151,527,528],{"code":528},"showError()","：请求失败时显示错误提示，并提供重试入口",[15,531,532],{},"虽然当前实现里失败后默认回退成普通欢迎卡片，但把错误函数先写好，是个很好的习惯。",[30,534,536],{"id":535},"第五步计算访客与博主的距离","第五步：计算访客与博主的距离",[15,538,539,540,542],{},"如果只显示地区，互动感还是差一点。",[26,541],{},"\n加上“你目前距博主约 xx 公里”之后，这块内容就会立刻生动很多。",[307,544,548],{"className":545,"code":546,"filename":547,"language":419,"meta":314},[416],"const calcDistance = (lat, lng) => {\n  if (!lat || !lng || isNaN(lat) || isNaN(lng)) return '未知';\n\n  const R = 6371;\n  const dLat = (lat - CONFIG.BLOG_LAT) * Math.PI / 180;\n  const dLng = (lng - CONFIG.BLOG_LNG) * Math.PI / 180;\n\n  const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +\n    Math.cos(CONFIG.BLOG_LAT * Math.PI / 180) * Math.cos(lat * Math.PI / 180) *\n    Math.sin(dLng / 2) * Math.sin(dLng / 2);\n\n  const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));\n  return Math.round(R * c);\n};\n","距离计算.js",[151,549,546],{"__ignoreMap":314},[15,551,552,553,555],{},"这里用的是常见的地球球面距离算法。",[26,554],{},"\n对侧边栏这种场景来说，四舍五入到公里已经完全够用了。",[123,557,559,565,572,575],{"title":558},"这里有一个容易忽略的小坑",[15,560,561,562,323],{},"当前判断写的是 ",[151,563,564],{"code":564},"if (!lat || !lng)",[15,566,567,568,571],{},"这意味着如果经纬度刚好为 ",[151,569,570],{"code":570},"0","，也会被当成无效值。",[15,573,574],{},"更稳一点可以改成：",[307,576,580],{"className":577,"code":578,"filename":579,"language":419,"meta":314},[416],"const calcDistance = (lat, lng) => {\n  if (lat == null || lng == null) return '未知';\n  if (Number.isNaN(lat) || Number.isNaN(lng)) return '未知';\n  // 下面保留原来的计算逻辑\n};\n","更稳的写法.js",[151,581,578],{"__ignoreMap":314},[30,583,585],{"id":584},"第六步给不同时间段配上问候语","第六步：给不同时间段配上问候语",[15,587,588],{},"一个纯数据卡片会很死，加入时间问候之后，立刻就会有“它在和人说话”的感觉。",[307,590,594],{"className":591,"code":592,"filename":593,"language":419,"meta":314},[416],"const getTimeGreeting = () => {\n  const h = new Date().getHours();\n  if (h \u003C 6) return \"凌晨好🌙，夜深了注意休息~\";\n  if (h \u003C 9) return \"早上好🌤️，一日之计在于晨\";\n  if (h \u003C 12) return \"上午好☀️，工作顺利~\";\n  if (h \u003C 14) return \"中午好🍱，记得午休喔~\";\n  if (h \u003C 17) return \"下午好🕞，饮茶先啦！\";\n  if (h \u003C 19) return \"傍晚好🌇，记得按时吃饭~\";\n  if (h \u003C 22) return \"晚上好🌙，夜生活嗨起来！\";\n  return \"夜深了🌛，早点休息吧~\";\n};\n","时间问候语.js",[151,595,592],{"__ignoreMap":314},[15,597,598,599,601],{},"这段逻辑非常简单，但很实用。",[26,600],{},"\n而且你完全可以按自己的博客风格去改这些文案。",[328,603,605],{"tip":604},"不是服务器时间，也不是通过地区反推的标准时区时间。","这里读取的是访客浏览器本地时间。",[30,607,609],{"id":608},"第七步按地区生成欢迎文案","第七步：按地区生成欢迎文案",[15,611,612],{},"这一段是整个模块最有辨识度的地方。",[15,614,615],{},"比起冷冰冰地显示“你来自广东 广州”，更自然的做法是：",[40,617,618,621,624],{},[43,619,620],{},"日本访客看到“よろしく，一起去看樱花吗”",[43,622,623],{},"广州访客看到“看小蛮腰，喝早茶了嘛~”",[43,625,626],{},"成都访客看到“宽窄巷子，成都慢生活。”",[15,628,629],{},"实现函数如下：",[307,631,635],{"className":632,"code":633,"filename":634,"language":419,"meta":314},[416],"const getRegionGreeting = (country, province, city) => {\n  if (!country) return \"神秘的访客，欢迎来到我的小站~\";\n\n  const foreignGreetings = {\n    \"日本\": \"よろしく，一起去看樱花吗\",\n    \"美国\": \"Let us live in peace!\",\n    \"英国\": \"想同你一起夜乘伦敦眼\"\n  };\n\n  if (foreignGreetings[country]) return foreignGreetings[country];\n  if (country !== \"中国\") return \"带我去你的国家逛逛吧~\";\n\n  const directGreetings = {\n    \"北京\": \"北——京——欢迎你~~~\",\n    \"上海\": \"走在外滩，感受历史与现代的交融。\",\n    \"重庆\": \"山城重庆，火锅与夜景都很美！\"\n  };\n\n  if (directGreetings[province]) return directGreetings[province];\n\n  // 下面继续做省份和城市匹配\n};\n","地区问候语逻辑.js",[151,636,633],{"__ignoreMap":314},[15,638,639],{},"完整版本里，你可以继续维护一张省份和城市映射表：",[40,641,642,645,648,654],{},[43,643,644],{},"先按省份命中",[43,646,647],{},"再按城市命中",[43,649,650,651],{},"如果城市没命中，就退回省级 ",[151,652,653],{"code":653},"_default",[43,655,656],{},"最后再给一个统一兜底",[10,658,660],{"type":12,"title":659},"这部分最值得你花心思",[15,661,662,663,665],{},"这个模块的体验感，几乎一半都来自这里。",[26,664],{},"\n技术上它并不复杂，但文案写得好不好，会直接影响成品质感。",[135,667,668],{"id":668},"为什么还要处理省份和城市后缀",[15,670,671],{},"接口返回值不一定统一。",[15,673,674],{},"比如同一个地区，可能返回：",[40,676,677,682,687,692],{},[43,678,679],{},[151,680,681],{"code":681},"广东",[43,683,684],{},[151,685,686],{"code":686},"广东省",[43,688,689],{},[151,690,691],{"code":691},"南京",[43,693,694],{},[151,695,696],{"code":696},"南京市",[15,698,699],{},"所以在做匹配前，最好先标准化一下：",[307,701,705],{"className":702,"code":703,"filename":704,"language":419,"meta":314},[416],"let prov = province;\nif (prov) {\n  prov = prov.replace(/省$|自治区$|回族自治区$|维吾尔自治区$|壮族自治区$/g, '');\n}\n\nif (city) {\n  const cityName = city.replace(/市$/, '');\n}\n","省市标准化.js",[151,706,703],{"__ignoreMap":314},[15,708,709],{},"这一步不花哨，但非常必要。",[30,711,713],{"id":712},"第八步把卡片真正渲染出来","第八步：把卡片真正渲染出来",[15,715,716],{},"前面的函数都只是“准备材料”，真正把内容显示到侧边栏上的，是渲染函数。",[307,718,722],{"className":719,"code":720,"filename":721,"language":419,"meta":314},[416],"const showWelcome = (data) => {\n  const { country, province, city, ip, latitude, longitude, isp } = data || {};\n\n  let location = '神秘地区';\n  if (country) {\n    if (country === '中国') {\n      location = [province, city].filter(Boolean).join(' ') || '中国';\n    } else {\n      location = country;\n    }\n  }\n\n  const dist = calcDistance(parseFloat(latitude), parseFloat(longitude));\n  const address = [country, province, city].filter(Boolean).join(' · ') || '--';\n  const ipDisplay = (ip || '未知').replace(/\u003C/g, '&lt;').replace(/>/g, '&gt;');\n\n  welcomeEl.innerHTML = `\n    \u003Cdiv class=\"welcome-card\">\n      \u003Cp class=\"greeting\">欢迎你~ \u003Cbr>来自 \u003Cspan class=\"highlight\">${location}\u003C/span> 的小伙伴，你好呀!\u003C/p>\n      \u003Cp class=\"greeting\">你目前距博主约 \u003Cspan class=\"highlight\">${dist} 公里\u003C/span>\u003C/p>\n      \u003Cp class=\"greeting\">${getTimeGreeting()}\u003C/p>\n      \u003Cp class=\"greeting greeting-tip\">Tip：\u003Cspan class=\"highlight\">${getRegionGreeting(country, province, city)}\u003C/span> 🍂\u003C/p>\n      \u003Cdiv class=\"detail-expand\">\n        \u003Cdiv class=\"expand-content\" style=\"display: none;\">\n          \u003Cdiv class=\"detail-panel\">\n            \u003Cdiv class=\"detail-row\">\u003Cspan class=\"label\">IP\u003C/span>\u003Cspan class=\"value\">${ipDisplay}\u003C/span>\u003C/div>\n            \u003Cdiv class=\"detail-row\">\u003Cspan class=\"label\">地址\u003C/span>\u003Cspan class=\"value\">${address}\u003C/span>\u003C/div>\n            \u003Cdiv class=\"detail-row\">\u003Cspan class=\"label\">运营商\u003C/span>\u003Cspan class=\"value\">${isp || '未知'}\u003C/span>\u003C/div>\n          \u003C/div>\n        \u003C/div>\n        \u003Cbutton type=\"button\" class=\"toggle-btn\">\n          \u003Cspan class=\"toggle-icon\">▼\u003C/span>\n          \u003Cspan class=\"toggle-text\">展开来访详情\u003C/span>\n        \u003C/button>\n      \u003C/div>\n    \u003C/div>\n  `;\n};\n","欢迎卡片渲染.js",[151,723,720],{"__ignoreMap":314},[15,725,726],{},"这一段主要完成了几件事：",[40,728,729,732,735,738,741],{},[43,730,731],{},"处理地区显示文字",[43,733,734],{},"计算距离",[43,736,737],{},"拼接地址",[43,739,740],{},"转义 IP 文本",[43,742,743],{},"组合成一整块 HTML 输出到页面",[15,745,746,749],{},[151,747,748],{"code":748},"ipDisplay"," 这一行要保留，因为它做了最基础的 HTML 转义。",[123,751,753],{"title":752},"渲染结果里实际有哪些内容",[40,754,755,758,760,763,766,769,772,775,778],{},[43,756,757],{},"一句欢迎语",[43,759,109],{},[43,761,762],{},"与博主距离",[43,764,765],{},"当前时间问候",[43,767,768],{},"地区提示文案",[43,770,771],{},"可展开详情面板",[43,773,774],{},"IP",[43,776,777],{},"地址",[43,779,780],{},"运营商",[30,782,784],{"id":783},"第九步实现展开和收起详情","第九步：实现展开和收起详情",[15,786,787],{},"卡片渲染出来之后，还要加上按钮交互。",[307,789,793],{"className":790,"code":791,"filename":792,"language":419,"meta":314},[416],"const btn = welcomeEl.querySelector('.toggle-btn');\nconst content = welcomeEl.querySelector('.expand-content');\n\nif (btn && content) {\n  btn.addEventListener('click', () => {\n    const isOpen = content.style.display !== 'none';\n    content.style.display = isOpen ? 'none' : 'block';\n    btn.classList.toggle('expanded', !isOpen);\n    btn.querySelector('.toggle-text').textContent = isOpen ? '展开来访详情' : '收起来访详情';\n  });\n}\n","展开详情按钮.js",[151,794,791],{"__ignoreMap":314},[15,796,797],{},"这里的逻辑非常直接：",[40,799,800,803,806,809],{},[43,801,802],{},"默认详情区域隐藏",[43,804,805],{},"点击按钮后切换显示状态",[43,807,808],{},"按钮图标跟着旋转",[43,810,811],{},"按钮文字也跟着改",[15,813,814],{},"这类交互最适合侧边栏，因为它能把信息收起来，不会让小区域显得太拥挤。",[30,816,818],{"id":817},"第十步请求接口并启动模块","第十步：请求接口并启动模块",[15,820,821],{},"最后一步，就是把前面所有函数串起来。",[307,823,827],{"className":824,"code":825,"filename":826,"language":419,"meta":314},[416],"const fetchAndShow = async () => {\n  try {\n    const response = await fetch(CONFIG.API_URL, {\n      method: 'GET',\n      headers: {\n        'key': CONFIG.API_KEY\n      }\n    });\n\n    if (!response.ok) {\n      throw new Error(`HTTP ${response.status}`);\n    }\n\n    const result = await response.json();\n\n    if (result.code === 200 && result.data) {\n      showWelcome(result.data);\n    } else {\n      throw new Error(result.message || '数据格式错误');\n    }\n  } catch (err) {\n    console.error('获取IP信息失败:', err);\n    showWelcome({});\n  }\n};\n\nfetchAndShow();\n","请求与启动.js",[151,828,825],{"__ignoreMap":314},[15,830,831],{},"这一步就是整个模块真正的入口。",[15,833,834],{},"页面加载后：",[91,836,837,840,843],{},[43,838,839],{},"请求接口",[43,841,842],{},"成功就渲染真实欢迎卡片",[43,844,845],{},"失败就渲染默认欢迎卡片",[15,847,848],{},"所以就算接口偶尔抽风，侧边栏也不会直接空掉。",[30,850,851],{"id":851},"完整可复制版本",[15,853,854,855,857],{},"如果你不想自己把前面的片段重新拼一遍，可以直接用下面这份完整代码。",[26,856],{},"\n这份是可以直接粘贴使用的版本，只要改掉里面的占位信息就行。",[307,859,864],{"className":860,"code":861,"filename":862,"language":313,"meta":863},[310],"\u003Cstyle>\n  .welcome-section {\n    margin-bottom: 16px;\n  }\n\n  .welcome-section .section-title {\n    display: flex;\n    align-items: center;\n    gap: 8px;\n    font-size: 15px;\n    font-weight: 600;\n    color: var(--anzhiyu-fontcolor);\n    margin-bottom: 12px;\n  }\n\n  .welcome-section .section-title .icon {\n    font-size: 1.2rem;\n  }\n\n  .welcome-list {\n    display: flex;\n    flex-direction: column;\n    gap: 2px;\n  }\n\n  .welcome-list-item {\n    display: flex;\n    align-items: center;\n    gap: 10px;\n    font-size: 13px;\n    color: var(--anzhiyu-fontcolor);\n    line-height: 1.5;\n  }\n\n  .welcome-list-item .emoji {\n    font-size: 1rem;\n    width: 24px;\n    text-align: center;\n    flex-shrink: 0;\n  }\n\n  .welcome-list-item a {\n    color: var(--anzhiyu-theme);\n    text-decoration: none;\n  }\n\n  .welcome-list-item a:hover {\n    text-decoration: underline;\n  }\n\n  .kbd {\n    display: inline-block;\n    padding: 2px 6px;\n    font-size: 11px;\n    font-family: monospace;\n    color: var(--anzhiyu-fontcolor);\n    background: var(--anzhiyu-secondbg);\n    border: 1px solid var(--anzhiyu-card-border);\n    border-radius: 4px;\n    box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);\n    margin: 0 2px;\n  }\n\n  .section-divider {\n    height: 1px;\n    background: linear-gradient(90deg, transparent, var(--anzhiyu-card-border), transparent);\n    margin: 8px 0;\n  }\n\n  #welcome-info {\n    user-select: none;\n  }\n\n  .loading-spinner {\n    width: 32px;\n    height: 32px;\n    margin: 30px auto;\n    border: 3px solid var(--anzhiyu-secondbg);\n    border-top-color: var(--anzhiyu-theme);\n    border-radius: 50%;\n    animation: spin 0.8s linear infinite;\n  }\n\n  @keyframes spin {\n    to {\n      transform: rotate(360deg);\n    }\n  }\n\n  .welcome-card .greeting {\n    margin: 2px 0;\n    font-size: 13px;\n    line-height: 1.6;\n    color: var(--anzhiyu-fontcolor);\n  }\n\n  .welcome-card .greeting .highlight {\n    color: var(--anzhiyu-theme);\n    font-weight: 600;\n  }\n\n  .welcome-card .greeting.greeting-tip {\n    font-size: 12px;\n    color: var(--anzhiyu-secondtext);\n    padding-top: 10px;\n    margin-top: 10px;\n    border-top: 1px dashed var(--anzhiyu-card-border);\n  }\n\n  .detail-expand {\n    margin-top: 12px;\n  }\n\n  .expand-content {\n    overflow: hidden;\n  }\n\n  .detail-panel {\n    background: var(--anzhiyu-secondbg);\n    border-radius: 8px;\n    padding: 10px 12px;\n    margin-bottom: 10px;\n  }\n\n  .detail-row {\n    display: flex;\n    align-items: center;\n    padding: 6px 0;\n    font-size: 12px;\n    border-bottom: 1px dashed transparent;\n  }\n\n  .detail-row:not(:last-child) {\n    border-bottom-color: var(--anzhiyu-card-border);\n  }\n\n  .detail-row .label {\n    color: var(--anzhiyu-secondtext);\n    width: 50px;\n    flex-shrink: 0;\n  }\n\n  .detail-row .value {\n    color: var(--anzhiyu-fontcolor);\n    flex: 1;\n  }\n\n  .toggle-btn {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    gap: 6px;\n    width: 100%;\n    padding: 8px;\n    background: var(--anzhiyu-secondbg);\n    border: none;\n    border-radius: 6px;\n    color: var(--anzhiyu-theme);\n    font-size: 12px;\n    cursor: pointer;\n    transition: all 0.2s;\n  }\n\n  .toggle-btn:hover {\n    background: var(--anzhiyu-theme-op);\n  }\n\n  .toggle-btn .toggle-icon {\n    transition: transform 0.2s;\n  }\n\n  .toggle-btn.expanded .toggle-icon {\n    transform: rotate(180deg);\n  }\n\n  .error-message {\n    text-align: center;\n    padding: 20px 0;\n  }\n\n  .error-message .error-icon {\n    font-size: 2.5rem;\n    margin-bottom: 10px;\n  }\n\n  .error-message p {\n    font-size: 13px;\n    color: var(--anzhiyu-fontcolor);\n    margin: 5px 0;\n  }\n\n  #retry-button {\n    color: var(--anzhiyu-theme);\n    cursor: pointer;\n    font-style: normal;\n    font-weight: 500;\n  }\n\n  #retry-button:hover {\n    text-decoration: underline;\n  }\n\u003C/style>\n\n\u003Cdiv class=\"welcome-section\">\n  \u003Cdiv class=\"section-title\">\n    \u003Cspan class=\"icon\">👤\u003C/span>\n    \u003Cspan>欢迎来访者\u003C/span>\n  \u003C/div>\n  \u003Cdiv class=\"welcome-list\">\n    \u003Cdiv class=\"welcome-list-item\">\n      \u003Cspan class=\"emoji\">👋🏻\u003C/span>\n      \u003Cspan>Hi，我是站长，欢迎你！\u003C/span>\n    \u003C/div>\n    \u003Cdiv class=\"welcome-list-item\">\n      \u003Cspan class=\"emoji\">❓\u003C/span>\n      \u003Cspan>如有问题欢迎评论区交流！\u003C/span>\n    \u003C/div>\n    \u003Cdiv class=\"welcome-list-item\">\n      \u003Cspan class=\"emoji\">😫\u003C/span>\n      \u003Cspan>页面异常？试试 \u003Cspan class=\"kbd\">Ctrl\u003C/span>+\u003Cspan class=\"kbd\">F5\u003C/span>\u003C/span>\n    \u003C/div>\n    \u003Cdiv class=\"welcome-list-item\">\n      \u003Cspan class=\"emoji\">📧\u003C/span>\n      \u003Cspan>联系我：\u003Ca href=\"mailto:your-email@example.com\">发送邮件 🚀\u003C/a>\u003C/span>\n    \u003C/div>\n  \u003C/div>\n\u003C/div>\n\n\u003Cdiv class=\"section-divider\">\u003C/div>\n\n\u003Cdiv id=\"welcome-info\">\n  \u003Cdiv class=\"loading-spinner\">\u003C/div>\n\u003C/div>\n\n\u003Cscript>\n  (function () {\n    'use strict';\n\n    const CONFIG = {\n      API_KEY: 'YOUR_API_KEY',\n      API_URL: 'https://v1.nsuuu.com/api/ipip',\n      BLOG_LAT: 39.9042,\n      BLOG_LNG: 116.4074\n    };\n\n    const welcomeEl = document.getElementById('welcome-info');\n    if (!welcomeEl) return;\n\n    const showLoading = () => {\n      welcomeEl.innerHTML = '\u003Cdiv class=\"loading-spinner\">\u003C/div>';\n    };\n\n    const showError = (msg) => {\n      welcomeEl.innerHTML = `\n        \u003Cdiv class=\"error-message\">\n          \u003Cdiv class=\"error-icon\">😕\u003C/div>\n          \u003Cp>${msg}\u003C/p>\n          \u003Cp>点击 \u003Ci id=\"retry-button\">↻ 重试\u003C/i>\u003C/p>\n        \u003C/div>\n      `;\n\n      document.getElementById('retry-button')?.addEventListener('click', () => {\n        showLoading();\n        fetchAndShow();\n      });\n    };\n\n    const calcDistance = (lat, lng) => {\n      if (!lat || !lng || isNaN(lat) || isNaN(lng)) return '未知';\n      const R = 6371;\n      const dLat = (lat - CONFIG.BLOG_LAT) * Math.PI / 180;\n      const dLng = (lng - CONFIG.BLOG_LNG) * Math.PI / 180;\n      const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +\n        Math.cos(CONFIG.BLOG_LAT * Math.PI / 180) * Math.cos(lat * Math.PI / 180) *\n        Math.sin(dLng / 2) * Math.sin(dLng / 2);\n      const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));\n      return Math.round(R * c);\n    };\n\n    const getTimeGreeting = () => {\n      const h = new Date().getHours();\n      if (h \u003C 6) return \"凌晨好🌙，夜深了注意休息~\";\n      if (h \u003C 9) return \"早上好🌤️，一日之计在于晨\";\n      if (h \u003C 12) return \"上午好☀️，工作顺利~\";\n      if (h \u003C 14) return \"中午好🍱，记得午休喔~\";\n      if (h \u003C 17) return \"下午好🕞，饮茶先啦！\";\n      if (h \u003C 19) return \"傍晚好🌇，记得按时吃饭~\";\n      if (h \u003C 22) return \"晚上好🌙，夜生活嗨起来！\";\n      return \"夜深了🌛，早点休息吧~\";\n    };\n\n    const getRegionGreeting = (country, province, city) => {\n      if (!country) return \"神秘的访客，欢迎来到我的小站~\";\n\n      const foreignGreetings = {\n        \"日本\": \"よろしく，一起去看樱花吗\",\n        \"美国\": \"Let us live in peace!\",\n        \"英国\": \"想同你一起夜乘伦敦眼\",\n        \"俄罗斯\": \"干了这瓶伏特加！\",\n        \"法国\": \"C'est La Vie\",\n        \"德国\": \"Die Zeit verging im Fluge.\",\n        \"澳大利亚\": \"一起去大堡礁吧！\",\n        \"加拿大\": \"拾起一片枫叶赠予你\",\n        \"韩国\": \"안녕하세요，一起去首尔塔吧\",\n        \"新加坡\": \"花园城市欢迎你~\",\n        \"泰国\": \"萨瓦迪卡，泰国欢迎你~\"\n      };\n\n      if (foreignGreetings[country]) return foreignGreetings[country];\n      if (country !== \"中国\") return \"带我去你的国家逛逛吧~\";\n\n      const directGreetings = {\n        \"北京\": \"北——京——欢迎你~~~\",\n        \"上海\": \"走在外滩，感受历史与现代的交融。\",\n        \"天津\": \"来天津，尝尝狗不理包子~\",\n        \"重庆\": \"山城重庆，火锅与夜景都很美！\",\n        \"香港\": \"购物天堂香港，买买买！\",\n        \"澳门\": \"澳门，东方蒙特卡洛~\",\n        \"台湾\": \"宝岛台湾，夜市小吃超赞！\"\n      };\n\n      if (directGreetings[province]) return directGreetings[province];\n\n      const provinceGreetings = {\n        \"广东\": {\n          \"广州\": \"看小蛮腰，喝早茶了嘛~\",\n          \"深圳\": \"来深圳，感受科技与活力！\",\n          \"珠海\": \"浪漫之城珠海，海风轻拂。\",\n          \"东莞\": \"东莞，制造业之都，经济活跃。\",\n          \"佛山\": \"佛山，武术之乡，陶瓷文化深厚。\",\n          \"湛江\": \"湛江，南海之滨，海鲜美味。\",\n          \"_default\": \"带你感受广东的热情与美食！\"\n        },\n        \"浙江\": {\n          \"杭州\": \"西湖美景，三月天~\",\n          \"宁波\": \"来宁波，感受大海的气息。\",\n          \"温州\": \"温州人杰地灵，商贸繁荣。\",\n          \"绍兴\": \"绍兴，酒乡文化，古韵悠长。\",\n          \"湖州\": \"湖州，太湖之滨，风景如画。\",\n          \"_default\": \"这里是浙江，充满江南的韵味！\"\n        },\n        \"四川\": {\n          \"成都\": \"宽窄巷子，成都慢生活。\",\n          \"绵阳\": \"享受科技城的宁静与创新。\",\n          \"自贡\": \"自贡的盐文化与灯会，独具魅力。\",\n          \"德阳\": \"德阳，历史悠久，文化底蕴深厚。\",\n          \"乐山\": \"乐山大佛，世界文化遗产。\",\n          \"_default\": \"来四川，品麻辣火锅，赏壮丽山河。\"\n        }\n      };\n\n      let prov = province;\n      if (prov) {\n        prov = prov.replace(/省$|自治区$|回族自治区$|维吾尔自治区$|壮族自治区$/g, '');\n      }\n\n      if (provinceGreetings[prov]) {\n        const cityMap = provinceGreetings[prov];\n        if (city && cityMap[city]) return cityMap[city];\n        if (city) {\n          const cityName = city.replace(/市$/, '');\n          if (cityMap[cityName]) return cityMap[cityName];\n        }\n        return cityMap[\"_default\"] || \"带我去你的城市逛逛吧！\";\n      }\n\n      return \"带我去你的城市逛逛吧！\";\n    };\n\n    const showWelcome = (data) => {\n      const { country, province, city, ip, latitude, longitude, isp } = data || {};\n\n      let location = '神秘地区';\n      if (country) {\n        if (country === '中国') {\n          location = [province, city].filter(Boolean).join(' ') || '中国';\n        } else {\n          location = country;\n        }\n      }\n\n      const dist = calcDistance(parseFloat(latitude), parseFloat(longitude));\n      const address = [country, province, city].filter(Boolean).join(' · ') || '--';\n      const ipDisplay = (ip || '未知').replace(/\u003C/g, '&lt;').replace(/>/g, '&gt;');\n\n      welcomeEl.innerHTML = `\n        \u003Cdiv class=\"welcome-card\">\n          \u003Cp class=\"greeting\">欢迎你~ \u003Cbr>来自 \u003Cspan class=\"highlight\">${location}\u003C/span> 的小伙伴，你好呀!\u003C/p>\n          \u003Cp class=\"greeting\">你目前距博主约 \u003Cspan class=\"highlight\">${dist} 公里\u003C/span>\u003C/p>\n          \u003Cp class=\"greeting\">${getTimeGreeting()}\u003C/p>\n          \u003Cp class=\"greeting greeting-tip\">Tip：\u003Cspan class=\"highlight\">${getRegionGreeting(country, province, city)}\u003C/span> 🍂\u003C/p>\n          \u003Cdiv class=\"detail-expand\">\n            \u003Cdiv class=\"expand-content\" style=\"display: none;\">\n              \u003Cdiv class=\"detail-panel\">\n                \u003Cdiv class=\"detail-row\">\u003Cspan class=\"label\">IP\u003C/span>\u003Cspan class=\"value\">${ipDisplay}\u003C/span>\u003C/div>\n                \u003Cdiv class=\"detail-row\">\u003Cspan class=\"label\">地址\u003C/span>\u003Cspan class=\"value\">${address}\u003C/span>\u003C/div>\n                \u003Cdiv class=\"detail-row\">\u003Cspan class=\"label\">运营商\u003C/span>\u003Cspan class=\"value\">${isp || '未知'}\u003C/span>\u003C/div>\n              \u003C/div>\n            \u003C/div>\n            \u003Cbutton type=\"button\" class=\"toggle-btn\">\n              \u003Cspan class=\"toggle-icon\">▼\u003C/span>\n              \u003Cspan class=\"toggle-text\">展开来访详情\u003C/span>\n            \u003C/button>\n          \u003C/div>\n        \u003C/div>\n      `;\n\n      const btn = welcomeEl.querySelector('.toggle-btn');\n      const content = welcomeEl.querySelector('.expand-content');\n\n      if (btn && content) {\n        btn.addEventListener('click', () => {\n          const isOpen = content.style.display !== 'none';\n          content.style.display = isOpen ? 'none' : 'block';\n          btn.classList.toggle('expanded', !isOpen);\n          btn.querySelector('.toggle-text').textContent = isOpen ? '展开来访详情' : '收起来访详情';\n        });\n      }\n    };\n\n    const fetchAndShow = async () => {\n      try {\n        const response = await fetch(CONFIG.API_URL, {\n          method: 'GET',\n          headers: {\n            'key': CONFIG.API_KEY\n          }\n        });\n\n        if (!response.ok) {\n          throw new Error(`HTTP ${response.status}`);\n        }\n\n        const result = await response.json();\n\n        if (result.code === 200 && result.data) {\n          showWelcome(result.data);\n        } else {\n          throw new Error(result.message || '数据格式错误');\n        }\n      } catch (err) {\n        console.error('获取IP信息失败:', err);\n        showWelcome({});\n      }\n    };\n\n    fetchAndShow();\n  })();\n\u003C/script>\n","完整可复制代码","wrap expand",[151,865,861],{"__ignoreMap":314},[15,867,868],{},"直接使用时，至少先改这 4 项：",[91,870,871,873,875,879],{},[43,872,184],{},[43,874,187],{},[43,876,877],{},[151,878,192],{"code":192},[43,880,881,198,883],{},[151,882,197],{"code":197},[151,884,201],{"code":201},[30,886,887],{"id":887},"这几个地方你大概率会自己改",[889,890,891],"card-list",{},[40,892,893,896,899,902,905,908,911],{},[43,894,895],{},"顶部欢迎区文字和邮箱链接",[43,897,898],{},"接口地址和 API key",[43,900,901],{},"博主坐标",[43,903,904],{},"国外问候语文案",[43,906,907],{},"直辖市问候语文案",[43,909,910],{},"各省各城市的欢迎提示",[43,912,913],{},"按钮文字和卡片语气",[15,915,916],{},"如果你想改成自己的风格，最推荐优先改这三部分：",[91,918,919,922,925],{},[43,920,921],{},"顶部固定文案",[43,923,924],{},"地区问候语",[43,926,927],{},"卡片主文案",[15,929,930],{},"因为它们最影响“这个模块像不像你的站”。",[30,932,933],{"id":933},"使用时要注意什么",[10,935,937,943],{"type":20,":card":936},"true",[938,939,940],"template",{"v-slot:title":314},[15,941,942],{},"这段代码能用，但如果你准备长期维护，下面这些点最好提前知道。",[91,944,945,950,959,968,975],{},[43,946,947,949],{},[151,948,192],{"code":192}," 直接写在前端，访客是能看到的。",[43,951,952,953,955,956,323],{},"当前版本虽然写了 ",[151,954,528],{"code":528},"，但失败时默认还是走 ",[151,957,958],{"code":958},"showWelcome({})",[43,960,961,964,965,967],{},[151,962,963],{"code":963},"calcDistance()"," 把 ",[151,966,570],{"code":570}," 当成了无效值。",[43,969,970,971,974],{},"直辖市如果接口返回的是 ",[151,972,973],{"code":973},"北京市"," 这种格式，可能匹配不到专属文案。",[43,976,977,980],{},[151,978,979],{"code":979},".permission-dialog"," 这套样式现在属于预留状态，暂时没真正用起来。",[135,982,983],{"id":983},"如果你想把它做得更稳一点",[15,985,986],{},"可以继续往下优化：",[889,988,989],{},[40,990,991,994,997,1003,1006],{},[43,992,993],{},"用服务端代理接口，避免前端直接暴露 key。",[43,995,996],{},"给省份和城市做更统一的标准化处理。",[43,998,999,1000,1002],{},"失败时真正调用 ",[151,1001,528],{"code":528},"，再让用户手动重试。",[43,1004,1005],{},"给请求增加超时控制。",[43,1007,1008],{},"把地区映射表单独拆出去，方便长期维护。",[30,1010,1011],{"id":1011},"结尾",[15,1013,1014],{},"这个侧边栏欢迎模块最有意思的地方，其实不在技术复杂度，而在“数据”和“文案”结合之后，能让一个普通侧边栏突然有了点人情味。",[15,1016,1017],{},"它并不只是告诉访客“你来自哪里”，而是在试着用一句更自然的话去接住这个来访动作。",[15,1019,1020],{},"如果你刚好也喜欢这种细节感，这种小模块会很值得折腾。",[1022,1023,1024,1029],"quote",{},[938,1025,1026],{"v-slot:icon":314},[15,1027,1028],{},"🍂",[15,1030,1031],{},"真正让这个模块好看的，不只是样式和动画，而是那句恰到好处的欢迎语。",[10,1033,1034],{"type":12,"title":13},[15,1035,1036],{},"本文由 ChatGPT 5.4 协同撰写与整理，由 OpenClaw 自动化构建发布。",{"title":314,"searchDepth":1038,"depth":1038,"links":1039},4,[1040,1042,1043,1048,1049,1050,1054,1055,1056,1057,1058,1061,1062,1063,1064,1065,1066,1069],{"id":32,"depth":1041,"text":32},2,{"id":86,"depth":1041,"text":86},{"id":133,"depth":1041,"text":133,"children":1044},[1045,1047],{"id":137,"depth":1046,"text":138},3,{"id":144,"depth":1046,"text":144},{"id":204,"depth":1041,"text":204},{"id":290,"depth":1041,"text":291},{"id":336,"depth":1041,"text":337,"children":1051},[1052,1053],{"id":343,"depth":1046,"text":343},{"id":392,"depth":1046,"text":392},{"id":408,"depth":1041,"text":409},{"id":501,"depth":1041,"text":502},{"id":535,"depth":1041,"text":536},{"id":584,"depth":1041,"text":585},{"id":608,"depth":1041,"text":609,"children":1059},[1060],{"id":668,"depth":1046,"text":668},{"id":712,"depth":1041,"text":713},{"id":783,"depth":1041,"text":784},{"id":817,"depth":1041,"text":818},{"id":851,"depth":1041,"text":851},{"id":887,"depth":1041,"text":887},{"id":933,"depth":1041,"text":933,"children":1067},[1068],{"id":983,"depth":1046,"text":983},{"id":1011,"depth":1041,"text":1011},[1071],"博客美化","2026-03-14 21:50:09","拆解最新版侧边栏地区欢迎卡片的结构、运行逻辑、可修改项与常见坑点，适合直接照着接入和二次改造。",false,"md","https://dev.jiugg.top/i/f8eb68b2-0a98-4e6a-aebb-cb7ce9e202e2.webp",{"slots":1078},{},true,"/2026/20260314-1",null,"https://cdn.jiugg.top/boke/2026/侧边栏欢迎卡片教程.mp3","https://cdn.jiugg.top/boke/2026/侧边栏欢迎卡片教程.lrc","AI播客",{"text":1086,"minutes":1087,"time":1088,"words":1089},"29 min read",28.085,1685100,5617,{"title":5,"description":1073},{"loc":1080},"posts/2026/20260314-1",[1094,170,248,240,1071],"教程","tech","J0yHaw7mc78pmE43zRKtiT6LqVhTMUuonlm5ndbRL7A",[1098,1103],{"title":1099,"path":1100,"stem":1101,"date":1102,"type":1095,"children":-1},"anheyu博客侧边栏创建倒数卡片","/2026/anheyu","posts/2026/anheyu博客侧边栏创建倒数卡片","2026-03-10 22:41:02",{"title":1104,"path":1105,"stem":1106,"date":1107,"type":1095,"children":-1},"高通平台免解 BL 临时 Root 实战","/2026/20260320-1","posts/2026/20260320-1","2026-03-20 23:24:28",1775556257714]