此文章仅适用于已经安装好Qexo并已经适配其说说界面的hexo主题,单独使用无效!

Qexo有其独有的说说配置,和众多hexo主题的说说功能并不兼容

由于美化主题自带的说说界面需要改动主题本身的文件,对以后的主题升级很不友好

而Qexo的说说可以通过引入静态资源文件进行美化

于是博主就写了一个仿UyoAhz大佬的(大佬的代码在github是加密的,可个性化性不高)

食用方法:

在source/talks/index.md中添加如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<head>
<!-- ... -->
<link rel="stylesheet" href="https://fastly.jsdelivr.net/gh/kuiyr0810/qexo-talks@main/suns/talk.min.css">
<script src="//fastly.jsdelivr.net/gh/kuiyr0810/qt@main/suns/talk.min.js"></script>
<!-- ... -->
</head>
<body>
<!-- ... -->
<div id="my-shouts-container"></div>
<script>
myQexoShouts.init({
el: "#my-shouts-container",
avatar: "https://img.kuiyr.de/file/1753932255231_image.png", // 你的头像
name: "Sunshine.", // 你的名字
limit: 10, // 加载几条
baseURL: "https://admin.example.com" // 你的Qexo API地址
}).catch(function(error) {
console.error("加载过程中出现问题:", error);
});
</script>
</body>

保存推送即可!

源码在kuiyr0810/qexo-talks: qexo简洁大方说说界面

主要引用了两个文件

一个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
 myQexoShouts = {
talks: [],
currentPage: 1,
isLoading: false,
config: {},

_formatTime: function(timestamp) {
// 检查时间戳是否是10位数(秒),如果是,则乘以1000转为毫秒
const ts = timestamp.toString().length === 10 ? Number(timestamp) * 1000 : Number(timestamp);
// ----------------------

const now = new Date();
const past = new Date(ts);
const diffInSeconds = Math.floor((now.getTime() - past.getTime()) / 1000);

const minute = 60;
const hour = minute * 60;
const day = hour * 24;
const week = day * 7;

if (diffInSeconds < minute) {
return '刚刚';
} else if (diffInSeconds < hour) {
return `${Math.floor(diffInSeconds / minute)}分钟前`;
} else if (diffInSeconds < day) {
return `${Math.floor(diffInSeconds / hour)}小时前`;
} else if (diffInSeconds < week) {
return `${Math.floor(diffInSeconds / day)}天前`;
} else {
const year = past.getFullYear();
const month = (past.getMonth() + 1).toString().padStart(2, '0');
const date = past.getDate().toString().padStart(2, '0');
return `${year}-${month}-${date}`;
}
},


_createTalkItemHTML: function(talk) {
const formattedTime = this._formatTime(talk.time);
const isoTime = new Date(Number(talk.time)).toISOString();

const tagsHTML = talk.tags.map(tag =>
`<a class="qexot-tag-item">#${tag}</a>`
).join('');
// ----------------------

const likedIcon = `<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" fill="red"><path transform="scale(0.03,0.03)" d="M0 190.9V185.1C0 115.2 50.52 55.58 119.4 44.1C164.1 36.51 211.4 51.37 244 84.02L256 96L267.1 84.02C300.6 51.37 347 36.51 392.6 44.1C461.5 55.58 512 115.2 512 185.1V190.9C512 232.4 494.8 272.1 464.4 300.4L283.7 469.1C276.2 476.1 266.3 480 256 480C245.7 480 235.8 476.1 228.3 469.1L47.59 300.4C17.23 272.1 0 232.4 0 190.9L0 190.9z"/></svg>`;
const unlikeIcon = `<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16"><path transform="scale(0.03,0.03)" d="M244 84L255.1 96L267.1 84.02C300.6 51.37 347 36.51 392.6 44.1C461.5 55.58 512 115.2 512 185.1V190.9C512 232.4 494.8 272.1 464.4 300.4L283.7 469.1C276.2 476.1 266.3 480 256 480C245.7 480 235.8 476.1 228.3 469.1L47.59 300.4C17.23 272.1 0 232.4 0 190.9V185.1C0 115.2 50.52 55.58 119.4 44.1C164.1 36.51 211.4 51.37 244 84C243.1 84 244 84.01 244 84L244 84zM255.1 163.9L210.1 117.1C188.4 96.28 157.6 86.4 127.3 91.44C81.55 99.07 48 138.7 48 185.1V190.9C48 219.1 59.71 246.1 80.34 265.3L256 429.3L431.7 265.3C452.3 246.1 464 219.1 464 190.9V185.1C464 138.7 430.4 99.07 384.7 91.44C354.4 86.4 323.6 96.28 301.9 117.1L255.1 163.9z"/></svg>`;

return `
<div class="qexot-item" id="qexot-item-${talk.id}">
<div class="qexot-header">
<img class="qexot-avatar" src="${this.config.avatar}" alt="${this.config.name}" onerror="this.style.display='none'">
<div class="qexot-user-info">
<span class="qexot-name">${this.config.name}</span>
<time class="qexot-datatime" datetime="${isoTime}">${formattedTime}</time>
</div>
<div class="qexot-tags-container">${tagsHTML}</div>
</div>
<div class="qexot-content">
<div class="datacont">${talk.content}</div>
</div>
<div class="qexot-bottom">
<a class="qexot-like" onclick="myQexoShouts.likeTalk('${talk.id}')">
${talk.liked ? likedIcon : unlikeIcon}
<span class="qexot-like-count">${talk.like}</span>
</a>
</div>
</div>
`;
},

_render: function(items, append = false) { /* ... */ },
_updateLoadMoreButton: function(totalCount) { /* ... */ },
loadMoreTalks: async function() { /* ... */ },
likeTalk: async function(id) { /* ... */ },

init: function(userConfig) {
this.config = {
el: null,
baseURL: null,
limit: 5,
name: 'Qexo User',
avatar: '',
timeFormat: 'YYYY-mm-dd HH:MM:SS',
};

if (!this.config.el || !this.config.baseURL) {
console.error("Qexo Talks Error: 'el' and 'baseURL' are required.");
return;
}
this.loadMoreTalks();
}
};

