普通视图

发现新文章,点击刷新页面。
昨天以前游钓四方的博客
  • ✇游钓四方的博客
  • 聊聊自行车游钓四方的博客
    洋车子的起源 在河南老家,人们通常称自行车为“洋车子”。确实,是洋鬼子的杰作,早在1791年,法国佬西夫拉克设计出了世界上第一辆自行车,据说他是受溜冰鞋的启发,整车为木制,上管由一根大横梁作为主体车架,下方装两个木制车轮,没有转向结构,拐弯全靠搬。当然,它也没有传动系统,骑多快这就真看腿了 0-100的开始 追溯历史,19世纪六七十年代,自行车由洋鬼子、华人带入中国,数量寥寥 随着时间的推移,20世纪二十年代,上海做自行车销售的同昌车行开始仿制生自行车零部件,再搭配进口件的基础上进行组装贴牌。从此内地第一家仿制、组装整车的自行车企业诞生,据注册登记记载有:飞马、飞鹰、飞人、飞轮等型号。直到新中国1956年国家队将其收编、创新再贴牌。由此诞生:上海凤凰 新中国1955-1957年期间,是内地自行车产业发展的关键时期,由主管自行车工业的工信部,组织上海、天津、沈阳的自行车厂进行标准化设计,确定了28寸自行车的规格和零配件的质量标准,为内地自行车行业制定了第一部行业标准,为后来几十年的自行车普及奠定了基础 到了新中国六七十年代,自行车在全国范围内迅速普及,尽管当时的产业链尚不
     

聊聊自行车

2024年8月16日 03:02

洋车子的起源

在河南老家,人们通常称自行车为“洋车子”。确实,是洋鬼子的杰作,早在1791年,法国佬西夫拉克设计出了世界上第一辆自行车,据说他是受溜冰鞋的启发,整车为木制,上管由一根大横梁作为主体车架,下方装两个木制车轮,没有转向结构,拐弯全靠搬。当然,它也没有传动系统,骑多快这就真看腿了

河北 中国自行车博物馆 仿制藏品

0-100的开始

追溯历史,19世纪六七十年代,自行车由洋鬼子、华人带入中国,数量寥寥

随着时间的推移,20世纪二十年代,上海做自行车销售的同昌车行开始仿制生自行车零部件,再搭配进口件的基础上进行组装贴牌。从此内地第一家仿制、组装整车的自行车企业诞生,据注册登记记载有:飞马、飞鹰、飞人、飞轮等型号。直到新中国1956年国家队将其收编、创新再贴牌。由此诞生:上海凤凰

新中国1955-1957年期间,是内地自行车产业发展的关键时期,由主管自行车工业的工信部,组织上海、天津、沈阳的自行车厂进行标准化设计,确定了28寸自行车的规格和零配件的质量标准,为内地自行车行业制定了第一部行业标准,为后来几十年的自行车普及奠定了基础

到了新中国六七十年代,自行车在全国范围内迅速普及,尽管当时的产业链尚不完善,但许多品牌已逐渐崭露头角,下面有请八十年代国产五虎上将:上海永久、凤凰、天津飞鸽、红旗、江苏金狮

上海永久ZA51-9

富裕的象征

新中国1981年9月,三流日报曾转载了一篇文章:湖北农民杨小运在超额完成国家交售指标后,县里问他想要什么奖励,他说想要一辆永久牌自行车。很多年后,杨小运回忆:“我是壮起胆子提出了自己的想法。须知道那时候能够骑上一辆永久牌自行车是多么得意的事情,因为据说只有凭什么“工业券”才能买到这种稀罕物,比现在坐小轿车还要显摆,简直就是一种身份的象征。在我的印象中,好像只有公社党委书记这样的人物才配有这么一辆自行车

由此可见,自行车不仅是代步工具,更是那个年代富裕与社会地位的象征,俗称小资三件套:缝纫机、手表,自行车

60年代初-80年代末发行的自行车券

短暂的辉煌

然而,好景不长。随着改革开放的推进,外资品牌涌入中国市场,这些闭门仿车国产老品牌被外资的新工业设计所冲击,如:GIANT、Merida、Trek等等

此时的自行车带来最直接的就是视觉感官,功能多样的车架和丰富多彩的颜色,彻底颠覆了内地自行车的固有形象。相比之下,国产品牌的市场份额断崖式下跌,怕不是编制兜底,如今已全部倒闭,至此,沪上小资品牌殊荣不再

在新工业品牌中,最具代表性的是GIANT,尽管价格昂贵,但其时尚的设计和优越的性能激发了人民的购买欲望,迅速取代国产五虎上将,成为新一代的“高级自行车”,GIANT驻扎内地后年年蝉联市场第一,这一变革标志着中国内地自行车市场的一个重要转折点

较受欢迎的 GIANT ATX 760 图为90款

从改革开放到21世纪初的几十年间,中国自行车运动突然就死在了襁褓里,由于汽车的普及和部分国人的观念癌症,自行车逐渐边缘化,成为人们眼中过时的交通工具。就此,内地再也没有形成新的自行车运动

而在此期间,外面的世界发生了天翻地覆的变化。UCI在瑞士洛桑成立了世界自行车中心和场地车训练场,致力于培训精英选手并为赛事提供支持。高卢鸡的环法赛愈演愈烈,成为全球最受瞩目的公路自行车赛事之一。与此同时,小日子的Shimano凭借革命性的STI技术,将刹车和变速融合,逐步通过专利,垄断全球自行车零部件市场

1998年环法路过一片向日葵地

