前言

在博客中,除了长篇的技术文章,我们有时也想分享一些简短的想法、生活感悟或者日常动态。说说功能就像微博、朋友圈一样,可以让我们快速记录生活中的点滴,让博客更加生动有趣。

本教程将教你如何在 Hexo + Butterfly 主题中实现一个完整的说说功能。

功能特点

核心功能

  • 📝 支持发布简短动态
  • 📅 自动按时间降序排序
  • 🕐 智能时间显示(刚刚、几分钟前、具体日期)
  • 📍 支持显示发布来源(Web、手机等)
  • 🎨 精美的卡片式设计
  • 📱 完美的响应式布局
  • 🌙 深色模式适配
  • ✨ 流畅的动画效果

实现步骤

第一步:创建说说页面

使用 Hexo 命令创建说说页面:

1
hexo new page talking

编辑 source/talking/index.md,设置页面类型:

1
2
3
4
5
6
7
8
9
10
---
title: 说说
date: 2026-01-07 18:35:09
type: talking
comments: true
---

<div id="talking-container">
<div class="talking-loading">加载中...</div>
</div>

第二步:创建数据文件

source/_data/ 目录下创建 talking.yml 文件(如果 _data 目录不存在,需要先创建):

1
2
3
4
5
6
7
8
9
10
11
- content: 今天天气真好,适合写代码!☀️
date: 2026-01-07 10:00:00
from: Web

- content: 刚刚完成了 Hexo 博客的 PWA 功能配置,现在支持离线访问了!🎉
date: 2026-01-07 14:30:00
from: Web

- content: 学习新技术的过程虽然辛苦,但看到成果的那一刻真的很有成就感 💪
date: 2026-01-06 20:15:00
from: Web

数据格式说明

  • content: 说说内容(支持 emoji)
  • date: 发布时间(格式:YYYY-MM-DD HH:mm:ss)
  • from: 发布来源(可选,如:Web、iPhone、Android)

第三步:创建生成器脚本

scripts/ 目录下创建 talking-generator.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const yaml = require('js-yaml');
const fs = require('fs');
const path = require('path');

hexo.extend.generator.register('talking',ction(locals) {
const talkingPath = path.join(hexo.source_dir, '_data', 'talking.yml');

if (!fs.existsSync(talkingPath)) {
return [];
}

try {
consa = yaml.load(fs.readFileSync(talkingPath, 'utf8'));

// 按日期降序排序
if (Array.isArray(talkingData)) {
talkingData.sort((a, b) => new Date(b.date) - new Date(a.date));
}

return [{
path: 'talking.json',
data: JSON.stringify(talkingData || [])
}];
} catch (error) {
console.error('生成说说数据失败:', error);
return [];
}
});

脚本功能

  • 读取 talking.yml 文件
  • 将 YAML 数据转换为 JSON 格式
  • 按日期降序排序
  • 生成 talking.json 供前端调用

第四步:创建样式文件

source/css/ 目录下创建 talking.css

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
/* 说说页面样式 */
#talking-container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}

.talking-item {
background: var(--card-bg);
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
animation: fadeInUp 0.6s ease-out;
}

.talking-item:hover {
transform: translateY(-5px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
}

.talking-content {
font-size: 16px;
line-height: 1.8;
color: var(--font-color);
margin-bottom: 15px;
word-wrap: break-word;
}

.talking-meta {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 14px;
color: var(--font-color);
opacity: 0.7;
}

.talking-date {
display: flex;
align-items: center;
}

.talking-date i {
margin-right: 5px;
}

.talking-from {
display: flex;
align-items: center;
}

.talking-from i {
margin-right: 5px;
}

/* 加载动画 */
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}

/* 响应式设计 */
@media screen and (max-width: 768px) {
#talking-container {
padding: 10px;
}

.talking-item {
padding: 15px;
}

.talking-content {
font-size: 14px;
}

.talking-meta {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
}

/* 暗黑模式适配 */
[data-theme="dark"] .talking-item {
background: var(--card-bg);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}

[data-theme="dark"] .talking-item:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
}

样式特点

  • 使用 CSS 变量适配主题
  • 卡片式设计,悬停效果
  • 渐入动画
  • 响应式布局
  • 深色模式支持

第五步:创建 JavaScript 文件

source/js/ 目录下创建 talking.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
// 说说页面功能
document.addEventListener('DOMContentLoaded', function() {
// 检查是否在说说页面
if (window.location.pathname.includes('/talking/')) {
loadTalking();
}
});

// 加载说说数据
function loadTalking() {
fetch('/talking.json')
.then(response => response.json())
.then(data => {
renderTalking(data);
})
.catch(error => {
console.error('加载说说数据失败:', error);
showError();
});
}

// 渲染说说列表
function renderTalking(talkingList) {
const container = document.getElementById('talking-container');

if (!container) {
console.error('找不到说说容器');
return;
}

if (!talkingList || talkingList.length === 0) {
container.innerHTML = '<div class="talking-empty">暂无说说</div>';
return;
}

let html = '';
talkingList.forEach((item, index) => {
html += `
<div class="talking-item" style="animation-delay: ${index * 0.1}s">
<div class="talking-content">${escapeHtml(item.content)}</div>
<div class="talking-meta">
<div class="talking-date">
<i class="far fa-clock"></i>
<span>${formatDate(item.date)}</span>
</div>
<div class="talking-from">
<i class="fas fa-map-marker-alt"></i>
<span>${escapeHtml(item.from || 'Web')}</span>
</div>
</div>
</div>
`;
});

container.innerHT html;
}