myQexoShouts._render = function(items, append = false) {const container = document.querySelector(this.config.el);if (!container) return;const listContainer = container.querySelector('.qexot-list');if (!append && !listContainer) {container.innerHTML = `<section class="qexot"><div class="qexot-list"></div></section>`;}const targetList = container.querySelector('.qexot-list');const itemsHTML = items.map(talk => this._createTalkItemHTML(talk)).join('');targetList.insertAdjacentHTML('beforeend', itemsHTML);};
myQexoShouts._updateLoadMoreButton = function(totalCount) {const container = document.querySelector(this.config.el);let moreButton = document.getElementById("qexot-more");if (moreButton) moreButton.remove();if (this.talks.length < totalCount) {const buttonHTML = `<center id="qexot-more"><div class="qexot-more" onclick="myQexoShouts.loadMoreTalks()">加载更多</div></center>`;container.insertAdjacentHTML('beforeend', buttonHTML);}};
myQexoShouts.loadMoreTalks = async function() {if (this.isLoading) return;this.isLoading = true;const container = document.querySelector(this.config.el);const moreButton = document.getElementById("qexot-more");if (moreButton) moreButton.innerHTML = '加载中...';if (this.currentPage === 1) {container.innerHTML = '<div class="qexo_loading"><p style="text-align: center; display: block">说说加载中...</p></div>';}try {const url = new URL('/pub/talks/', this.config.baseURL);url.searchParams.append('page', this.currentPage);url.searchParams.append('limit', this.config.limit);const response = await fetch(url.href);if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);const res = await response.json();if (res.status) {this._render(res.data, this.currentPage > 1);this.talks = this.talks.concat(res.data);this._updateLoadMoreButton(res.count);this.currentPage++;} else {throw new Error(res.data.msg || "API returned an error.");}} catch (error) {console.error("Failed to fetch talks:", error);if (this.currentPage === 1) {container.innerHTML = '<blockquote>说说加载失败,请检查 API 地址或网络连接。<br>错误详情请查看 F12 控制台。</blockquote>';}} finally {this.isLoading = false;}};
myQexoShouts.likeTalk = async function(id) {const url = new URL('/pub/like_talk/', this.config.baseURL);const talk = this.talks.find(t => t.id === id);if (!talk) return;try {const response = await fetch(url.href, {method: 'POST',headers: { 'Content-Type': 'application/x-www-form-urlencoded' },body: `id=${id}`});if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);const res = await response.json();if (res.status) {talk.liked = res.action;talk.like += res.action ? 1 : -1;const talkElement = document.getElementById(`qexot-item-${id}`);if (talkElement) {const likeElement = talkElement.querySelector('.qexot-like');const likeCountElement = likeElement.querySelector('.qexot-like-count');const svgElement = likeElement.querySelector('svg');const newIcon = this._createTalkItemHTML(talk).match(/<a class="qexot-like".*?>([\s\S]*?)<\/a>/s)[1].trim().split(/<span/)[0];svgElement.outerHTML = newIcon;likeCountElement.textContent = talk.like;}} else {throw new Error(res.data.msg || "Like action failed.");}} catch (error) {console.error("Failed to like talk:", error);}};

一个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
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
<style>

