效果预览 与本站首页的“欢迎来访者-侧边栏卡片”同款
添加方法 创建页面文件 在[blogroot]/themes/butterfly/layout/includes/widget中新建card_welcome.pug文件,并添加如下内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 .card-widget.card-welcome .item-headline i.fa.fa-user span 欢迎 #welcome-info .error-message p span.emoji 😥 br span.text 遇到了问题 br span.text 位置 API 请求错误 br span.text 请等待博主修复呀🤗~ script(src='/js/card_welcome.js')
引入页面文件 将该结构插入到对应的位置,可以在当前文件夹[blogroot]/themes/butterfly/layout/includes/widget中的index.pug找到对应结构,如下:
1 2 3 4 5 else //- page !=partial('includes/widget/card_author', {}, {cache: true}) !=partial('includes/widget/card_announcement', {}, {cache: true}) !=partial('includes/widget/card_top_self', {}, {cache: true})
1 2 3 4 5 6 else //- page !=partial('includes/widget/card_author', {}, {cache: true}) !=partial('includes/widget/card_announcement', {}, {cache: true}) !=partial('includes/widget/card_welcome', {}, {cache: true}) !=partial('includes/widget/card_top_self', {}, {cache: true})
创建脚本文件 在[blogroot]/source/js中新建card-welcome.js文件,并添加如下内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 function fetchIpLocation ( ) { return fetch ("https://ip-api.o0w0b.top/?lang=zh-CN" ) .then (response => response.json ()) .then (data => { return { ip : data.query , data : { country : data.country , prov : data.regionName , city : data.city , district : data.district , lat : data.lat , lon : data.lon } }; }) .catch (err => { console .error ("获取 IP 定位失败:" , err); return null ; }); } function getDistance (lon1, lat1, lon2, lat2 ) { function toRad (d ) { return d * Math .PI / 180 ; } const R = 6371 ; const dLat = toRad (lat2 - lat1); const dLon = toRad (lon2 - lon1); const a = Math .sin (dLat / 2 ) ** 2 + Math .cos (toRad (lat1)) * Math .cos (toRad (lat2)) * Math .sin (dLon / 2 ) ** 2 ; const c = 2 * Math .atan2 (Math .sqrt (a), Math .sqrt (1 - a)); return (R * c).toFixed (2 ); } function showWelcome (ipLocation ) { if (!ipLocation || !ipLocation.data ) { console .error ('ipLocation data is not available.' ); return ; } let lon = ipLocation.data .lon ; let lat = ipLocation.data .lat ; let dist = getDistance (126.904 , 37.0849 , lon, lat); let pos = ipLocation.data .country ; let ip = ipLocation.ip ; let posdesc; switch (ipLocation.data .country ) { case "日本" : posdesc = "こんにちは!<br>日本樱花盛开,景色如画" ; break ; case "美国" : posdesc = "Hello! <br>美国大地辽阔,城市风光各异" ; break ; case "英国" : posdesc = "Hello! <br>伦敦塔桥与泰晤士河相映成景" ; break ; case "俄罗斯" : posdesc = "Привет! <br>俄罗斯广袤土地,冬日雪景迷人" ; break ; case "法国" : posdesc = "Bonjour! <br>法国乡村与巴黎街景交相辉映" ; break ; case "德国" : posdesc = "Hallo! <br>德国古堡林立,啤酒节热闹非凡" ; break ; case "澳大利亚" : posdesc = "G’day! <br>澳大利亚海岸与内陆风光壮丽" ; break ; case "加拿大" : posdesc = "Hey! <br>加拿大枫叶红遍,湖光山色宜人" ; break ; case "韩国" : posdesc = "안녕하세요! <br>韩国泡菜、辣炒年糕色香味俱全" ; break ; case "中国" : pos = ipLocation.data .prov + " " + ipLocation.data .city + " " + ipLocation.data .district ; switch (ipLocation.data .prov ) { case "北京市" : posdesc = "北京故宫庄严,天安门广场宏伟" ; break ; case "天津市" : posdesc = "天津海河蜿蜒,古文化街风情浓" ; break ; case "河北省" : posdesc = "河北长城蜿蜒,群山连绵" ; break ; case "山西省" : posdesc = "山西古建筑众多,历史厚重" ; break ; case "内蒙古自治区" : posdesc = "内蒙古草原辽阔,风吹草低见牛羊" ; break ; case "辽宁省" : posdesc = "辽宁沿海风光秀丽,城市与海岸交错" ; break ; case "吉林省" : posdesc = "吉林冬季雪景壮丽,松花江蜿蜒" ; break ; case "黑龙江省" : posdesc = "黑龙江冰雪世界,松花江静谧" ; break ; case "上海市" : posdesc = "上海外滩璀璨,高楼林立" ; break ; case "江苏省" : switch (ipLocation.data .city ) { case "南京市" : posdesc = "南京古都,秦淮河夜色迷人" ; break ; case "苏州市" : posdesc = "苏州园林精美,水乡小桥流水" ; break ; default : posdesc = "江苏江南水乡,河流纵横" ; break ; } break ; case "浙江省" : switch (ipLocation.data .city ) { case "杭州市" : posdesc = "杭州西湖烟雨,山水如画" ; break ; default : posdesc = "浙江山水秀丽,文化底蕴深厚" ; break ; } break ; case "河南省" : switch (ipLocation.data .city ) { case "郑州市" : posdesc = "郑州古今交融,城中绿地广阔" ; break ; case "信阳市" : posdesc = "信阳茶园连片,绿意盎然" ; break ; case "南阳市" : posdesc = "南阳河流纵横,历史遗迹丰富" ; break ; case "驻马店市" : posdesc = "驻马店平原开阔,田园景色宜人" ; break ; case "开封市" : posdesc = "开封古都,古建筑保存完好" ; break ; case "洛阳市" : posdesc = "洛阳牡丹盛开,花城景色优美" ; break ; default : posdesc = "河南自然与历史景观丰富" ; break ; } break ; case "安徽省" : posdesc = "安徽黄山云海,奇峰异石层出" ; break ; case "福建省" : posdesc = "福建山海相连,海岸风光秀丽" ; break ; case "江西省" : posdesc = "江西庐山高耸,江河环绕" ; break ; case "山东省" : posdesc = "山东泰山雄伟,沿海城市风光美" ; break ; case "湖北省" : switch (ipLocation.data .city ) { case "黄冈市" : posdesc = "黄冈江河交错,山水相依" ; break ; case "武汉市" : posdesc = "武汉江滩开阔,城市景色壮丽" ; break ; default : posdesc = "湖北山水秀丽,河湖众多" ; break ; } break ; case "湖南省" : posdesc = "湖南岳麓山秀美,江河湖泊环绕" ; break ; case "广东省" : switch (ipLocation.data .city ) { case "广州市" : posdesc = "广州珠江两岸,城市天际线壮观" ; break ; case "深圳市" : posdesc = "深圳高楼林立,现代都市景观丰富" ; break ; default : posdesc = "广东沿海城市众多,风光秀丽" ; break ; } break ; case "广西壮族自治区" : posdesc = "桂林山水甲天下,漓江蜿蜒" ; break ; case "海南省" : posdesc = "海南海岸线长,沙滩与蓝天相映" ; break ; case "四川省" : posdesc = "四川群山环绕,江河纵横" ; break ; case "贵州省" : posdesc = "贵州喀斯特地貌奇特,山水秀丽" ; break ; case "云南省" : posdesc = "云南高山湖泊众多,彩云之南美景" ; break ; case "西藏自治区" : posdesc = "西藏高原辽阔,雪山与草原交错" ; break ; case "陕西省" : posdesc = "陕西古城众多,历史遗迹丰富" ; break ; case "甘肃省" : posdesc = "甘肃戈壁广阔,丝路文化厚重" ; break ; case "青海省" : posdesc = "青海湖碧水环山,景色壮丽" ; break ; case "宁夏回族自治区" : posdesc = "宁夏黄河蜿蜒,沙漠与绿洲交错" ; break ; case "新疆维吾尔自治区" : posdesc = "新疆天山雪峰,高原风光辽阔" ; break ; case "台湾省" : posdesc = "台湾岛屿纵横,山海景色独特" ; break ; case "香港特别行政区" : posdesc = "香港维多利亚港,城市天际线迷人" ; break ; case "澳门特别行政区" : posdesc = "澳门滨海风光与历史建筑交相辉映" ; break ; default : posdesc = "欢迎欢迎!" ; break ; } break ; default : posdesc = "Hello! 欢迎来自国外的朋友" ; break ; } let timeChange; let date = new Date (); if (date.getHours () >= 5 && date.getHours () < 11 ) timeChange = "<span>🌅 早安!新的一天开始啦,记得吃早餐哦~</span>" ; else if (date.getHours () >= 11 && date.getHours () < 13 ) timeChange = "<span>🍴 中午好!吃点好吃的补充能量吧~</span>" ; else if (date.getHours () >= 13 && date.getHours () < 17 ) timeChange = "<span>☕ 下午好!来杯茶,继续加油吧~</span>" ; else if (date.getHours () >= 17 && date.getHours () < 19 ) timeChange = "<span>🌇 傍晚好!今天辛苦了,放松一下吧~</span>" ; else if (date.getHours () >= 19 && date.getHours () < 24 ) timeChange = "<span>🌙 晚上好!属于自己的时间到啦,随心享受吧~</span>" ; else timeChange = "<span>🌌 夜深了,早点休息,明天继续加油!</span>" ; let welcomeInfoElement = document .getElementById ("welcome-info" ); if (welcomeInfoElement) { welcomeInfoElement.innerHTML = ` <p>Hey~ 来自 <span class="user-location">${pos} </span> 的来访者!😝</p> <p>${posdesc} 🏞️</p> <!-- <p>目前距博主约 <span class="distance">${dist} </span> 公里!</p> --> <p>经度:<span class="distance">${lon} </span><br>纬度:<span class="distance">${lat} </span></p> <p>网络 IP:<span class="ip-address">${ip} </span></p> <p class="time-greeting">${timeChange} </p> ` ; } else { console .log ("Pjax无法获取元素" ); } } function isWelcomeInfoAvailable ( ) { let welcomeInfoElement = document .getElementById ("welcome-info" ); return welcomeInfoElement !== null ; } function handlePjaxComplete (ipLocation ) { if (isWelcomeInfoAvailable ()) { showWelcome (ipLocation); } } function onLoad ( ) { fetchIpLocation ().then (ipLocation => { if (isWelcomeInfoAvailable ()) { showWelcome (ipLocation); } document .addEventListener ("pjax:complete" , () => handlePjaxComplete (ipLocation)); }); } window .onload = onLoad;
如果要显示访客与你的距离,找到如下内容,并取消注释
计算距离部分,要修改为自己当前位置的经度、纬度(原文件中,经度是126.904、纬度是37.0849)
1 let dist = getDistance (126.904 , 37.0849 , ipLocation.data .lon , ipLocation.data .lat );
创建样式文件 在[blogroot]/themes/source/css/_layout中新建card-welcome.styl文件,并添加如下内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 [data-theme=light] --welcome-bg : rgba (230 ,230 ,235 ,0.8 ) [data-theme=dark] --welcome-bg : rgba (25 ,35 ,60 ,0.8 ) #aside-content .card-widget .card-welcome padding : 20px 24px .item-headline margin : 0px .item-content margin : 0px #welcome-info text-align center background-color : var (--welcome-bg) border-radius : 8px padding : 12px .error-message height : 200px display : flex justify-content : center align-items : center p text-align : center .emoji font-size : 40px .text font-size : 16px color : var (--text-color) p margin : 0px .user-location font-size : 16px color : var (--default-bg-color) font-weight : bold .distance font-size : 16px color : var (--default-bg-color) font-weight : bold .ip-address filter : blur (5px ) font-size : 16px color : var (--default-bg-color) transition : filter 0.5s &:hover filter : none font-weight : bold .time-greeting font-size : 15px color : var (--text-color) font-weight : bold margin-top : 8px
自建 API(可选) 起因 ip-api.com 的 HTTPS 端点只对「商业 Key」开放,免费用户直接走 HTTPS 会被 403 拒绝;免费版只能用明文 HTTP
解决办法 用 Cloudflare Worker 反向代理,Worker 自带 HTTPS 证书
准备工作
项目
说明
账号
免费 Cloudflare 账户
域名
任意已接入 Cloudflare 的域名(可选,后期绑定)
创建 Worker
登录 Cloudflare Dashboard
侧边栏 Workers → 创建服务 → 名称随意(例如 ip-api)→ 创建
点击右上角编辑代码 ,清空默认代码,替换为下方内容(二选一):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 addEventListener ('fetch' , (event ) => { event.respondWith (handleRequest (event.request )); }); async function handleRequest (request ) { const clientIP = request.headers .get ('CF-Connecting-IP' ); const search = new URL (request.url ).search ; const upstream = `http://ip-api.com/json/${clientIP} ${search} ` ; return fetch (upstream); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 addEventListener ('fetch' , (event ) => { event.respondWith (handleRequest (event.request )); }); async function handleRequest (request ) { const clientIP = request.headers .get ('CF-Connecting-IP' ); const search = new URL (request.url ).search ; const upstream = `http://ip-api.com/json/${clientIP} ${search} ` ; const res = await fetch (upstream); const ALLOWED = [ 'http://localhost:4000' , 'https://blog.o0w0b.top' ]; const headers = { 'Content-Type' : 'application/json' , }; const origin = request.headers .get ('Origin' ); if (ALLOWED .includes (origin)) { headers['Access-Control-Allow-Origin' ] = origin; } else { return new Response ('CORS not allowed' , { status : 403 }); } return new Response (res.body , { status : res.status , statusText : res.statusText , headers }); }
修改这里面的内容就可以
1 2 3 4 5 const ALLOWED = [ 'http://localhost:4000' , 'https://blog.o0w0b.top' ];
点击右上角部署 ,Workers 的访问链接 https://xxx.xxx.workers.dev 就是 API 地址
绑定自己的域名(可选) 网上有很多 Cloudflare Worker 绑定域名的教程