反观中国内地。自行车产业在这一波全球变革中停滞不前,国产品牌没有主动认清提升技术和产品价值的重要性,依然闭门造车,专注于下沉市场。没有技术,没有创新,短期图利坑蒙拐骗,论长期发展无异于自我毁灭

直到今天,大部分内地品牌在观念和行动上依然停留在过去的成功经验 ,靠坑蒙拐骗人民继续吃老本,最近又冒出一个内地品牌——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
     

写一个骑行页面(二)

2024年8月14日 20:40

在前几天写的数据展示页面中,日历与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;
}

JSON数据与日历数据两者时区不一致


// 涉及函数: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);

UPDATE 日历交互动画

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的图
     

与时间抗衡:笔记本清灰换硅脂记

2024年8月11日 20:19

下午,笔记本忽然发出了咔哧咔哧的响声,仿佛是临死前的挣扎。我知道,又是哪零件坏了。大半年没清灰,这机器,似乎比人还脆弱

笔记本内部堆满灰尘

压CPU的螺丝多半滑丝,硅脂也是,抹得太多,溢得像过剩的腐败

过多的硅脂溢出

满是灰尘的风扇

风扇拆下来时洒下许多灰尘,这风扇犹如一台无力的革命机器,快要垮掉。

风满是灰尘的风扇

一抹灰尘,手上黑漆漆的

满是污垢的盖板

折腾了半个小时,总算洗干净了,接下来是抹硅脂

清理干净,准备组装

这硅脂还是一九年买的,小日子的信越X-23-7868-2D,导热效果还不错,不过这种产品很虚,需要时间来验证

假一赔命

涂了不少,想压住它的病症,但心里知道,这些努力也只是苟延残喘。机器,终究是要坏的

硅脂涂抹过多

螺丝虽然滑丝了,但还勉强拧得上。我想,我该买些新螺丝,以免将来它彻底罢工

安装完毕

一次点亮。这台笔记本跟了我好多年,从上学时就陪着我,现在还能跑动。除了渲染不给力,写代码倒是没问题。

我觉得,电脑这东西,特别是Windows系统,得学会一些优化技巧。不然,即使配置再高,也会卡顿,这和底层设计脱不开关系,和Android有异曲同工之处。

相比之下,我更喜欢Linux。之前用Manjaro Linux做主力机有四年时间。不过,Linux的图形界面BUG让我很头疼,几乎每个版本都有各种关于GUI的BUG。刚接触Linux时,最让我害怕的就是系统更新。说到这里,不得不提一下王垠

谈 Linux,Windows 和 Mac

点亮成功

  • ✇游钓四方的博客
  • 写一个骑行页面游钓四方的博客
    作为一个爱好骑行的博主,总觉得博客里少了点什么,骑行骑行的,怎么能没有一个专门的骑行数据展示页呢 在设计这个页面的时候,参考了许多骑行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 │ ├─cycli
     

写一个骑行页面

2024年8月11日 16:02

作为一个爱好骑行的博主,总觉得博客里少了点什么,骑行骑行的,怎么能没有一个专门的骑行数据展示页呢

在设计这个页面的时候,参考了许多骑行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

cycling.js

目前所有的逻辑都在这一个文件里完成,现在的功能还是个雏,因为没有打通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);
    });
}

cycling.scss

骑行统计页面不会止步于此,接下来还会有很大的延申改动,我提前把变量接口留好了,定义了一些主样式变量,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';

webpack配置

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、懒加载上线

初稿

骑行:https://lhasa.icu/cycling.html

  • ✇游钓四方的博客
  • 写一个Chrome表单自动化插件游钓四方的博客
    在刷博客的时候,最麻烦的事情之一就是手动填写各种表单。为了提高我的冲浪体验,诞生了这款表单自动化插件。经过爬虫上百次调教,兼容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 │ formM
     

写一个Chrome表单自动化插件

2024年8月7日 14:26

在刷博客的时候,最麻烦的事情之一就是手动填写各种表单。为了提高我的冲浪体验,诞生了这款表单自动化插件。经过爬虫上百次调教,兼容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

webpack.config.js

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

// 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();
  }
});

formManager.js

该文件负责向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等骑车回来再做

Github

Form-automation-plugin:https://github.com/achuanya/Form-automation-plugin

  • ✇游钓四方的博客
  • 七月最后一天骑行,有氧100公里游钓四方的博客
    今天是七月的最后一天,晚上必须来一次有氧小长途骑行,目标暂且定为100公里 出发前,先泡两瓶蛋白粉放进冰箱,一瓶550ML,一瓶750ML,我还是觉得不够用。骑车过程中不想下来,容易打断节奏。我打算坐尾再安装一个支架放一瓶750ML,只要室外温度不是特变态,百里油耗三瓶水没有问题。 检查一下前变、后拨、夹气和胎压。前胎由于自补液在气嘴处凝固,导致无法打气,不过目前胎压足够,不影响骑行,估计再骑一周胎压就不行了,由于管胎的特殊结构,我还没有合适的解决方案,除了换胎。 下午喝了两碗绿豆粥,吃了些核桃饼,开始做最后的准备,带了两件便携式螺丝刀,一小包纸巾,一包干湿巾,一包电解质盐丸,这些正好放进后尾包,占了大概60%空间,剩余空间还能放盒烟。 衣服就没啥好挑的,穿条ASSOS背带裤和速白干背心就行了,如果不是为了注意个人形象,我直接背带裤光膀子。在上海生活的时候也特热,多次骑行都是背带裤光膀子,有两次路过外滩被交警抓了,说外滩都是游客,我个人形象影响市容。 到地方了,这骑行路段是挺不错的,我有七年没来这里了,今天来到这里发现大变样了,还可以租皮划艇,改天一定玩玩。在这里绕圈骑了
     