// 格式化日期
function formatDate(dateString) {
const date = new Date(dateString);
const now = new Date();
const diff = now - date;

// 小于1分钟
if (diff < 60000) {
return '刚刚';
}

// 小于1小时
if (diff < 3600000) {
return Math.floor(diff / 60000) + ' 分钟前';
}

// 小于1天
if (diff < 86400000) {
return Math.floor(diff / 3600000) + ' 小时前';
}

// 小于7天
if (diff < 604800000) {
return Math.floor(diff / 86400000) + ' 天前';
}

// 超过7天,显示具体日期
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');

return `${year}-${month}-${day} ${hours}:${minutes}`;
}

// HTML 转义
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}

// 显示错误信息
function showError() {
const container = document.getElementById('talking-container');
if (container) {
container.innerHTML = `
<div class="talking-error">
<i class="fas fa-exclamation-triangle"></i>
<p>加载说说失败,请稍后再试</p>
</div>
`;
}
}

功能说明

  • 自动检测说说页面并加载数据
  • 智能时间格式化(相对时间和绝对时间)
  • HTML 转义防止 XSS 攻击
  • 错误处理和友好提示
  • 渐进式动画效果

第六步:注入 CSS 和 JS

编辑 _config.butterfly.yml,在 inject 部分添加:

1
2
3
4
5
inject:
head:
- <link rel="stylesheet" href="/css/talking.css"> # 说说页面
bottom:
- <script src="/js/talking.js"></script>

第七步:添加导航菜单

_config.butterfly.ymlmenu 部分添加说说入口:

1
2
3
4
5
6
menu:
首页: / || fas fa-home
时间轴: /archives/ || fas fa-archive
标签: /tags/ || fas fa-tags
分类: /categories/ || fas fa-folder-open
说说: /talking/ || fas fa-comments # 新增

第八步:生成并测试

1
2
3
4
5
6
7
8
# 清理缓存
hexo clean

# 生成网站
hexo generate

# 启动本地服务器
hexo server

访问 http://localhost:4000/talking/ 查看效果。

使用说明

发布新说说

编辑 source/_data/talking.yml,在文件开头添加新的说说:

1
2
3
- content: 这是一条新的说说 🎉
date: 2026-01-08 10:00:00
from: Web

然后重新生成网站:

1
hexo clean && hexo generate

时间格式说明

时间显示规则:

  • 刚刚:1分钟内
  • X 分钟前:1小时内
  • X 小时前:24小时内
  • X 天前:7天内
  • 具体日期:超过7天显示 YYYY-MM-DD HH:mm

自定义样式

你可以修改 talking.css 来自定义样式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* 修改卡片背景色 */
.talking-item {
background: #f8f9fa;
}

/* 修改文字颜色 */
.talking-content {
color: #333;
}

/* 修改悬停效果 */
.talking-item:hover {
transform: translateY(-8px);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.2);
}

进阶功能

1. 添加图片支持

修改数据格式:

1
2
3
4
5
6
- content: 今天拍到的美丽日落 🌅
date: 2026-01-08 18:30:00
from: iPhone
images:
- /images/sunset1.jpg
- /images/sunset2.jpg

修改 talking.js 的渲染函数:

1
2
3
4
5
6
7
8
// 在 renderTalking 函数中添加图片渲染
if (item.images && item.images.length > 0) {
html += '<div class="talking-images">';
item.images.forEach(img => {
html += `<img src="${img}" alt="说说图片" class="talking-image">`;
});
html += '</div>';
}

2. 添加点赞功能

可以集成 LeanCloud 或其他后端服务实现点赞功能。

3. 添加评论功能

说说页面已经支持评论,你可以在每条说说下添加评论区。

4. 添加标签分类

1
2
3
4
5
6
- content: 学习 Vue 3 的新特性
date: 2026-01-08 10:00:00
from: Web
tags:
- 前端
- Vue

常见问题

Q1: 说说不显示怎么办?

检查步骤

  1. 确认 talking.yml 文件存在且格式正确
  2. 检查浏览器控制台是否有错误
  3. 确认 talking.json 是否生成(在 public/ 目录下)
  4. 检查 CSS 和 JS 是否正确注入

Q2: 时间显示不正确?

确保 talking.yml 中的日期格式正确:YYYY-MM-DD HH:mm:ss

Q3: 样式显示异常?

  1. 清理缓存:hexo clean
  2. 检查 CSS 文件路径是否正确
  3. 确认主题变量是否定义

Q4: 如何批量导入说说?

可以编写脚本从其他平台(如微博、Twitter)导出数据,然后转换为 YAML 格式。

性能优化

1. 分页加载

当说说数量较多时,可以实现分页:

1
2
3
4
5
6
7
8
9
const pageSize = 10;
let currentPage = 1;

function renderPage(data, page) {
const start = (page - 1) * pageSize;
const end = start + pageSize;
const pageData = data.slice(start, end);
renderTalking(pageData);
}

2. 懒加载图片

如果说说包含图片,使用懒加载提升性能:

1
<img src="placeholder.jpg" data-src="${img}" class="lazyload">

3. 缓存优化

利用浏览器缓存减少请求:

1
2
3
4
const cachedData = localStorage.getItem('talking-cache');
if (cachedData) {
renderTalking(JSON.parse(cachedData));
}

总结

通过本教程,我们实现了一个完整的说说功能,包括:

数据管理:使用 YAML 文件管理说说数据
自动生成:通过 Hexo 生成器自动转换为 JSON
页面渲染:动态加载和渲染说说列表
样式设计:精美的卡片式设计和动画效果
响应式布局:完美适配各种设备
深色模式:自动适配主题模式

说说功能让博客更加生动有趣,可以记录生活中的点滴,与访客分享日常。你可以根据自己的需求进一步扩展功能,如添加图片、标签、点赞等。

相关文章


💡 提示:如果你觉得这篇教程对你有帮助,欢迎点赞、收藏和分享!有任何问题欢迎在评论区留言。