/* 基础重置,确保内部元素盒模型统一 */
#my-shouts-container *,
#my-shouts-container *::before,
#my-shouts-container *::after {
box-sizing: border-box;
}

/* 卡片阴影与悬浮特效 */

#my-shouts-container .qexot-item {
background-color: #fff;
padding: 16px;
border-radius: 8px;
margin-bottom: 20px;
overflow: hidden;

/* 1. 默认状态下的阴影 */
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.04), 0 1px 3px rgba(0, 0, 0, 0.06);

/* 2. 平滑过渡效果的关键 */
/* 让 transform 和 box-shadow 属性的变化在 0.3 秒内平滑完成 */
transition: transform 0.3s ease, box-shadow 0.3s ease;
}

/* 3. 鼠标悬浮时的动效 */
#my-shouts-container .qexot-item:hover {
/* 卡片向上平移 5 像素,产生“浮起”效果 */
transform: translateY(-5px);

/* 阴影变得更深、更广,增强立体感 */
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.06), 0 4px 12px rgba(0, 0, 0, 0.08);
}


/* 头部容器: 保持 flex 布局 */
#my-shouts-container .qexot-header {
display: flex !important;
align-items: center !important; /* 垂直居中对齐所有项 */
margin-bottom: 16px;
}

/* --- 动态头像边框样式 --- */
@keyframes pulseBorder {
0% {
border-color: #007bff; /* 初始边框颜色 */
box-shadow: 0 0 0 0 rgba(0, 123, 255, 0.5); /* 初始阴影大小 */
}
70% {
border-color: #66a5ff; /* 中间边框颜色 */
box-shadow: 0 0 0 8px rgba(0, 123, 255, 0); /* 阴影扩大并消失 */
}
100% {
border-color: #007bff; /* 最终边框颜色 */
box-shadow: 0 0 0 0 rgba(0, 123, 255, 0); /* 阴影恢复 */
}
}

#my-shouts-container .qexot-avatar {
/* 保留原有的头像样式 */
width: 50px;
height: 50px;
border-radius: 50%;
object-fit: cover;
flex-shrink: 0;
margin: 0 12px 0 0 !important;

/* 添加动态边框 */
border: 2px solid #007bff; /* 初始边框 */
animation: pulseBorder 2s infinite alternate; /* 应用动画 */
}

/* 用户信息容器: 让内部元素垂直排列 */
#my-shouts-container .qexot-user-info {
display: flex;
flex-direction: column;
}

/* 用户名: 无需额外边距 */
#my-shouts-container .qexot-name {
font-weight: 600;
font-size: 16px;
color: #333;
margin-right: 0; /* 必须移除大的边距 */
}

/* 时间: 无需额外边距 */
#my-shouts-container .qexot-datatime {
font-size: 12px;
color: #999;
margin-top: 2px;
}

/* 内容区域 */
#my-shouts-container .qexot-content {
font-size: 15px;
line-height: 1;
color: #383838;
white-space: pre-wrap;
font-family: "Kaiti SC", KaiTi, "FangSong", "Apple LiSung", "STKaiti", cursive;
}


/* 标签的容器,让标签们靠右对齐 */
#my-shouts-container .qexot-tags-container {
margin-left: auto; /* 将整个标签容器推到最右边 */
display: flex;
flex-wrap: wrap; /* 如果标签太多,允许换行 */
justify-content: flex-end; /* 让标签在容器内也靠右 */
}

/* 单个标签的样式 */
#my-shouts-container .qexot-tag-item {
background-color: #f0f4ff;
color: #4a6dff;
font-size: 12px;
padding: 4px 10px;
border-radius: 12px;
text-decoration: none;
white-space: nowrap;
margin-left: 6px; /* 标签之间的间距 */
margin-bottom: 4px; /* 标签换行时的垂直间距 */
}


#my-shouts-container .qexot-content .datacont {
clear: both;
}

/* 图片样式 */
#my-shouts-container .qexot-content img {
display: block;
max-width: 60%;
border-radius: 6px;
margin-top: 10px;
}

/* 底部区域 (点赞按钮) */
#my-shouts-container .qexot-bottom {
display: flex !important; /* 使用 !important 确保 flex 布局不被覆盖 */
justify-content: flex-end !important;
margin-top: 16px;
}

#my-shouts-container .qexot-like {
display: flex;
align-items: center;
color: #666;
cursor: pointer;
}

#my-shouts-container .qexot-like svg {
margin-right: 4px;
}

/* 加载更多按钮 */
#my-shouts-container .qexot-more {
background-color: #f0f0f0;
padding: 8px 20px;
border-radius: 20px;
cursor: pointer;
display: inline-block;
margin: 10px auto;
color: #555;
}
</style>

注释很详细了可以二次开发,改成自己喜欢的样式即可!