七月最后一天骑行,有氧100公里

2024年8月1日 01:37

今天是七月的最后一天,晚上必须来一次有氧小长途骑行,目标暂且定为100公里

出发前,先泡两瓶蛋白粉放进冰箱,一瓶550ML,一瓶750ML,我还是觉得不够用。骑车过程中不想下来,容易打断节奏。我打算坐尾再安装一个支架放一瓶750ML,只要室外温度不是特变态,百里油耗三瓶水没有问题。

奥普帝蒙 黑标分离乳清蛋白粉

检查一下前变、后拨、夹气和胎压。前胎由于自补液在气嘴处凝固,导致无法打气,不过目前胎压足够,不影响骑行,估计再骑一周胎压就不行了,由于管胎的特殊结构,我还没有合适的解决方案,除了换胎。

下午喝了两碗绿豆粥,吃了些核桃饼,开始做最后的准备,带了两件便携式螺丝刀,一小包纸巾,一包干湿巾,一包电解质盐丸,这些正好放进后尾包,占了大概60%空间,剩余空间还能放盒烟。

衣服就没啥好挑的,穿条ASSOS背带裤和速白干背心就行了,如果不是为了注意个人形象,我直接背带裤光膀子。在上海生活的时候也特热,多次骑行都是背带裤光膀子,有两次路过外滩被交警抓了,说外滩都是游客,我个人形象影响市容。

到地方了,这骑行路段是挺不错的,我有七年没来这里了,今天来到这里发现大变样了,还可以租皮划艇,改天一定玩玩。在这里绕圈骑了十五公里,又跑去周口公园绕圈,要说这夏天油耗确实高,机动车顶不住,人也顶不住啊!我的水有点不够了,今天才33°,河南的热和江浙沪的热真是不一样,回家后一直不适应,出公园后去蜜雪冰城买了一杯柠檬水,找店里的小妹妹白嫖了两瓶冰水,直奔淮阳区·龙湖

周口植物园

到龙湖后里程已经到了60公里,这时天也暗了,有些许疲惫,主要是颈椎疼,下把位骑多了。吃了两片盐丸,两手握把立边缘慢骑摆烂二十分钟,这个时候一定不能下来歇,推车都不行,下车体力直接归零,不知道别人咋样,我是这样的,与自己较劲,100公里?200公里又算个屁,出来了,就要干

奶奶煮的绿豆粥

在龙湖绕了三圈后达标,真是饿得不行了。到家已经十点,浑身湿透,黏糊糊的,洗了澡,洗了衣服,然后躺下看订阅,期待八月的骑行。

今日有氧100公里完成

  • ✇游钓四方的博客
  • 利用Go+Github Actions写个定时RSS爬虫游钓四方的博客
    说起这事,还是受一位博友的启发“1900”他的左邻右舍页面很棒,决定模仿一下。我平时也用 Inoreader,但我还是喜欢直接打开博客的感觉,心血来潮,搞。 起初,我打算使用 COS 和 GitHub Actions,但在测试过程中发现 GitHub 的延迟非常高,验证和文件写入速度极慢,频频失败。干脆直接上 GitHub 自产自销。 大致思路 main() │ ├── readFeedsFromGitHub() │ ├── GitHub API 调用 │ │ ├── 读取 rss_feeds.txt 文件 │ │ └── 处理文件报错 │ └── Return │ ├── fetchRSS() │ ├── 遍历 RSS │ │ ├── HTTP GET 请求 │ │ └── 处理请求错误 │ ├── 解析 RSS │ │ ├── 清理 XML 内容中的非法字符 │ │ ├── 提取域名 │ │ └── 格式化并排序 │ └── Return │ └── saveToGitHub() ├── Git
     

利用Go+Github Actions写个定时RSS爬虫

2024年7月27日 09:50

说起这事,还是受一位博友的启发“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

Go RSS 爬虫 Code

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!")
}

Go 生成的 json 数据

[
    {
        "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"
    }
]

Go 生成的日志

