十月份看完的第一本书
为这本书,我近两天几乎废寝忘食,心中感慨颇多,最深刻的收获便是对人性与形式的深刻理解。许多事情在当时看来似乎是理所当然,但如今回首,才发现那时的自己是多么稚嫩,甚至有些可笑
这是我第一次尝试电子书,有声加文字的双重体验感觉不错,美中不足的是,机械式的发音让情感显得苍白,在这个时代,做个拟人化的语音合成也不难啊,腾讯读书这块业务还是小众,资源太少了,很多我想看的书都找不到,涉及敏感话题的搜都搜不到,哎
为这本书,我近两天几乎废寝忘食,心中感慨颇多,最深刻的收获便是对人性与形式的深刻理解。许多事情在当时看来似乎是理所当然,但如今回首,才发现那时的自己是多么稚嫩,甚至有些可笑
这是我第一次尝试电子书,有声加文字的双重体验感觉不错,美中不足的是,机械式的发音让情感显得苍白,在这个时代,做个拟人化的语音合成也不难啊,腾讯读书这块业务还是小众,资源太少了,很多我想看的书都找不到,涉及敏感话题的搜都搜不到,哎
凌晨四点半,忽然醒了,辗转反侧,睡意全无,脑海中第一个蹦出的念头竟然是骑车
连衣服都没顾得上穿,立刻下床检查车子胎压,拿好盐丸和刚冲泡的蛋白粉,穿上骑行服,扛着车冲出家门,直奔早餐店
到早餐店的时间是4.50,来的太早了,选择不多,只有小米粥和茶叶蛋,包子还要等五分钟才熟,要了一碗小米粥、两个茶叶蛋、三个牛肉包。吃得有些急促,可以用赵本山的急头白脸吃一顿来表示
坐在店里,我心中还没想好骑去哪儿。随手打开地图,首页推荐了鹿邑的太清宫。还记得小时候常听到同学开玩笑说:老子是鹿邑的!
看了一下路线,沿着322省道,往返148km,这个省道我走过,去年去骑行去上海就是这个,路况烂的没法说,这次过去更是炸裂
路况越来越差了,柏油路被超载车辆压的细碎,我这25c管胎感觉随时都要爆,不禁怀念瓜车山地带来的安全感
8.20到达太清宫,但是车子的存放问题要解决一下,正好门口有卖香火的大妈,我付了她十块钱,让她帮我看着车子
太清宫的门票要60,有些贵了,相比之下,淮阳的太昊陵只要40,且规模和和设计都远胜太清宫
从三清大殿出来后看到这,我瞬间挂上痛苦面具,因为我今天出来穿的是骑行锁鞋,前脚跟高,后脚跟低,走路时一步两响,比高跟鞋都变态
出来后多少有些失望,毕竟这是老子的诞生地,但道教的气息却淡得几乎感觉不到,园林设计更是稀碎。相反,倒是社会主义的标语随处可见。我是感觉现在的寺庙和道观都没有其文化特色,如同各地方美食城的铁板鱿鱼
想必这和上层意识脱不开干系,组织对于宗教信仰很敏感
一路上导航和听歌消耗了不少电,在奶茶店喝了两杯,顺便给手机充了个电,不过鹿邑中午也太热了,我出发时还穿了棉背心
返程78km,全程逆风,出发前还吃多了,当我坐在车上那一刻,再次带上痛苦面具…
下午五点半点到家,回来得有些匆忙,因为这个省道很窄,晚上全是半挂,我不敢贪玩冒这个风险,图片还是有点少了,骑行时不可自拔,除了点烟会顺手会拿起手机拍照,其他时候不想断了骑行的节奏
在河南老家,人们通常称自行车为“洋车子”。确实,是洋鬼子的杰作,早在1791年,法国佬西夫拉克设计出了世界上第一辆自行车,据说他是受溜冰鞋的启发,整车为木制,上管由一根大横梁作为主体车架,下方装两个木制车轮,没有转向结构,拐弯全靠搬。当然,它也没有传动系统,骑多快这就真看腿了
追溯历史,19世纪六七十年代,自行车由洋鬼子、华人带入中国,数量寥寥
随着时间的推移,20世纪二十年代,上海做自行车销售的同昌车行开始仿制生自行车零部件,再搭配进口件的基础上进行组装贴牌。从此内地第一家仿制、组装整车的自行车企业诞生,据注册登记记载有:飞马、飞鹰、飞人、飞轮等型号。直到新中国1956年国家队将其收编、创新再贴牌。由此诞生:上海凤凰
新中国1955-1957年期间,是内地自行车产业发展的关键时期,由主管自行车工业的工信部,组织上海、天津、沈阳的自行车厂进行标准化设计,确定了28寸自行车的规格和零配件的质量标准,为内地自行车行业制定了第一部行业标准,为后来几十年的自行车普及奠定了基础
到了新中国六七十年代,自行车在全国范围内迅速普及,尽管当时的产业链尚不完善,但许多品牌已逐渐崭露头角,下面有请八十年代国产五虎上将:上海永久、凤凰、天津飞鸽、红旗、江苏金狮
新中国1981年9月,三流日报曾转载了一篇文章:湖北农民杨小运在超额完成国家交售指标后,县里问他想要什么奖励,他说想要一辆永久牌自行车。很多年后,杨小运回忆:“我是壮起胆子提出了自己的想法。须知道那时候能够骑上一辆永久牌自行车是多么得意的事情,因为据说只有凭什么“工业券”才能买到这种稀罕物,比现在坐小轿车还要显摆,简直就是一种身份的象征。在我的印象中,好像只有公社党委书记这样的人物才配有这么一辆自行车
由此可见,自行车不仅是代步工具,更是那个年代富裕与社会地位的象征,俗称小资三件套:缝纫机、手表,自行车
然而,好景不长。随着改革开放的推进,外资品牌涌入中国市场,这些闭门仿车国产老品牌被外资的新工业设计所冲击,如:GIANT、Merida、Trek等等
此时的自行车带来最直接的就是视觉感官,功能多样的车架和丰富多彩的颜色,彻底颠覆了内地自行车的固有形象。相比之下,国产品牌的市场份额断崖式下跌,怕不是编制兜底,如今已全部倒闭,至此,沪上小资品牌殊荣不再
在新工业品牌中,最具代表性的是GIANT,尽管价格昂贵,但其时尚的设计和优越的性能激发了人民的购买欲望,迅速取代国产五虎上将,成为新一代的“高级自行车”,GIANT驻扎内地后年年蝉联市场第一,这一变革标志着中国内地自行车市场的一个重要转折点
从改革开放到21世纪初的几十年间,中国自行车运动突然就死在了襁褓里,由于汽车的普及和部分国人的观念癌症,自行车逐渐边缘化,成为人们眼中过时的交通工具。就此,内地再也没有形成新的自行车运动
而在此期间,外面的世界发生了天翻地覆的变化。UCI在瑞士洛桑成立了世界自行车中心和场地车训练场,致力于培训精英选手并为赛事提供支持。高卢鸡的环法赛愈演愈烈,成为全球最受瞩目的公路自行车赛事之一。与此同时,小日子的Shimano凭借革命性的STI技术,将刹车和变速融合,逐步通过专利,垄断全球自行车零部件市场
反观中国内地。自行车产业在这一波全球变革中停滞不前,国产品牌没有主动认清提升技术和产品价值的重要性,依然闭门造车,专注于下沉市场。没有技术,没有创新,短期图利坑蒙拐骗,论长期发展无异于自我毁灭
直到今天,大部分内地品牌在观念和行动上依然停留在过去的成功经验 ,靠坑蒙拐骗人民继续吃老本,最近又冒出一个内地品牌——Maserati玛莎拉蒂自行车,一个车架都要使用公模,搭配些工业垃圾套件组装。技术水平确实了不起,至少标贴得还算正。我都感到丢人,是不是人民好糊弄?都当成傻子?《这些,它们够用了》
进入21世纪后,随着资本经济的涌入,共享单车兴起,给中国的自行车运动带来了新的生机,环保意识的提升和健康生活方式的推广,使得越来越多的人将骑行作为健身方式和生活态度,骑行团体的壮大和各种赛事也愈发频繁
疫情期间,由于长时间的封闭管控,许多人渴望户外活动的机会,当管控解除后,户外运动迅速升温,体育及户外运动板块纷纷涨停板,骑行也因此成为了炙手可热的选择之一
然而,在各方面利好的同时,骑行运动也面临诸多挑战,在鄙人看来,部分国人对于新鲜事物的包容度较低,这与千百年来的文化属性密不可分
在这个二十一世纪的现代社会,有些人甚至认为骑行是“文化入侵”,这一观点显然过于狭隘,抱着自由言论的心态,鄙人不敢苟同
中国作为一个发展中国家,许多城市的道路基础设施尚未完善,尤其是在道路规划方面,机动车道与非机动车道的设计往往缺乏科学性和合理性,例如,原本就狭窄的非机动车道常常被汽车占用,加上逆行和鬼探头等行为,骑行安全难以保障,导致了骑行风险的增加。在这种情况下,所谓的“暴骑团”被迫驶入机动车道,进一步加剧了骑行者与机动车之间的矛盾,交通事故频发
此外,骑行者的素质问题也是不可忽视的因素。部分骑行者缺乏基本的交规意识、逆行、闯红灯等行为屡见不鲜。面对这种情况,我只能说,管好自己
关于最近发生的河北亲子骑行事件,引发了社会的广泛关注和讨论,不少网友竟表示同情,在视频中司机被殴打被迫下跪,什么死者为大?呸恶心。鄙人认为这是家长全责!缺乏对骑行的敬畏之心,忽视了安全因素。对于部分圣母指责辱骂司机我只想说
在这个环境内,你可以漠视周围发生的一切不公平,直到这种不公平在某一天以一种你无法预期的方式降临到你身上
在前几天写的数据展示页面中,日历与JSON数据的时间处理依赖于本地时区的getDay()和setDate()方法。然而,博客部署在GitHub Pages,时区的不同导致日历出现了显示偏差
涉及函数:getStartDate
原代码:
这里的getDay()和setDate()方法是基于Github本地时区,不细心
function getStartDate(today, daysOffset) {
const currentDayOfWeek = today.getDay();
const daysToMonday = (currentDayOfWeek === 0 ? 6 : currentDayOfWeek - 1);
const startDate = new Date(today);
startDate.setDate(today.getDate() - daysToMonday - daysOffset);
startDate.setDate(startDate.getDate() - (startDate.getDay() === 0 ? 6 : startDate.getDay() - 1));
return startDate;
}
改进后:
修改后:采用getUTCDay()和setUTCDate()方法,使用UTC时间来保证时间处理的一致性
function getStartDate(today, daysOffset) {
const currentDayOfWeek = today.getUTC11Day();
const daysToMonday = (currentDayOfWeek === 0 ? 6 : currentDayOfWeek - 1);
const startDate = new Date(today);
startDate.setUTCDate(today.getUTCDate() - daysToMonday - daysOffset);
return startDate;
}
// 涉及函数:generateCalendar
// 原代码:
// 在与JSON日期数据进行比较时,由于时区问题,日历的显示存在错位
const todayStr = getChinaTime().toISOString().split('T')[0];
let currentDate = new Date(startDate);
// 改进后:
// 将currentDate时间归零,避免由于时区差异导致的日期比较错误
const todayStr = getChinaTime().toISOString().split('T')[0];
let currentDate = new Date(startDate);
currentDate.setUTCHours(0, 0, 0, 0);
涉及函数:createDayContainer
同上,本地时间异常
// 原代码:
dateNumber.innerText = date.getDate();
// 改进后
dateNumber.innerText = date.getUTCDate();
涉及函数:displayCalendar
同上,本地时间异常
// 原代码:
currentDate.setDate(currentDate.getDate() + 1);
// 改进后
currentDate.setUTCDate(currentDate.getUTCDate() + 1);
1.默认显示当天日期,不显示球体
2.光标悬浮其他日历时,隐藏当天日期,显示球体,反之亦然
3.整体主题色为:rgb(36, 36, 40)
4.日历的所有日期下添加2px厚下划线
function createDayContainer(date, activities) {
const dayContainer = document.createElement('div');
dayContainer.className = 'day-container';
const dateNumber = document.createElement('span');
dateNumber.className = 'date-number';
dateNumber.innerText = date.getUTCDate();
dayContainer.appendChild(dateNumber);
const activity = activities.find(activity => activity.activity_time === date.toISOString().split('T')[0]);
// console.log(processedActivities);
if (activity) processedActivities.push(activity);
// 根据骑行距离设置球的大小
const ballSize = activity ? Math.min(parseFloat(activity.riding_distance) / 4, 24) : 2;
const ball = document.createElement('div');
ball.className = 'activity-indicator';
ball.style.width = `${ballSize}px`;
ball.style.height = `${ballSize}px`;
if (!activity) ball.classList.add('no-activity');
ball.style.left = '50%';
ball.style.top = '50%';
dayContainer.appendChild(ball);
dayContainer.addEventListener('mouseenter', () => {
if (date.toDateString() === new Date().toDateString()) {
dateNumber.style.opacity = '0';
ball.style.opacity = '1';
} else {
if (todayContainer) {
todayContainer.querySelector('.date-number').style.opacity = '0';
todayContainer.querySelector('.activity-indicator').style.opacity = '1';
}
}
});
dayContainer.addEventListener('mouseleave', () => {
if (date.toDateString() === new Date().toDateString()) {
dateNumber.style.opacity = '1';
ball.style.opacity = '0';
} else {
if (todayContainer) {
todayContainer.querySelector('.date-number').style.opacity = '1';
todayContainer.querySelector('.activity-indicator').style.opacity = '0';
}
}
});
if (date.toDateString() === new Date().toDateString()) {
todayContainer = dayContainer;
dayContainer.classList.add('today');
ball.style.backgroundColor = '#242428';
dateNumber.style.color = '#242428';
dateNumber.style.opacity = '1';
ball.style.opacity = '0';
}
return dayContainer;
}
下午,笔记本忽然发出了咔哧咔哧的响声,仿佛是临死前的挣扎。我知道,又是哪零件坏了。大半年没清灰,这机器,似乎比人还脆弱
压CPU的螺丝多半滑丝,硅脂也是,抹得太多,溢得像过剩的腐败
风扇拆下来时洒下许多灰尘,这风扇犹如一台无力的革命机器,快要垮掉。
一抹灰尘,手上黑漆漆的
折腾了半个小时,总算洗干净了,接下来是抹硅脂
这硅脂还是一九年买的,小日子的信越X-23-7868-2D,导热效果还不错,不过这种产品很虚,需要时间来验证
涂了不少,想压住它的病症,但心里知道,这些努力也只是苟延残喘。机器,终究是要坏的
螺丝虽然滑丝了,但还勉强拧得上。我想,我该买些新螺丝,以免将来它彻底罢工
一次点亮。这台笔记本跟了我好多年,从上学时就陪着我,现在还能跑动。除了渲染不给力,写代码倒是没问题。
我觉得,电脑这东西,特别是Windows系统,得学会一些优化技巧。不然,即使配置再高,也会卡顿,这和底层设计脱不开关系,和Android有异曲同工之处。
相比之下,我更喜欢Linux。之前用Manjaro Linux做主力机有四年时间。不过,Linux的图形界面BUG让我很头疼,几乎每个版本都有各种关于GUI的BUG。刚接触Linux时,最让我害怕的就是系统更新。说到这里,不得不提一下王垠
作为一个爱好骑行的博主,总觉得博客里少了点什么,骑行骑行的,怎么能没有一个专门的骑行数据展示页呢
在设计这个页面的时候,参考了许多骑行APP,然而,国内的骑行数据页面设计真的是一言难尽…
我骑行看数据用Strava多一些,但是它的PC端交互体验,实在不敢苟同。除了用APP版本,我几乎不会去它的网页。不得不说,国外这些骑行数据端做的确实很到位,我个人觉得数据分析方面Strava比Garmin要好!
老规矩,先放目录结构。由于网站的主样式文件main压缩后都超160kb了,为了避免堵塞加载,新开辟一条生产线
说起SCSS,还是受Fooleap的启发才接触到的,我非常喜欢这种方式,它允许嵌套CSS,让代码更加模块化、结构化,还支持变量、继承。比起传统CSS那真是有过之而无不及啊!
Blog
├─assets
│ cycling.min.css
│ cycling.min.js
│
├─pages
│ cycling.html
│
└─src
│ cycling.js
│ main.js
│
├─cycling
│ cycling.scss
│ _bar-chart.scss
│ _base.scss
│ _calendar.scss
│ _message-box.scss
│ _sports.scss
│
└─sass
目前所有的逻辑都在这一个文件里完成,现在的功能还是个雏,因为没有打通Strava api,JSON数据是我手搓的..最近一直在搞Strava api,有好大哥懂吗?它们现在限制了每小时的请求次数,我本来就是半吊子水平,现在是雪上加霜
import './cycling/cycling.scss';
// 为了数据的统一性,generateCalendar处理后赋值供全局使用
let processedActivities = [];
// 日历
function generateCalendar(activities, startDate, numWeeks) {
const calendarElement = document.getElementById('calendar');
calendarElement.innerHTML = '';
const daysOfWeek = ['一', '二', '三', '四', '五', '六', '日'];
daysOfWeek.forEach(day => {
const dayElement = document.createElement('div');
dayElement.className = 'calendar-week-header';
dayElement.innerText = day;
calendarElement.appendChild(dayElement);
});
const todayStr = getChinaTime().toISOString().split('T')[0];
// 起始日期
let currentDate = new Date(startDate);
processedActivities = [];
// 创建日历
function createDayContainer(date, activities) {
const dayContainer = document.createElement('div');
dayContainer.className = 'day-container';
const dateNumber = document.createElement('span');
dateNumber.className = 'date-number';
dateNumber.innerText = date.getDate();
dayContainer.appendChild(dateNumber);
const activity = activities.find(activity => activity.activity_time === date.toISOString().split('T')[0]);
if (activity) processedActivities.push(activity);
// 根据骑行距离设置球的大小
const ballSize = activity ? Math.min(parseFloat(activity.riding_distance) / 4, 24) : 2;
const ball = document.createElement('div');
ball.className = 'activity-indicator';
ball.style.width = `${ballSize}px`;
ball.style.height = `${ballSize}px`;
if (!activity) ball.classList.add('no-activity');
ball.style.left = '50%';
ball.style.top = '50%';
dayContainer.appendChild(ball);
dayContainer.addEventListener('mouseenter', () => {
dateNumber.style.opacity = '1';
ball.style.opacity = '0';
});
dayContainer.addEventListener('mouseleave', () => {
dateNumber.style.opacity = '0';
ball.style.opacity = '1';
});
// 今天日期和球的颜色
if (date.toDateString() === new Date().toDateString()) {
dayContainer.classList.add('today');
ball.style.backgroundColor = '#2ea9df';
dateNumber.style.color = '#2ea9df';
}
return dayContainer;
}
// 异步显示,模仿打字机效果
async function displayCalendar() {
for (let week = 0; week < numWeeks; week++) {
for (let day = 0; day < 7; day++) {
const currentDateStr = currentDate.toISOString().split('T')[0];
// 不再计算超过今天的日期
if (currentDateStr > todayStr) return;
const dayContainer = createDayContainer(currentDate, activities);
calendarElement.appendChild(dayContainer);
// 速度
await new Promise(resolve => setTimeout(resolve, 30));
currentDate.setDate(currentDate.getDate() + 1);
}
}
}
displayCalendar().then(() => {
generateBarChart();
displayTotalActivities();
});
}
// 柱形图
function generateBarChart() {
const barChartElement = document.getElementById('barChart');
barChartElement.innerHTML = '';
const today = getChinaTime();
const startDate = getStartDate(today, 21);
// 每周数据
const weeklyData = {};
// 每周总活动时间
processedActivities.forEach(activity => {
const activityDate = new Date(activity.activity_time);
const weekStart = getWeekStartDate(activityDate);
const weekEnd = new Date(weekStart);
weekEnd.setDate(weekStart.getDate() + 6);
const weekKey = `${weekStart.toISOString().split('T')[0]} - ${weekEnd.toISOString().split('T')[0]}`;
weeklyData[weekKey] = (weeklyData[weekKey] || 0) + convertToHours(activity.moving_time);
});
// 最大时间
const maxTime = Math.max(...Object.values(weeklyData), 0);
// 创建柱形图
Object.keys(weeklyData).forEach(week => {
const barContainer = document.createElement('div');
barContainer.className = 'bar-container';
const bar = document.createElement('div');
bar.className = 'bar';
const width = (weeklyData[week] / maxTime) * 190;
bar.style.setProperty('--bar-width', `${width}px`);
const durationText = document.createElement('div');
durationText.className = 'bar-duration';
durationText.innerText = '0h';
const messageBox = createMessageBox();
const clickMessageBox = createMessageBox();
barContainer.style.position = 'relative';
bar.appendChild(durationText);
barContainer.appendChild(bar);
barContainer.appendChild(messageBox);
barContainer.appendChild(clickMessageBox);
barChartElement.appendChild(barContainer);
bar.style.width = '0';
bar.offsetHeight;
// 动画效果
bar.style.transition = 'width 1s ease-out';
bar.style.width = `${width}px`;
durationText.style.opacity = '1';
// 动态文本
animateText(durationText, 0, weeklyData[week], 1000);
setupBarInteractions(bar, messageBox, clickMessageBox, weeklyData[week]);
});
}
// 动态文本显示
function animateText(element, startValue, endValue, duration) {
const startTime = performance.now();
function update() {
const elapsed = performance.now() - startTime;
const progress = Math.min(elapsed / duration, 1);
const currentValue = Math.floor(progress * endValue);
element.innerText = `${currentValue}h`;
if (progress < 1) {
requestAnimationFrame(update);
} else {
element.innerText = `${endValue.toFixed(1)}h`;
}
}
update();
}
// 计算总公里数
function calculateTotalKilometers(activities) {
return activities.reduce((total, activity) => total + parseFloat(activity.riding_distance) || 0, 0);
}
// 显示总活动数和总公里数
function displayTotalActivities() {
const totalCountElement = document.getElementById('totalCount');
const totalDistanceElement = document.getElementById('totalDistance');
if (!totalCountElement || !totalDistanceElement) return;
const totalCountValue = totalCountElement.querySelector('#totalCountValue');
const totalDistanceValue = totalDistanceElement.querySelector('#totalDistanceValue');
const totalCountSpinner = totalCountElement.querySelector('.loading-spinner');
const totalDistanceSpinner = totalDistanceElement.querySelector('.loading-spinner');
totalCountSpinner.classList.add('active');
totalDistanceSpinner.classList.add('active');
const uniqueDays = new Set(processedActivities.map(activity => activity.activity_time));
const totalCount = uniqueDays.size;
const totalKilometers = calculateTotalKilometers(processedActivities);
animateCount(totalCountValue, totalCount, 1000, 50);
animateCount(totalDistanceValue, totalKilometers, 1000, 50, true);
setTimeout(() => {
totalDistanceValue.textContent = `${totalKilometers.toFixed(2)} km`;
totalCountSpinner.classList.remove('active');
totalDistanceSpinner.classList.remove('active');
}, 1000);
}
// 获取一周的开始日期
function getWeekStartDate(date) {
const day = date.getDay();
const diff = (day === 0 ? -6 : 1) - day;
const weekStart = new Date(date);
weekStart.setDate(weekStart.getDate() + diff);
return weekStart;
}
// 将JSON的时间数据转换为小时
function convertToHours(moving_time) {
const [hours, minutes] = moving_time.split(':').map(Number);
return hours + (minutes / 60);
}
// 博客托管Github Pages需要中国时间
function getChinaTime() {
const now = new Date();
const offset = 8 * 60 * 60 * 1000;
return new Date(now.getTime() + offset);
}
// 手搓JSON
async function loadActivityData() {
const response = await fetch('XXXXXX');
return response.json();
}
(async function() {
const today = getChinaTime();
const startDate = getStartDate(today, 21);
const activities = await loadActivityData();
generateCalendar(activities, startDate, 4);
})();
// 创建消息盒子
function createMessageBox() {
const messageBox = document.createElement('div');
messageBox.className = 'message-box';
return messageBox;
}
// 获取起始时间
function getStartDate(today, daysOffset) {
const currentDayOfWeek = today.getDay();
const daysToMonday = (currentDayOfWeek === 0 ? 6 : currentDayOfWeek - 1);
const startDate = new Date(today);
startDate.setDate(today.getDate() - daysToMonday - daysOffset);
startDate.setDate(startDate.getDate() - (startDate.getDay() === 0 ? 6 : startDate.getDay() - 1));
return startDate;
}
// 动态更新计数器
function animateCount(element, totalValue, duration, intervalDuration, isDistance = false) {
const step = totalValue / (duration / intervalDuration);
let count = 0;
const interval = setInterval(() => {
count += step;
if (count >= totalValue) {
count = totalValue;
clearInterval(interval);
}
element.textContent = isDistance ? count.toFixed(2) : Math.round(count);
}, intervalDuration);
}
// 骚话集合
function setupBarInteractions(bar, messageBox, clickMessageBox, weeklyData) {
let mouseLeaveTimeout;
let autoHideTimeout;
bar.addEventListener('mouseenter', () => {
clearTimeout(mouseLeaveTimeout);
clearTimeout(autoHideTimeout);
const message = weeklyData > 14 ? '这周干的还不错' : '偷懒了啊';
messageBox.innerText = message;
messageBox.classList.add('show');
autoHideTimeout = setTimeout(() => {
messageBox.classList.remove('show');
}, 700);
});
bar.addEventListener('mouseleave', () => {
mouseLeaveTimeout = setTimeout(() => {
messageBox.classList.remove('show');
}, 700);
});
bar.addEventListener('click', () => {
clickMessageBox.innerText = '一起来运动吧!';
clickMessageBox.classList.add('show');
setTimeout(() => {
clickMessageBox.classList.remove('show');
}, 700);
messageBox.classList.remove('show');
clearTimeout(mouseLeaveTimeout);
clearTimeout(autoHideTimeout);
});
}
骑行统计页面不会止步于此,接下来还会有很大的延申改动,我提前把变量接口留好了,定义了一些主样式变量,SCSS模块化继承了一些基础样式,二次开发会轻松很多
// 总次数和总距离字体
$primary-color: #2ea9df;
// 柱状图字体
$gray-color: #333;
// 柱状图颜色
$light-gray-color: #EBE6F2;
// 柱状图边框
$light-gray-border-color: #DFD7E9;
// 未活动日历
$no-activity-color: gray;
//------ 分类色
// 公路车
$cycling-color: #EBE6F2;
$cycling-border-color: #DFD7E9;
// 跑步
$running-color: #D5E5D3;
$running-border-color: #BDD6BA;
// 背景和文本颜色
$background-color: #333;
$text-color: #fff;
@import 'base';
@import 'calendar';
@import 'bar-chart';
@import 'sports';
@import 'message-box';
html和scss没啥好看的,配置一下收工
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
mode: 'production',
entry: {
main: path.resolve(__dirname, 'src/main.js'),
cycling: path.resolve(__dirname, 'src/cycling.js'),
},
output: {
path: path.resolve(__dirname, 'assets'),
filename: '[name].min.js',
publicPath: '/'
},
stats: {
entrypoints: false,
children: false
},
module: {
rules: [
{
test: /\.(scss|css)$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'postcss-loader',
'sass-loader'
]
},
{
test: /\.html$/,
use: ['html-loader']
}
],
},
resolve: {
alias: {
'iDisqus.css': 'disqus-php-api/dist/iDisqus.css',
}
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].min.css'
})
],
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
parallel: true
}),
new CssMinimizerPlugin()
],
}
};
Fooleap的博客真的是相当不错,我特别喜欢他写的Jekyll主题,还有很大的折腾空间,比如全站PJAX、懒加载等等
这一周,我也着手用JQuery重新了写整站,完事后感觉真傻逼了,属于画蛇添足,多此一举。毕竟小站点,拖着一个磨盘挺累的。不上国内服务器的话,原生这条路死磕到底了,不过PJAX是必须要上的,预计下星期全站PJAX、懒加载上线
在刷博客的时候,最麻烦的事情之一就是手动填写各种表单。为了提高我的冲浪体验,诞生了这款表单自动化插件。经过爬虫上百次调教,兼容95%博客,另外5%的网站正常人写不出来,autocomplete小伎俩都上不了台面,各种防止逆向、防调试测试,心累。
插件纯绿色,无隐私可言。除images外,全部资源和代码文件都经过Webpack打包,下面是项目的目录结构以及各部分的说明:
Form-automation-plugin
│ index.html
│ LICENSE
│ manifest.json
│ package-lock.json
│ package.json
│ README.md
│ webpack.config.js
│
├─dist
│ 33a80cb13f78b37acb39.woff2
│ 8093dd36a9b1ec918992.ttf
│ 8521c461ad88443142d9.woff
│ autoFill.min.js
│ eventHandler.min.js
│ formManager.min.js
│ main.min.css
│
└─src
│ autoFill.js
│ eventHandler.js
│ formManager.js
│ template.css
│ template.html
│
├─fonts
│ iconfont.css
│ iconfont.ttf
│ iconfont.woff
│ iconfont.woff2
│
└─images
Appreciation-code.jpg
icon128.png
icon16.png
icon48.png
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: {
autoFill: './src/autoFill.js',
eventHandler: './src/eventHandler.js',
formManager: './src/formManager.js',
},
output: {
filename: '[name].min.js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
mode: 'production',
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
],
},
],
},
plugins: [
new MiniCssExtractPlugin({
filename: 'main.min.css',
}),
new HtmlWebpackPlugin({
template: path.resolve(__dirname, 'src', 'template.html'),
filename: '../index.html',
inject: 'body',
}),
],
resolve: {
extensions: ['.js', '.css'],
},
};
// autoFill.js文件是插件的最重要的核心模块,涉及到了插件的主要输出功能
// 遍历当前页面所有input,将autocomplete值设置为on,监听Textarea输入时触发
function handleAutocomplete() {
const inputs = document.querySelectorAll('input');
inputs.forEach(input => {
const autocompleteAttr = input.getAttribute('autocomplete');
if (autocompleteAttr) {
input.setAttribute('autocomplete', 'on');
} else {
input.setAttribute('autocomplete', 'on');
}
});
}
// 这个函数有些臃肿,马上要去骑车,懒得搞了,现在的逻辑已经完善到9成了,大多数意外情况都卷了进去,但是一些防逆向防调试,我暂时无法解决,前端菜鸟,还望大哥指点一二
function fillInputFields() {
chrome.storage.sync.get(["name", "email", "url"], (data) => {
// console.log(data);
const hasValidName = data.name !== undefined && data.name !== "";
const hasValidEmail = data.email !== undefined && data.email !== "";
const hasValidUrl = data.url !== undefined && data.url !== "";
// 关键字
const nameKeywords = [
"name", "author", "display_name", "full-name", "username", "nick", "displayname",
"first-name", "last-name", "full name", "real-name", "given-name",
"family-name", "user-name", "pen-name", "alias", "name-field", "displayname"
];
const emailKeywords = [
"email", "mail", "contact", "emailaddress", "mailaddress",
"email-address", "mail-address", "email-addresses", "mail-addresses",
"emailaddresses", "mailaddresses", "contactemail", "useremail",
"contact-email", "user-mail"
];
const urlKeywords = [
"url", "link", "website", "homepage", "site", "web", "address",
"siteurl", "webaddress", "homepageurl", "profile", "homepage-link"
];
const inputs = document.querySelectorAll("input, textarea");
inputs.forEach((input) => {
const typeAttr = input.getAttribute("type")?.toLowerCase() || "";
const nameAttr = input.getAttribute("name")?.toLowerCase() || "";
let valueToSet = "";
// 处理 URL
if (urlKeywords.some(keyword => nameAttr.includes(keyword))) {
if (hasValidUrl) {
valueToSet = data.url;
}
}
// 处理邮箱
else if (emailKeywords.some(keyword => nameAttr.includes(keyword))) {
if (hasValidEmail) {
valueToSet = data.email;
}
}
// 处理名称
else if (nameKeywords.some(keyword => nameAttr.includes(keyword))) {
if (hasValidName) {
valueToSet = data.name;
}
}
// 处理没有 type 属性或者 type 为 text 的情况
if ((typeAttr === "" || typeAttr === "text") && valueToSet === "") {
if (nameAttr && nameKeywords.some(keyword => nameAttr.includes(keyword))) {
if (hasValidName) {
valueToSet = data.name;
}
} else if (nameAttr && urlKeywords.some(keyword => nameAttr.includes(keyword))) {
if (hasValidUrl) {
valueToSet = data.url;
}
}
}
// 处理 type 为 email
if (typeAttr === "email" && valueToSet === "") {
if (nameAttr && emailKeywords.some(keyword => nameAttr.includes(keyword))) {
if (hasValidEmail) {
valueToSet = data.email;
}
}
}
// 处理 type 为 url
if (typeAttr === "url" && valueToSet === "") {
if (nameAttr && urlKeywords.some(keyword => nameAttr.includes(keyword))) {
if (hasValidUrl) {
valueToSet = data.url;
}
}
}
// 填充输入字段
if (valueToSet !== "") {
input.value = valueToSet;
}
});
});
}
function clearInputFields() {
const inputs = document.querySelectorAll("input");
inputs.forEach((input) => {
const typeAttr = input.getAttribute("type")?.toLowerCase();
if (typeAttr === "text" || typeAttr === "email") {
input.value = "";
}
});
}
// 监听 textarea 标签的输入事件
document.addEventListener("input", (event) => {
if (event.target.tagName.toLowerCase() === "textarea") {
handleAutocomplete();
fillInputFields();
}
});
该文件负责向Chrome本地存储和修改,就CURD,没啥含量
import './fonts/iconfont.css';
import './template.css';
document.getElementById("save").addEventListener("click", () => {
const saveButton = document.getElementById("save");
if (saveButton.textContent === "更改") {
unlockInputFields();
changeButtonText("保存");
return;
}
const name = document.getElementById("name").value.trim();
const email = document.getElementById("email").value.trim();
const url = document.getElementById("url").value.trim();
// 验证邮箱格式的正则表达式
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (name === "" || email === "") {
alert("请填写必填字段:姓名和邮箱!");
return;
}
if (!emailPattern.test(email)) {
alert("请输入有效的邮箱地址!");
return;
}
// 从 Chrome 存储中读取当前的值
chrome.storage.sync.get(["name", "email", "url"], (data) => {
const isNameAndEmailChanged = name !== data.name || email !== data.email;
const isUrlChanged = url !== data.url;
if (isNameAndEmailChanged || isUrlChanged) {
chrome.storage.sync.set({ name, email, url }, () => {
lockInputFields();
changeButtonText("更改");
});
} else {
lockInputFields();
changeButtonText("更改");
}
});
});
// 页面加载完成时执行
document.addEventListener("DOMContentLoaded", () => {
chrome.storage.sync.get(["name", "email", "url"], (data) => {
document.getElementById("name").value = data.name || "";
document.getElementById("email").value = data.email || "";
document.getElementById("url").value = data.url || "";
if (data.name || data.email || data.url) {
lockInputFields();
changeButtonText("更改");
}
});
const menuItems = document.querySelectorAll('.dl-menu li a');
const tabContents = document.querySelectorAll('.tab-content');
menuItems.forEach(menuItem => {
menuItem.addEventListener('click', (event) => {
event.preventDefault();
tabContents.forEach(tab => tab.classList.remove('active'));
const targetId = menuItem.getAttribute('href').substring(1);
document.getElementById(targetId).classList.add('active');
menuItems.forEach(item => item.parentElement.classList.remove('active'));
menuItem.parentElement.classList.add('active');
});
});
});
// 锁定输入框
function lockInputFields() {
document.getElementById("name").setAttribute("disabled", "true");
document.getElementById("email").setAttribute("disabled", "true");
document.getElementById("url").setAttribute("disabled", "true");
}
// 解锁输入框
function unlockInputFields() {
document.getElementById("name").removeAttribute("disabled");
document.getElementById("email").removeAttribute("disabled");
document.getElementById("url").removeAttribute("disabled");
}
// 更改按钮文本
function changeButtonText(text) {
document.getElementById("save").textContent = text;
}
git clone到本地,浏览器打开:chrome://extensions/,加载已解压的扩展程序
由于我没有注册Chrome应用商店开发者,目前只能本地运行,过几天上线应用商店,Tampermonkey等骑车回来再做
Form-automation-plugin:https://github.com/achuanya/Form-automation-plugin
今天是七月的最后一天,晚上必须来一次有氧小长途骑行,目标暂且定为100公里
出发前,先泡两瓶蛋白粉放进冰箱,一瓶550ML,一瓶750ML,我还是觉得不够用。骑车过程中不想下来,容易打断节奏。我打算坐尾再安装一个支架放一瓶750ML,只要室外温度不是特变态,百里油耗三瓶水没有问题。
检查一下前变、后拨、夹气和胎压。前胎由于自补液在气嘴处凝固,导致无法打气,不过目前胎压足够,不影响骑行,估计再骑一周胎压就不行了,由于管胎的特殊结构,我还没有合适的解决方案,除了换胎。
下午喝了两碗绿豆粥,吃了些核桃饼,开始做最后的准备,带了两件便携式螺丝刀,一小包纸巾,一包干湿巾,一包电解质盐丸,这些正好放进后尾包,占了大概60%空间,剩余空间还能放盒烟。
衣服就没啥好挑的,穿条ASSOS背带裤和速白干背心就行了,如果不是为了注意个人形象,我直接背带裤光膀子。在上海生活的时候也特热,多次骑行都是背带裤光膀子,有两次路过外滩被交警抓了,说外滩都是游客,我个人形象影响市容。
到地方了,这骑行路段是挺不错的,我有七年没来这里了,今天来到这里发现大变样了,还可以租皮划艇,改天一定玩玩。在这里绕圈骑了十五公里,又跑去周口公园绕圈,要说这夏天油耗确实高,机动车顶不住,人也顶不住啊!我的水有点不够了,今天才33°,河南的热和江浙沪的热真是不一样,回家后一直不适应,出公园后去蜜雪冰城买了一杯柠檬水,找店里的小妹妹白嫖了两瓶冰水,直奔淮阳区·龙湖
到龙湖后里程已经到了60公里,这时天也暗了,有些许疲惫,主要是颈椎疼,下把位骑多了。吃了两片盐丸,两手握把立边缘慢骑摆烂二十分钟,这个时候一定不能下来歇,推车都不行,下车体力直接归零,不知道别人咋样,我是这样的,与自己较劲,100公里?200公里又算个屁,出来了,就要干
在龙湖绕了三圈后达标,真是饿得不行了。到家已经十点,浑身湿透,黏糊糊的,洗了澡,洗了衣服,然后躺下看订阅,期待八月的骑行。
说起这事,还是受一位博友的启发“1900”他的左邻右舍页面很棒,决定模仿一下。我平时也用 Inoreader,但我还是喜欢直接打开博客的感觉,心血来潮,搞。
起初,我打算使用 COS 和 GitHub Actions,但在测试过程中发现 GitHub 的延迟非常高,验证和文件写入速度极慢,频频失败。干脆直接上 GitHub 自产自销。
main()
│
├── readFeedsFromGitHub()
│ ├── GitHub API 调用
│ │ ├── 读取 rss_feeds.txt 文件
│ │ └── 处理文件报错
│ └── Return
│
├── fetchRSS()
│ ├── 遍历 RSS
│ │ ├── HTTP GET 请求
│ │ └── 处理请求错误
│ ├── 解析 RSS
│ │ ├── 清理 XML 内容中的非法字符
│ │ ├── 提取域名
│ │ └── 格式化并排序
│ └── Return
│
└── saveToGitHub()
├── GitHub API 调用
│ ├── 保存到 _data/rss_data.json 供 Jekyll 调用
│ └── 处理错误
└── Return
由于用 Go 搬砖,所有的包、类型和方法均可在 GitHub API 客户端库的第 39 版文档查询
关于 Github API 有一点需要注意,配置好环境变量后,Token 操作仓库需要有一定的权限,务必启用 Read and write permissions 读取和写入权限
go mod init github.com/achuanya/Grab-latest-RSS
// Go-GitHub v39
go get github.com/google/go-github/v39/github
// RSS 和 Atom feeds 解析库
go get github.com/mmcdole/gofeed
// OAuth2 认证和授权
go get golang.org/x/oauth2
package main
import (
"bufio"
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"os"
"regexp"
"sort"
"sync"
"time"
"github.com/google/go-github/v39/github"
"github.com/mmcdole/gofeed"
"golang.org/x/oauth2"
)
const (
maxRetries = 3 // 最大重试次数
retryInterval = 10 * time.Second // 重试间隔时间
)
type Config struct {
GithubToken string // GitHub API 令牌
GithubName string // GitHub 用户名
GithubRepository string // GitHub 仓库名
}
// 用于解析 avatar_data.json 文件的结构
type Avatar struct {
Name string `json:"name"` // 用户名
Avatar string `json:"avatar"` // 头像 URL
}
// 爬虫抓取的数据结构
type Article struct {
DomainName string `json:"domainName"` // 域名
Name string `json:"name"` // 博客名称
Title string `json:"title"` // 文章标题
Link string `json:"link"` // 文章链接
Date string `json:"date"` // 格式化后的文章发布时间
Avatar string `json:"avatar"` // 头像 URL
}
// 初始化并返回配置信息
func initConfig() Config {
return Config{
GithubToken: os.Getenv("TOKEN"), // 从环境变量中获取 GitHub API 令牌
GithubName: "achuanya", // GitHub 用户名
GithubRepository: "lhasa.github.io", // GitHub 仓库名
}
}
// 清理 XML 内容中的非法字符
func cleanXMLContent(content string) string {
re := regexp.MustCompile(`[\x00-\x1F\x7F-\x9F]`)
return re.ReplaceAllString(content, "")
}
// 尝试解析不同格式的时间字符串
func parseTime(timeStr string) (time.Time, error) {
formats := []string{
time.RFC3339,
time.RFC3339Nano,
time.RFC1123Z,
time.RFC1123,
}
for _, format := range formats {
if t, err := time.Parse(format, timeStr); err == nil {
return t, nil
}
}
return time.Time{}, fmt.Errorf("unable to parse time: %s", timeStr)
}
// 将时间格式化为 "January 2, 2006"
func formatTime(t time.Time) string {
return t.Format("January 2, 2006")
}
// 从 URL 中提取域名,并添加 https:// 前缀
func extractDomain(urlStr string) (string, error) {
u, err := url.Parse(urlStr)
if err != nil {
return "", err
}
domain := u.Hostname()
protocol := "https://"
if u.Scheme != "" {
protocol = u.Scheme + "://"
}
fullURL := protocol + domain
return fullURL, nil
}
// 获取当前的北京时间
func getBeijingTime() time.Time {
beijingTimeZone := time.FixedZone("CST", 8*3600)
return time.Now().In(beijingTimeZone)
}
// 记录错误信息到 error.log 文件
func logError(config Config, message string) {
logMessage(config, message, "error.log")
}
// 记录信息到指定的文件
func logMessage(config Config, message string, fileName string) {
ctx := context.Background()
client := github.NewClient(oauth2.NewClient(ctx, oauth2.StaticTokenSource(&oauth2.Token{
AccessToken: config.GithubToken,
})))
filePath := "_data/" + fileName
fileContent := []byte(message + "\n\n")
file, _, resp, err := client.Repositories.GetContents(ctx, config.GithubName, config.GithubRepository, filePath, nil)
if err != nil && resp.StatusCode == http.StatusNotFound {
_, _, err := client.Repositories.CreateFile(ctx, config.GithubName, config.GithubRepository, filePath, &github.RepositoryContentFileOptions{
Message: github.String("Create " + fileName),
Content: fileContent,
Branch: github.String("master"),
})
if err != nil {
fmt.Printf("error creating %s in GitHub: %v\n", fileName, err)
}
return
} else if err != nil {
fmt.Printf("error checking %s in GitHub: %v\n", fileName, err)
return
}
decodedContent, err := file.GetContent()
if err != nil {
fmt.Printf("error decoding %s content: %v\n", fileName, err)
return
}
updatedContent := append([]byte(decodedContent), fileContent...)
_, _, err = client.Repositories.UpdateFile(ctx, config.GithubName, config.GithubRepository, filePath, &github.RepositoryContentFileOptions{
Message: github.String("Update " + fileName),
Content: updatedContent,
SHA: github.String(*file.SHA),
Branch: github.String("master"),
})
if err != nil {
fmt.Printf("error updating %s in GitHub: %v\n", fileName, err)
}
}
// 从 GitHub 仓库中获取 JSON 文件内容
func fetchFileFromGitHub(config Config, filePath string) (string, error) {
ctx := context.Background()
client := github.NewClient(oauth2.NewClient(ctx, oauth2.StaticTokenSource(&oauth2.Token{
AccessToken: config.GithubToken,
})))
file, _, resp, err := client.Repositories.GetContents(ctx, config.GithubName, config.GithubRepository, filePath, nil)
if err != nil {
if resp.StatusCode == http.StatusNotFound {
return "", fmt.Errorf("file not found: %s", filePath)
}
return "", fmt.Errorf("error fetching file %s from GitHub: %v", filePath, err)
}
content, err := file.GetContent()
if err != nil {
return "", fmt.Errorf("error decoding file %s content: %v", filePath, err)
}
return content, nil
}
// 从 GitHub 仓库中读取头像配置
func loadAvatarsFromGitHub(config Config) (map[string]string, error) {
content, err := fetchFileFromGitHub(config, "_data/avatar_data.json")
if err != nil {
return nil, err
}
var avatars []Avatar
if err := json.Unmarshal([]byte(content), &avatars); err != nil {
return nil, err
}
avatarMap := make(map[string]string)
for _, a := range avatars {
avatarMap[a.Name] = a.Avatar
}
return avatarMap, nil
}
// 从 RSS 列表中抓取最新的文章,并按发布时间排序
func fetchRSS(config Config, feeds []string) ([]Article, error) {
var articles []Article
var mu sync.Mutex // 用于保证并发安全
var wg sync.WaitGroup // 用于等待所有 goroutine 完成
avatars, err := loadAvatarsFromGitHub(config)
if err != nil {
logError(config, fmt.Sprintf("[%s] [Load avatars error] %v", getBeijingTime().Format("Mon Jan 2 15:04:2006"), err))
return nil, err
}
fp := gofeed.NewParser()
httpClient := &http.Client{
Timeout: 10 * time.Second,
}
for _, feedURL := range feeds {
wg.Add(1)
go func(feedURL string) {
defer wg.Done()
var resp *http.Response
var bodyString string
var fetchErr error
for i := 0; i < maxRetries; i++ {
resp, fetchErr = httpClient.Get(feedURL)
if fetchErr == nil {
bodyBytes := new(bytes.Buffer)
bodyBytes.ReadFrom(resp.Body)
bodyString = bodyBytes.String()
resp.Body.Close()
break
}
logError(config, fmt.Sprintf("[%s] [Get RSS error] %s: Attempt %d/%d: %v", getBeijingTime().Format("Mon Jan 2 15:04:2006"), feedURL, i+1, maxRetries, fetchErr))
time.Sleep(retryInterval)
}
if fetchErr != nil {
logError(config, fmt.Sprintf("[%s] [Failed to fetch RSS] %s: %v", getBeijingTime().Format("Mon Jan 2 15:04:2006"), feedURL, fetchErr))
return
}
cleanBody := cleanXMLContent(bodyString)
var feed *gofeed.Feed
var parseErr error
for i := 0; i < maxRetries; i++ {
feed, parseErr = fp.ParseString(cleanBody)
if parseErr == nil {
break
}
logError(config, fmt.Sprintf("[%s] [Parse RSS error] %s: Attempt %d/%d: %v", getBeijingTime().Format("Mon Jan 2 15:04:2006"), feedURL, i+1, maxRetries, parseErr))
time.Sleep(retryInterval)
}
if parseErr != nil {
logError(config, fmt.Sprintf("[%s] [Failed to parse RSS] %s: %v", getBeijingTime().Format("Mon Jan 2 15:04:2006"), feedURL, parseErr))
return
}
mainSiteURL := feed.Link
domainName, err := extractDomain(mainSiteURL)
if err != nil {
logError(config, fmt.Sprintf("[%s] [Extract domain error] %s: %v", getBeijingTime().Format("Mon Jan 2 15:04:2006"), mainSiteURL, err))
domainName = "unknown"
}
name := feed.Title
avatarURL := avatars[name]
if avatarURL == "" {
avatarURL = "https://cos.lhasa.icu/LinksAvatar/default.png"
}
if len(feed.Items) > 0 {
item := feed.Items[0]
publishedTime, err := parseTime(item.Published)
if err != nil && item.Updated != "" {
publishedTime, err = parseTime(item.Updated)
}
if err != nil {
logError(config, fmt.Sprintf("[%s] [Getting article time error] %s: %v", getBeijingTime().Format("Mon Jan 2 15:04:2006"), item.Title, err))
publishedTime = time.Now()
}
originalName := feed.Title
// 该长的地方短,该短的地方长
nameMapping := map[string]string{
"obaby@mars": "obaby",
"青山小站 | 一个在帝都搬砖的新时代农民工": "青山小站",
"Homepage on Miao Yu | 于淼": "于淼",
"Homepage on Yihui Xie | 谢益辉": "谢益辉",
}
validNames := make(map[string]struct{})
for key := range nameMapping {
validNames[key] = struct{}{}
}
_, valid := validNames[originalName]
if !valid {
for key := range validNames {
if key == originalName {
logError(config, fmt.Sprintf("[%s] [Name mapping not found] %s", getBeijingTime().Format("Mon Jan 2 15:04:2006"), originalName))
break
}
}
} else {
name = nameMapping[originalName]
}
mu.Lock()
articles = append(articles, Article{
DomainName: domainName,
Name: name,
Title: item.Title,
Link: item.Link,
Avatar: avatarURL,
Date: formatTime(publishedTime),
})
mu.Unlock()
}
}(feedURL)
}
wg.Wait()
sort.Slice(articles, func(i, j int) bool {
date1, _ := time.Parse("January 2, 2006", articles[i].Date)
date2, _ := time.Parse("January 2, 2006", articles[j].Date)
return date1.After(date2)
})
return articles, nil
}
// 将爬虫抓取的数据保存到 GitHub
func saveToGitHub(config Config, data []Article) error {
ctx := context.Background()
client := github.NewClient(oauth2.NewClient(ctx, oauth2.StaticTokenSource(&oauth2.Token{
AccessToken: config.GithubToken,
})))
manualArticles := []Article{
{
DomainName: "https://foreverblog.cn",
Name: "十年之约",
Title: "穿梭虫洞-随机访问十年之约友链博客",
Link: "https://foreverblog.cn/go.html",
Date: "January 01, 2000",
Avatar: "https://cos.lhasa.icu/LinksAvatar/foreverblog.cn.png",
},
{
DomainName: "https://www.travellings.cn",
Name: "开往",
Title: "开往-友链接力",
Link: "https://www.travellings.cn/go.html",
Date: "January 01, 2000",
Avatar: "https://cos.lhasa.icu/LinksAvatar/www.travellings.png",
},
}
data = append(data, manualArticles...)
jsonData, err := json.Marshal(data)
if err != nil {
return err
}
filePath := "_data/rss_data.json"
file, _, resp, err := client.Repositories.GetContents(ctx, config.GithubName, config.GithubRepository, filePath, nil)
if err != nil && resp.StatusCode == http.StatusNotFound {
_, _, err := client.Repositories.CreateFile(ctx, config.GithubName, config.GithubRepository, filePath, &github.RepositoryContentFileOptions{
Message: github.String("Create rss_data.json"),
Content: jsonData,
Branch: github.String("master"),
})
if err != nil {
return fmt.Errorf("error creating rss_data.json in GitHub: %v", err)
}
return nil
} else if err != nil {
return fmt.Errorf("error checking rss_data.json in GitHub: %v", err)
}
_, _, err = client.Repositories.UpdateFile(ctx, config.GithubName, config.GithubRepository, filePath, &github.RepositoryContentFileOptions{
Message: github.String("Update rss_data.json"),
Content: jsonData,
SHA: github.String(*file.SHA),
Branch: github.String("master"),
})
if err != nil {
return fmt.Errorf("error updating rss_data.json in GitHub: %v", err)
}
return nil
}
// 从 GitHub 仓库中获取 RSS 文件
func readFeedsFromGitHub(config Config) ([]string, error) {
ctx := context.Background()
client := github.NewClient(oauth2.NewClient(ctx, oauth2.StaticTokenSource(&oauth2.Token{
AccessToken: config.GithubToken,
})))
filePath := "_data/rss_feeds.txt"
file, _, resp, err := client.Repositories.GetContents(ctx, config.GithubName, config.GithubRepository, filePath, nil)
if err != nil && resp.StatusCode == http.StatusNotFound {
errMsg := fmt.Sprintf("Error: %s not found in GitHub repository", filePath)
logError(config, fmt.Sprintf("[%s] [Read RSS file error] %v", getBeijingTime().Format("Mon Jan 2 15:04:2006"), errMsg))
return nil, fmt.Errorf(errMsg)
} else if err != nil {
errMsg := fmt.Sprintf("Error fetching %s from GitHub: %v", filePath, err)
logError(config, fmt.Sprintf("[%s] [Read RSS file error] %v", getBeijingTime().Format("Mon Jan 2 15:04:2006"), errMsg))
return nil, fmt.Errorf(errMsg)
}
content, err := file.GetContent()
if err != nil {
errMsg := fmt.Sprintf("Error decoding %s content: %v", filePath, err)
logError(config, fmt.Sprintf("[%s] [Read RSS file error] %v", getBeijingTime().Format("Mon Jan 2 15:04:2006"), errMsg))
return nil, fmt.Errorf(errMsg)
}
var feeds []string
scanner := bufio.NewScanner(bytes.NewReader([]byte(content)))
for scanner.Scan() {
feeds = append(feeds, scanner.Text())
}
if err := scanner.Err(); err != nil {
errMsg := fmt.Sprintf("Error reading RSS file content: %v", err)
logError(config, fmt.Sprintf("[%s] [Read RSS file error] %v", getBeijingTime().Format("Mon Jan 2 15:04:2006"), errMsg))
return nil, fmt.Errorf(errMsg)
}
return feeds, nil
}
func main() {
config := initConfig()
// 从 GitHub 仓库中读取 RSS feeds 列表
rssFeeds, err := readFeedsFromGitHub(config)
if err != nil {
logError(config, fmt.Sprintf("[%s] [Read RSS feeds error] %v", getBeijingTime().Format("Mon Jan 2 15:04:2006"), err))
fmt.Printf("Error reading RSS feeds from GitHub: %v\n", err)
return
}
// 抓取 RSS feeds
articles, err := fetchRSS(config, rssFeeds)
if err != nil {
logError(config, fmt.Sprintf("[%s] [Fetch RSS error] %v", getBeijingTime().Format("Mon Jan 2 15:04:2006"), err))
fmt.Printf("Error fetching RSS feeds: %v\n", err)
return
}
// 将抓取的数据保存到 GitHub 仓库
err = saveToGitHub(config, articles)
if err != nil {
logError(config, fmt.Sprintf("[%s] [Save data to GitHub error] %v", getBeijingTime().Format("Mon Jan 2 15:04:2006"), err))
fmt.Printf("Error saving data to GitHub: %v\n", err)
return
}
fmt.Println("Stop writing code and go ride a road bike now!")
}
[
{
"domainName": "https://yihui.org",
"name": "谢益辉",
"title": "Rd2roxygen",
"link": "https://yihui.org/rd2roxygen/",
"date": "April 14, 2024",
"avatar": "https://cos.lhasa.icu/LinksAvatar/yihui.org.png"
},
{
"domainName": "https://www.laruence.com",
"name": "风雪之隅",
"title": "PHP8.0的Named Parameter",
"link": "https://www.laruence.com/2022/05/10/6192.html",
"date": "May 10, 2022",
"avatar": "https://cos.lhasa.icu/LinksAvatar/www.laruence.com.png"
}
]
[Sat Jul 27 08:42:2024] [Parse RSS error] https://lhasa.icu: Failed to detect feed type
[Sat Jul 27 08:41:2024] [Get RSS error] https://lhasa.icu: Get "https://lhasa.icu": net/http: TLS handshake timeout
name: ScheduledRssRawler
on:
schedule:
- cron: '0 * * * *'
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: '1.22.5'
- name: Install dependencies
run: go mod tidy
working-directory: ./api
- name: Build
run: go build -o main
working-directory: ./api
- name: Run Go program
env:
TOKEN: $
run: ./main
working-directory: ./api
腾讯 COS 也写了一份,Github 有延迟问题就没用,也能用,逻辑上和 Go 是没啥区别
Grab-latest-RSS:https://github.com/achuanya/Grab-latest-RSS
COS Go SDK:https://cloud.tencent.com/document/product/436/31215
看到这张图的时候,我很震惊。这个CDN流量包是我昨天凌晨刚买的,直到此刻才发现我的CDN流量被恶意盗刷了。
事情是这样的,前天23号我在写新功能,本地调试调用了很多资源,当时看到消耗了90G的流量,我没有在意,以为是调试的问题。因为那天我写了一天代码,不停地调用Tencent COS,而COS还套了一层Tencent CDN。当时我以为是正常消耗,眼看流量不够,我又充了一个CDN加速包。
然而就在今晚22:45,我骑行回来关闭了免打扰模式,邮箱忽然弹出通知,腾讯云提示我CDN流量不足?我当时非常震惊,因为这是我24号凌晨刚买的流量包啊!
看到这张图时,我火了,在独立博客圈彻底火了,2天内请求数42万?赶超月光博客!
回想过去,我在博客圈认识的人一只手能数过来,更谈不上得罪谁。这事也怪我,之前COS没有任何防护,几乎处于裸奔状态。
由于我的博客托管在Github Pages,主机问题大可不必考虑,我能做的只有设置黑白名单和周期限流。
不再裸奔,已老实。
知道怎么回事了,24年后,大陆境内出现一窝狗,利用PCDN恶意流量攻击!
攻击的主要IP来源于山西、江苏和安徽联通等地的固定网段
攻击时间非常规律,集中在19:50到23:00之间
攻击者会针对体积较大的静态文件进行持续攻击
自7月初以来,已转头无差别地对大陆中小型网站展开攻击。
建议将山西等地的IP段暂时屏蔽,减少恶意流量的影响。
目前,GitHub上已经有相关项目 ban-pcdn-ip 用于收集这些恶意IP段。
管胎被扎了,还是后轮,我心如刀绞啊,太贵了,换不起,外面技师都不修管胎的
解刨管胎
玻璃渣子把管胎扎透了
拿砂纸打磨一下,涂完胶等风干
胶风干后贴片,按按揉揉
好多年没做针线活了,没想到今天给管胎缝闭口
线头有些遭了,扯断好多次
闭合管胎,涂胶加固
安装后轮打气
管胎缺点就优点就是轻,比开口胎、真空胎都轻,常用于专业竞赛和环法,就算车胎破了也能继续骑的,而开口胎和真空胎不行。
缺点就是破了就废了,各大车店都是不修管胎的,只能换,这用管胎的成本真是太高了,不是富哥真用不起,这款是意大利产的Challenge Elite Pro 25c,零售价三百多/条,就这还全网缺货,八月初才到货。我的轮组不支持另外两种胎,缝缝补补吧
再次经历三过家门而不入,就是为了凑这个整,今天管胎补的可以经测试一百公里没有问题。现在我心率还不是稳不住,恢复到之前的状态好难啊
得这辆车纯属缘分,前段时间在网上认识一个宁波的好大哥,没想到去年我们一起参加过同一个比赛,大哥是在宁波鄞州区开自行车店的,聊了许久大哥给我推荐一辆神车Wilier Cento 10SL!这是他朋友的爱车,财富自由润加拿大了,一些不方便带走的东西就卖掉了,这辆车刚到店里第一天,机缘巧合我就赶上了!
算上平踏、水杯架 整车重为7.45KG,一对平踏重0.3KG,换DA夹气分分钟上6!
这台车最吸引我的地方就是,圈刹!SL后最后一代顶级圈刹车,我在网上找了许久都找不到同款,这台车已经停产几年了,太稀有了,
出去转了转,拍点照片记录一下
之前在姑姑家移植过来的石榴树,一家人都走了,它留了下来,由于当时栽在河边政府没有铲掉
由于Jekyll默认使用UTC时区,导致博客更新时间不准确。这里需要写入上海时间:timezone: Asia/Shanghai,但是我在本地调试时需要在配置内注释掉,不然就会报错
上传到仓库 Github pages 不会出现这样的问题。老是注释调试挺麻烦的,Google搜出来的解决方案都是瞎扯淡,也不知道都是哪复制粘贴就发出来的。
gem install tzinfo-data
Gemfile 直接指定版本
gem 'tzinfo-data', '>= 1.2021a'
写入配置 timezone: Asia/Shanghai,确保调试的电脑时区也正常,开始运行
bundle exec jekyll serve
`
var preview = document.getElementById("preview");
var previewImage = document.getElementById("previewImage");
var previewImageTitle = <figcaption class="previewImageTitle"> + image.title[i] + </figcaption>;
previewImage.setAttribute('src', image.url[i]);
preview.style.display = 'flex';
var previousPreviewImageTitle = document.querySelector('.previewImageTitle');
if (previousPreviewImageTitle) {
previousPreviewImageTitle.parentNode.removeChild(previousPreviewImageTitle);
}
previewImage.insertAdjacentHTML('afterend', previewImageTitle);
preview.addEventListener("click", function() {
this.style.display = 'none';
});
`
markdown太丧心病狂了,js的代码块在转换的过程中给我生效了,大多方法都不能阻止这段代码不生效,把代码删删减减让我足足花了5分钟去注释这段代码…..
前几天Fooleap留言建议我用cn-font-split把字体做一下分包处理,分包后原18M变1M不到,一篇千文的文章才几百KB,加载速度没得说。
Haibao@DESKTOP-IB7LLPB MINGW64 /d/lhasa.github.io (master)
$ bundle exec npm run build
> fooleap@1.0.0 build D:\lhasa.github.io
> webpack -p && jekyll b
Hash: 470f4b7c37655d2798f9
Version: webpack 4.47.0
Time: 2347ms
Built at: 2024/02/11 上午5:27:39
Asset Size Chunks Chunk Names
main.min.css 27.2 KiB 0 [emitted] main
main.min.js 156 KiB 0 [emitted] main
[0] ./src/main.js 25.7 KiB {0} [built]
[3] ./src/sass/main.scss 39 bytes {0} [built]
+ 3 hidden modules
Configuration file: D:/lhasa.github.io/_config.yml
To use retry middleware with Faraday v2.0+, install `faraday-retry` gem
Source: D:/lhasa.github.io
Destination: D:/lhasa.github.io/_site
Incremental build: disabled. Enable with --incremental
Generating...
done in 7.403 seconds.
Auto-regeneration: disabled. Use --watch to enable.
这些天我把webPack临时学了一手,也是半斤八两,普通打个包是没问题,就是不知道这分包后的字体能不能用webPack打包,我还没试过。
因为网站在Github pages,这加载速度也快到顶了。目前还缺少一个已备案的域名,我想把腾讯云的COS用CDN来加速一下,这样的话静态资源应该会更快点,博客也没啥访问量,按流量计费,也花不了几个钱。
后来注意到URL包含了腾讯图片处理样式后缀,这里用正则做一下处理
image.url[i] = image.url[i].replace(/\.(jpg|jpeg|png|gif)[^/]*$/, '.$1');
今天换博客主要文字了,”仓耳今楷”,字体更美观更适合阅读。但是过程中遇到点问题
@font-face {
font-family: 仓耳今楷01-W04;
src: url("https://api.lhasa.icu/assets/font/tsanger01W04.ttf") format("truetype");
}
这段CSS写的是没有问题的,但是不生效,控制台报错跨域
腾讯云COS跨域访问CORS配置如下:
配置好后又遇到麻烦了,字体太大了,一个字体文件17.9M!网站都脱垮了
这里做一下处理,取子集压缩文字,需要用到 FontSmaller 和 现代汉语常用3500汉字
取子集压缩之后字体文件大小为1.94M
今天一个偶然接触到了Clarity Session Recording,当我看到它在我调试本地网站时,它居然录制了一条长达45分钟的视频,我大为震惊!由此,我怀揣着对技术的敬畏和亢奋的状态写下来这篇文章,虽然我非技术大牛,也非经济哲学家,但丝毫不影响我对技术的一些拙见。
技术,造福与社会的同时也存在着弊端,这是一个漫长的历史过程。在资本主义原始积累时期,技术发展相对缓慢,因此,在人们利用自然资源的同时,对于群众的利益得失,那是既看不出来,也堂而皇之。
在全民炼钢铁的时代,为了追求技术,挨家挨户交出铁器充数,上至灶台的铁锅,下至门板上的钢钉都要拆下来,全部投进了土高炉。先不讲炼铁对环境的污染,毕竟人都吃不饱,光是大食堂的一平二调、三高五风就饿死多少同胞啊!
犹如二零年新冠疫情初期的口罩机,相当受欢迎的低成本高回报疫情红利生产技术,口罩是一片难求,人民苦不堪言。而资本业绩翻一翻。但好景不长,随着口罩机业务跳水,增速不再。口罩凉了,口罩机成了一堆废铁。
要说技术亦福亦是祸,这不堪回首的民族历史足以证明。这种资本式的疯狂扩张给无知的人带来无尽的灾难,亦不可为,而为之,这就是资本的本性。
随着技术的迅猛发展,尤其是在信息时代,技术对社会、经济、文化等方方面面的影响愈发显著。曾经由无知带来的寒蝉效应逐渐缓解。相对来说,资本已经完成原始积累,对于技术的态度更为复杂,当WEB市场各种大利好大利空时,毫不犹豫的用PHP进行开发。当PHP已经不能获得更多合理的利润时,就会对PHP嗤之以鼻,甚至会给予判处死缓,阻碍它进入生产领域。
到了新媒体时代,我们在享受数字化便利的同时,也时刻面临着隐私泄露、信息滥用等风险。随着人工智能、大数据分析等技术的日益成熟,我们的个人信息被不断挖掘,商业巨头通过分析我们的行为、偏好来精准投放广告,甚至影响我们的决策过程,因百度竞价逝世的魏则西就是典型!
此外,技术的快速发展也带来了社会的数字鸿沟。那些掌握先进技术的人群能够更好的适应现代社会,而缺乏技术接触的人可能会陷入信息贫困。这种信息差不仅是技术能力的差异,更是社会资源分配的问题。
由此可见,在现代科技的巨大浪潮中,技术进步所带来的便利与其可能引发的负面效应是相对的。技术不仅仅是一种工具,更是对社会和人类价值观的挑战。我们在追逐技术的同时,必须寻找这个平衡点,制定法治和伦理准则,引导技术更加普惠,而非为一己之私,任道重远啊!