[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

Github Actons 1h/次

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

效果页:https://lhasa.icu/links.html

  • ✇游钓四方的博客
  • Tencent CDN 流量被恶意盗刷游钓四方的博客
    看到这张图的时候,我很震惊。这个CDN流量包是我昨天凌晨刚买的,直到此刻才发现我的CDN流量被恶意盗刷了。 事情是这样的,前天23号我在写新功能,本地调试调用了很多资源,当时看到消耗了90G的流量,我没有在意,以为是调试的问题。因为那天我写了一天代码,不停地调用Tencent COS,而COS还套了一层Tencent CDN。当时我以为是正常消耗,眼看流量不够,我又充了一个CDN加速包。 然而就在今晚22:45,我骑行回来关闭了免打扰模式,邮箱忽然弹出通知,腾讯云提示我CDN流量不足?我当时非常震惊,因为这是我24号凌晨刚买的流量包啊! 看到这张图时,我火了,在独立博客圈彻底火了,2天内请求数42万?赶超月光博客! 回想过去,我在博客圈认识的人一只手能数过来,更谈不上得罪谁。这事也怪我,之前COS没有任何防护,几乎处于裸奔状态。 由于我的博客托管在Github Pages,主机问题大可不必考虑,我能做的只有设置黑白名单和周期限流。 不再裸奔,已老实。 UPDATE 凌晨 02:32 知道怎么回事了,24年后,大陆境内出现一窝狗,利用PCDN恶意流量攻击!
     

Tencent CDN 流量被恶意盗刷

2024年7月25日 00:44

来自腾讯云的邮件

看到这张图的时候,我很震惊。这个CDN流量包是我昨天凌晨刚买的,直到此刻才发现我的CDN流量被恶意盗刷了。

事情是这样的,前天23号我在写新功能,本地调试调用了很多资源,当时看到消耗了90G的流量,我没有在意,以为是调试的问题。因为那天我写了一天代码,不停地调用Tencent COS,而COS还套了一层Tencent CDN。当时我以为是正常消耗,眼看流量不够,我又充了一个CDN加速包。

然而就在今晚22:45,我骑行回来关闭了免打扰模式,邮箱忽然弹出通知,腾讯云提示我CDN流量不足?我当时非常震惊,因为这是我24号凌晨刚买的流量包啊!

腾讯云 数据分析控制台

看到这张图时,我火了,在独立博客圈彻底火了,2天内请求数42万?赶超月光博客!

腾讯云 访问分布

TOP ONE 60.221.195.144

回想过去,我在博客圈认识的人一只手能数过来,更谈不上得罪谁。这事也怪我,之前COS没有任何防护,几乎处于裸奔状态。

由于我的博客托管在Github Pages,主机问题大可不必考虑,我能做的只有设置黑白名单和周期限流。

不再裸奔,已老实。

UPDATE 凌晨 02:32

知道怎么回事了,24年后,大陆境内出现一窝狗,利用PCDN恶意流量攻击!

  • 攻击的主要IP来源于山西、江苏和安徽联通等地的固定网段

  • 攻击时间非常规律,集中在19:50到23:00之间

  • 攻击者会针对体积较大的静态文件进行持续攻击

自7月初以来,已转头无差别地对大陆中小型网站展开攻击。

建议将山西等地的IP段暂时屏蔽,减少恶意流量的影响。

目前,GitHub上已经有相关项目 ban-pcdn-ip 用于收集这些恶意IP段。

  • ✇游钓四方的博客
  • 公路车管胎被扎,怎么补胎游钓四方的博客
    管胎被扎了,还是后轮,我心如刀绞啊,太贵了,换不起,外面技师都不修管胎的 解刨管胎 玻璃渣子把管胎扎透了 拿砂纸打磨一下,涂完胶等风干 胶风干后贴片,按按揉揉 好多年没做针线活了,没想到今天给管胎缝闭口 线头有些遭了,扯断好多次 闭合管胎,涂胶加固 安装后轮打气 管胎缺点就优点就是轻,比开口胎、真空胎都轻,常用于专业竞赛和环法,就算车胎破了也能继续骑的,而开口胎和真空胎不行。 缺点就是破了就废了,各大车店都是不修管胎的,只能换,这用管胎的成本真是太高了,不是富哥真用不起,这款是意大利产的Challenge Elite Pro 25c,零售价三百多/条,就这还全网缺货,八月初才到货。我的轮组不支持另外两种胎,缝缝补补吧 7-18 晚 测试补充 再次经历三过家门而不入,就是为了凑这个整,今天管胎补的可以经测试一百公里没有问题。现在我心率还不是稳不住,恢复到之前的状态好难啊
     

公路车管胎被扎,怎么补胎

2024年7月19日 14:38

管胎被扎了,还是后轮,我心如刀绞啊,太贵了,换不起,外面技师都不修管胎的

先拆后轮

解刨管胎

解刨管胎

玻璃渣子把管胎扎透了

扎眼

拿砂纸打磨一下,涂完胶等风干

打磨涂胶

胶风干后贴片,按按揉揉

贴片

好多年没做针线活了,没想到今天给管胎缝闭口

穿针引线

管胎外皮真厚

线头有些遭了,扯断好多次

穿针走线

闭合管胎,涂胶加固

闭合管胎,涂胶加固

安装后轮打气

安装后轮打气

管胎缺点就优点就是轻,比开口胎、真空胎都轻,常用于专业竞赛和环法,就算车胎破了也能继续骑的,而开口胎和真空胎不行。

缺点就是破了就废了,各大车店都是不修管胎的,只能换,这用管胎的成本真是太高了,不是富哥真用不起,这款是意大利产的Challenge Elite Pro 25c,零售价三百多/条,就这还全网缺货,八月初才到货。我的轮组不支持另外两种胎,缝缝补补吧

7-18 晚 测试补充

再次经历三过家门而不入,就是为了凑这个整,今天管胎补的可以经测试一百公里没有问题。现在我心率还不是稳不住,恢复到之前的状态好难啊

测试

  • ✇游钓四方的博客
  • 喜提新车 Wilier Cento 10SL游钓四方的博客
    得这辆车纯属缘分,前段时间在网上认识一个宁波的好大哥,没想到去年我们一起参加过同一个比赛,大哥是在宁波鄞州区开自行车店的,聊了许久大哥给我推荐一辆神车Wilier Cento 10SL!这是他朋友的爱车,财富自由润加拿大了,一些不方便带走的东西就卖掉了,这辆车刚到店里第一天,机缘巧合我就赶上了! 配置如下: Wilier Cento 10SL RIM STEMMA SL 把立 BARRA SL 弯把 WIND BREAK 50框 碳辐条 SHIMANO 105 R7000夹气,其他全车Ultegra 8000 算上平踏、水杯架 整车重为7.45KG,一对平踏重0.3KG,换DA夹气分分钟上6! 这台车最吸引我的地方就是,圈刹!SL后最后一代顶级圈刹车,我在网上找了许久都找不到同款,这台车已经停产几年了,太稀有了, 上图
     

喜提新车 Wilier Cento 10SL

2024年7月12日 22:33

得这辆车纯属缘分,前段时间在网上认识一个宁波的好大哥,没想到去年我们一起参加过同一个比赛,大哥是在宁波鄞州区开自行车店的,聊了许久大哥给我推荐一辆神车Wilier Cento 10SL!这是他朋友的爱车,财富自由润加拿大了,一些不方便带走的东西就卖掉了,这辆车刚到店里第一天,机缘巧合我就赶上了!

配置如下:

  • Wilier Cento 10SL RIM
  • STEMMA SL 把立
  • BARRA SL 弯把
  • WIND BREAK 50框 碳辐条
  • SHIMANO 105 R7000夹气,其他全车Ultegra 8000

算上平踏、水杯架 整车重为7.45KG,一对平踏重0.3KG,换DA夹气分分钟上6!

这台车最吸引我的地方就是,圈刹!SL后最后一代顶级圈刹车,我在网上找了许久都找不到同款,这台车已经停产几年了,太稀有了,

上图

车到了

我装车的时候把刹车线芯插坏了

我装好了

寄回来之前,技师称重

骑行照

  • ✇游钓四方的博客
  • 搞个公众号游钓四方的博客
    改名记录: 2024年02月08日 “阿川的博客”改名“游钓四方的博客” 2019年05月28日 “阿川的个人博客”改名“阿川的博客” 2018年10月26日 注册“阿川的个人博客” 今天捡回了18年注册的公众号,数据重新导了一遍,这手动整理几年的文章数据,我多少有些疲惫 这次熬的有点久了,明歇一天,再写个脚本让 Github Pages 文章自动同步到微信公众号,得想个办法 公众号还需要做个人认证,不然内置超链接是个麻烦事 公众号前端页面也需要重新写一个,腾讯自带UI限制文章数量 任重而道远啊!加油。
     

搞个公众号

2024年3月10日 15:51

改名记录:

  • 2024年02月08日 “阿川的博客”改名“游钓四方的博客”
  • 2019年05月28日 “阿川的个人博客”改名“阿川的博客”
  • 2018年10月26日 注册“阿川的个人博客”

今天捡回了18年注册的公众号,数据重新导了一遍,这手动整理几年的文章数据,我多少有些疲惫

这次熬的有点久了,明歇一天,再写个脚本让 Github Pages 文章自动同步到微信公众号,得想个办法

公众号还需要做个人认证,不然内置超链接是个麻烦事

公众号前端页面也需要重新写一个,腾讯自带UI限制文章数量

任重而道远啊!加油。

  • ✇游钓四方的博客
  • 解决Jekyll时区数据源游钓四方的博客
    由于Jekyll默认使用UTC时区,导致博客更新时间不准确。这里需要写入上海时间:timezone: Asia/Shanghai,但是我在本地调试时需要在配置内注释掉,不然就会报错 jekyll 3.9.3 | Error: No source of timezone data could be found. Please refer to https://tzinfo.github.io/datasourcenotfound for help resolving this error. 上传到仓库 Github pages 不会出现这样的问题。老是注释调试挺麻烦的,Google搜出来的解决方案都是瞎扯淡,也不知道都是哪复制粘贴就发出来的。 gem install tzinfo-data Gemfile 直接指定版本 gem 'tzinfo-data', '>= 1.2021a' 写入配置 timezone: Asia/Shanghai,确保调试的电脑时区也正常,开始运行 bundle exec jekyll serve
     

解决Jekyll时区数据源

2024年2月11日 23:07

由于Jekyll默认使用UTC时区,导致博客更新时间不准确。这里需要写入上海时间:timezone: Asia/Shanghai,但是我在本地调试时需要在配置内注释掉,不然就会报错

  • jekyll 3.9.3 | Error: No source of timezone data could be found. Please refer to https://tzinfo.github.io/datasourcenotfound for help resolving this error.

上传到仓库 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)
     

初一大吉,博客上上新

2024年2月11日 05:35

图片预览

`

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,加载速度没得说。

cn-font-split 分包后的效果

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来加速一下,这样的话静态资源应该会更快点,博客也没啥访问量,按流量计费,也花不了几个钱。

2024/2/14 更新

后来注意到URL包含了腾讯图片处理样式后缀,这里用正则做一下处理

image.url[i] = image.url[i].replace(/\.(jpg|jpeg|png|gif)[^/]*$/, '.$1');

  • ✇游钓四方的博客
  • 腾讯云COS文件跨域游钓四方的博客
    今天换博客主要文字了,”仓耳今楷”,字体更美观更适合阅读。但是过程中遇到点问题 @font-face { font-family: 仓耳今楷01-W04; src: url("https://api.lhasa.icu/assets/font/tsanger01W04.ttf") format("truetype"); } 这段CSS写的是没有问题的,但是不生效,控制台报错跨域 has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource. 腾讯云COS跨域访问CORS配置如下: 配置好后又遇到麻烦了,字体太大了,一个字体文件17.9M!网站都脱垮了 这里做一下处理,取子集压缩文字,需要用到 FontSmaller 和 现代汉语常用3500汉字 取子集压缩之后字体文件大小为1.94M
     

腾讯云COS文件跨域

2024年2月5日 17:33

今天换博客主要文字了,”仓耳今楷”,字体更美观更适合阅读。但是过程中遇到点问题

@font-face {
    font-family: 仓耳今楷01-W04;
    src: url("https://api.lhasa.icu/assets/font/tsanger01W04.ttf")  format("truetype");
}

这段CSS写的是没有问题的,但是不生效,控制台报错跨域

  • has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource.

腾讯云COS跨域访问CORS配置如下:

腾讯云COS跨域访问CORS设置

配置好后又遇到麻烦了,字体太大了,一个字体文件17.9M!网站都脱垮了

网站被拖垮了

这里做一下处理,取子集压缩文字,需要用到 FontSmaller现代汉语常用3500汉字

取子集压缩之后字体文件大小为1.94M

取子集压缩后的效果

  • ✇游钓四方的博客
  • 技术亦福亦是祸游钓四方的博客
    今天一个偶然接触到了Clarity Session Recording,当我看到它在我调试本地网站时,它居然录制了一条长达45分钟的视频,我大为震惊!由此,我怀揣着对技术的敬畏和亢奋的状态写下来这篇文章,虽然我非技术大牛,也非经济哲学家,但丝毫不影响我对技术的一些拙见。 技术,造福与社会的同时也存在着弊端,这是一个漫长的历史过程。在资本主义原始积累时期,技术发展相对缓慢,因此,在人们利用自然资源的同时,对于群众的利益得失,那是既看不出来,也堂而皇之。 在全民炼钢铁的时代,为了追求技术,挨家挨户交出铁器充数,上至灶台的铁锅,下至门板上的钢钉都要拆下来,全部投进了土高炉。先不讲炼铁对环境的污染,毕竟人都吃不饱,光是大食堂的一平二调、三高五风就饿死多少同胞啊! 犹如二零年新冠疫情初期的口罩机,相当受欢迎的低成本高回报疫情红利生产技术,口罩是一片难求,人民苦不堪言。而资本业绩翻一翻。但好景不长,随着口罩机业务跳水,增速不再。口罩凉了,口罩机成了一堆废铁。 要说技术亦福亦是祸,这不堪回首的民族历史足以证明。这种资本式的疯狂扩张给无知的人带来无尽的灾难,亦不可为,而为之,这就是资本的本性。
     

技术亦福亦是祸

2024年2月2日 15:50

今天一个偶然接触到了Clarity Session Recording,当我看到它在我调试本地网站时,它居然录制了一条长达45分钟的视频,我大为震惊!由此,我怀揣着对技术的敬畏和亢奋的状态写下来这篇文章,虽然我非技术大牛,也非经济哲学家,但丝毫不影响我对技术的一些拙见。

技术,造福与社会的同时也存在着弊端,这是一个漫长的历史过程。在资本主义原始积累时期,技术发展相对缓慢,因此,在人们利用自然资源的同时,对于群众的利益得失,那是既看不出来,也堂而皇之。

在全民炼钢铁的时代,为了追求技术,挨家挨户交出铁器充数,上至灶台的铁锅,下至门板上的钢钉都要拆下来,全部投进了土高炉。先不讲炼铁对环境的污染,毕竟人都吃不饱,光是大食堂的一平二调、三高五风就饿死多少同胞啊!

犹如二零年新冠疫情初期的口罩机,相当受欢迎的低成本高回报疫情红利生产技术,口罩是一片难求,人民苦不堪言。而资本业绩翻一翻。但好景不长,随着口罩机业务跳水,增速不再。口罩凉了,口罩机成了一堆废铁。

要说技术亦福亦是祸,这不堪回首的民族历史足以证明。这种资本式的疯狂扩张给无知的人带来无尽的灾难,亦不可为,而为之,这就是资本的本性。

随着技术的迅猛发展,尤其是在信息时代,技术对社会、经济、文化等方方面面的影响愈发显著。曾经由无知带来的寒蝉效应逐渐缓解。相对来说,资本已经完成原始积累,对于技术的态度更为复杂,当WEB市场各种大利好大利空时,毫不犹豫的用PHP进行开发。当PHP已经不能获得更多合理的利润时,就会对PHP嗤之以鼻,甚至会给予判处死缓,阻碍它进入生产领域。

到了新媒体时代,我们在享受数字化便利的同时,也时刻面临着隐私泄露、信息滥用等风险。随着人工智能、大数据分析等技术的日益成熟,我们的个人信息被不断挖掘,商业巨头通过分析我们的行为、偏好来精准投放广告,甚至影响我们的决策过程,因百度竞价逝世的魏则西就是典型!

此外,技术的快速发展也带来了社会的数字鸿沟。那些掌握先进技术的人群能够更好的适应现代社会,而缺乏技术接触的人可能会陷入信息贫困。这种信息差不仅是技术能力的差异,更是社会资源分配的问题。

由此可见,在现代科技的巨大浪潮中,技术进步所带来的便利与其可能引发的负面效应是相对的。技术不仅仅是一种工具,更是对社会和人类价值观的挑战。我们在追逐技术的同时,必须寻找这个平衡点,制定法治和伦理准则,引导技术更加普惠,而非为一己之私,任道重远啊!

  • ✇游钓四方的博客
  • 晒晒年中时的骑行游钓四方的博客
    年前回家了,怀念年中在上海骑车日子 以下素材来自 2023年5月14日 开赛当天,比赛时闷头骑没时间拍,遗憾 年中时iphone坏了换了手机,导致今年六月份 两天半的时间从 河南周口 骑行到 上海 记录没有了…是我目前用时最短,距离最长骑行的记录。还有好多在上海留下的足迹照片都没了,下次证据一定奉上!
     

晒晒年中时的骑行

2024年2月2日 05:24

年前回家了,怀念年中在上海骑车日子

骑行 TCR PRO 在闵行区华漕路附近路亚

骑行 TCR PRO 在黄浦区苏州河喝咖啡

骑行 XTC 800 在邹市明拳击馆附近夜骑

从闵行区虹桥青杉路出发,目的地:余姚市全民健身中心签名报道!

宁波·余姚第六届环四明山比赛·第17届马自骑资格认证赛 公路车·全程租·A0753 张宏海

PAS骑行服

比赛前物品准备·军火展示

比赛前一周训练·在四明山入口处

比赛前一周训练·在四明山不知名山腰

比赛前一周训练·望着后方已经爬过的群山 我站在山腰极度兴奋,大概剩100公里结束训练

以下素材来自 2023年5月14日 开赛当天,比赛时闷头骑没时间拍,遗憾

5月14日 完赛后 主办奖台留念

奖状与奖牌

最终成绩224名

Garmin Edge 1040 码表成绩

年中时iphone坏了换了手机,导致今年六月份 两天半的时间从 河南周口 骑行到 上海 记录没有了…是我目前用时最短,距离最长骑行的记录。还有好多在上海留下的足迹照片都没了,下次证据一定奉上!

  • ✇游钓四方的博客
  • 论南北饮食差异游钓四方的博客
    沪上为异客,巴月三成弦。朔风正摇落,行子已归旋。 在家真好!今起了个大早,六点半时天蒙蒙亮,我穿着毛绒睡衣骑电动车赶早怼了碗胡辣汤,五元/碗经济实惠,舀上一勺感受这辛辣酸甜,顿时想起沪漂这些年的心酸往事… 回想上海20/碗清汤淡水,那是什么狗八胡辣汤,色香味你是一样都不沾。勾八商家,不要跟我说房租成本,原料物价高,我做原料采购这些年,江浙沪海鲜农产品行情不敢说是专家,多少有点自己的见解,那一两胡椒面值几个钱,一碗水放俩菜叶子净毛利有多少?这些投机倒把的无良商家一点诚信都没有,餐饮届都让你们这些老鼠屎给败坏了。 说多说少我对爱丁堡有些偏见,但并不妨碍我对于淮阳菜、粤菜的喜爱,民以食为天嘛!由于之前就职粤菜杭菜的原料采购经历,今我想聊聊南北的饮食差异。 南甜北咸,东辣西酸 南米北面,若从菜系角度来讲,民间起初有四大菜系:粤菜、淮扬菜、鲁菜、川菜。后来随着生活提高又衍生出了:浙菜、闽菜、湘菜、徽菜。就此入的流的就这八大菜系,此外的就不讲了。 在餐饮界,八大菜系是谁也不服谁。鲁师傅嘲讽粤菜淡不拉几没味,啥都剩着吃,俺鲁菜作为御膳才是八大菜系之首。 听到鲁菜叫嚣,淮扬菜师傅立马就
     

论南北饮食差异

2024年1月29日 12:18

沪上为异客,巴月三成弦。朔风正摇落,行子已归旋。

在家真好!今起了个大早,六点半时天蒙蒙亮,我穿着毛绒睡衣骑电动车赶早怼了碗胡辣汤,五元/碗经济实惠,舀上一勺感受这辛辣酸甜,顿时想起沪漂这些年的心酸往事…

胡辣汤、肉包子、茶叶蛋、鸡蛋饼

回想上海20/碗清汤淡水,那是什么狗八胡辣汤,色香味你是一样都不沾。勾八商家,不要跟我说房租成本,原料物价高,我做原料采购这些年,江浙沪海鲜农产品行情不敢说是专家,多少有点自己的见解,那一两胡椒面值几个钱,一碗水放俩菜叶子净毛利有多少?这些投机倒把的无良商家一点诚信都没有,餐饮届都让你们这些老鼠屎给败坏了。

说多说少我对爱丁堡有些偏见,但并不妨碍我对于淮阳菜、粤菜的喜爱,民以食为天嘛!由于之前就职粤菜杭菜的原料采购经历,今我想聊聊南北的饮食差异。

南甜北咸,东辣西酸

南米北面,若从菜系角度来讲,民间起初有四大菜系:粤菜、淮扬菜、鲁菜、川菜。后来随着生活提高又衍生出了:浙菜、闽菜、湘菜、徽菜。就此入的流的就这八大菜系,此外的就不讲了。

在餐饮界,八大菜系是谁也不服谁。鲁师傅嘲讽粤菜淡不拉几没味,啥都剩着吃,俺鲁菜作为御膳才是八大菜系之首。 听到鲁菜叫嚣,淮扬菜师傅立马就坐不住了便大喊到:老子是国宴,一手白袍虾仁尼克松来了都要舔盘子。闻声,川师傅急忙插一嘴:你们淮阳菜太甜,像吃了蜜蜂屎,不如俺们的麻婆豆腐,那叫一个地道…你说一句我怼一句,一时间硝烟四起…本是同根生,口味差距为何如此之大?

南北气候差异

先从南北所处生态环境来讲,再映射原料。北方气候较为寒冷且干燥,北京为例,坐标蒙古高压的东南边缘,西北方向有广阔的沙漠和草原,所以北京全年降雨量少且集中在夏季。此外,北京的雾霾和沙尘暴也是常态化,春季最为明显。所以结论得出北方的土壤适合种植小麦、玉米、大豆等农作物,即面食,这些原料应对寒冷干燥的环境可以贮藏很长时间。

而南方则相反,小桥流水,细雨芊芊,最属江浙,我的最爱。这里属亚热带季风气候,全年降水充沛,集中在春夏季。即使在降水量低的冬季,受冷暖空气共同作用,也不会像北方寒冷干燥,隐约的记得在杭州采购部的时候,屋里的被子感觉都没有干过。

所以南方以湿润的气候独步天下,富饶的土壤和丰富的水资源在贫困的新手发育阶段显得尤为重要,我认为这也是南北方饮食差异的最大原因:经济。

差异的本质

结上来看,各菜系各有各的特色,鲁菜利用广阔的地理资源吃的有些粗犷,而南方的小资较为精细,要说原因,我看还是穷。从GDP来说,

2019年中国各省GDP总量及增速情况

要么说还是南方人富裕,全面小康喊了几十年的口号,得道的又有几个地方?毕竟地理位置和气候环境决定了起步的高度。图上排名靠前的省份大多是清一色的长江中下游地区,从这里咱们可以从地理位置以农耕的角度来看经济效应。

秦岭到淮河一线以南属亚热带地区,常年降水量大于800毫米,热量充足,降水充沛,得天独厚的条件能一年两熟或一年三熟。

秦岭到淮河一线以北常年旱地,有一年一熟或两年三熟就不错了。所以秦岭到淮河这条线的走向也是决定了南北饮食差异化的原因之一。

既然没钱,裤腰带勒紧过,川菜的辛辣起初也是经济的体现。如中国的旧社会,有势有钱有三吃:细,鲜,甜。就这三个条件在旧社会就不得了,穷人家别说吃甜,鲜,能有粮食就不错了,别管变质与否,所以难吃变质的食物加以调料掩盖其味道得以饱腹。

我奶奶是从五八年走过来的人,我小时候常挑食的时候我奶奶马上就会讲,放到五八年,你们这群人都得饿死,树皮都没得吃。也就是这些历史传承原因导致了现在的南北饮食差异,现在的日子不同以往了,除个别困难地区,大多数家庭还是能温饱的,口味也都传承了下来。

虽然现在的经济形势也很差,但比以前是好多了,人的嘴都吃叼了。我想南北饮食的差异化也会随着小康社会的推动逐渐减少。

  • ✇游钓四方的博客
  • 置办了一套茶具游钓四方的博客
    我挺喜欢喝茶的,在采购部的时候,凌晨起来上班都会泡壶一级龙井放在车上,都是供应商明着面送采购部让大家喝的,主任睁一只眼闭一只眼也不会说什么。 中午下午闲的时候,我常去江杨市场和江桥的国际餐具市场转悠,一是去询价看看新上市的原料了解一下当下行情,二是去供应商那坐坐,大伙聊聊行情吹吹牛逼,过程自然少不了盖碗茶。 话说对于这茶具我是熟悉又陌生,我的工作有一部分是采购餐厨具,但主要的茶具多由集团向龙泉瓷集中采购,还轮不到我插一嘴。前些天茶喝的时候忽然想到了茶具,抱着对传统文化的好奇心,我就置办了点便宜的德化窑白陶瓷。 我喜欢这样的陶瓷器,通透,一抹凝脂如玉的色,把玩着、抚摸着宛如女人的肌肤。 说到陶瓷器,我认为景德镇为上佳,德化和他差的不是一丁半点,从出身、烧制起就有所不同。 景德镇,始于唐,盛于宋,早在元世祖在位时就成立了御窑厂,上呈皇族,下卖显贵。由此以来景德镇的名号都成了陶瓷器的金字招牌,但凡沾上点关系,价格翻一翻。 而德化不同于老大哥的官窑背景,少些贵族气质,多些烟火气息,怎么说德化也是世界陶瓷之都啊! 其次,两者的工艺也有所不同,景德镇的高温烧制工艺讲究还原气氛,使
     

置办了一套茶具

2024年1月28日 16:36

我挺喜欢喝茶的,在采购部的时候,凌晨起来上班都会泡壶一级龙井放在车上,都是供应商明着面送采购部让大家喝的,主任睁一只眼闭一只眼也不会说什么。

中午下午闲的时候,我常去江杨市场和江桥的国际餐具市场转悠,一是去询价看看新上市的原料了解一下当下行情,二是去供应商那坐坐,大伙聊聊行情吹吹牛逼,过程自然少不了盖碗茶。

话说对于这茶具我是熟悉又陌生,我的工作有一部分是采购餐厨具,但主要的茶具多由集团向龙泉瓷集中采购,还轮不到我插一嘴。前些天茶喝的时候忽然想到了茶具,抱着对传统文化的好奇心,我就置办了点便宜的德化窑白陶瓷。

茶盘、盖碗、公道杯、斗笠杯、茶夹、茶漏

我喜欢这样的陶瓷器,通透,一抹凝脂如玉的色,把玩着、抚摸着宛如女人的肌肤。

说到陶瓷器,我认为景德镇为上佳,德化和他差的不是一丁半点,从出身、烧制起就有所不同。

景德镇,始于唐,盛于宋,早在元世祖在位时就成立了御窑厂,上呈皇族,下卖显贵。由此以来景德镇的名号都成了陶瓷器的金字招牌,但凡沾上点关系,价格翻一翻。

而德化不同于老大哥的官窑背景,少些贵族气质,多些烟火气息,怎么说德化也是世界陶瓷之都啊!

其次,两者的工艺也有所不同,景德镇的高温烧制工艺讲究还原气氛,使其具有了独特搜刮钱财的光泽,最具代表性则为青白瓷釉,一线大师齐聚一堂,精益求精的工艺赋予了景德镇陶瓷独特的艺术品格。

相关,德化的机械化流水线式生产就不像老大哥那考究,低温烧制,一个字,快!主打一个从人民中来,到人民中去。

两者肉眼还是容易区分的,前者由于高岭土含铁元素较高,高温烧制后骚了点,脸颊上泛着隐约的青涩。小老弟则含钾多些,低温烧制属软性瓷,通透,特白,所以两者在颜色上可以很直观的区分出来。

正在工作的盖碗

正在工作的斗笠杯

打了这么多字,我手都把不住门了,望着盖碗中的铁观音,捏住这斗笠杯,一饮而尽!入口清香,回甘绵长,这看着锅里的,吃着碗里的,神仙来了也上头啊!

❌
❌