AI摘要
引言
在为博客文章实现AI摘要工具时,我选择了 大佬--旋的来AISummary插件增强功能(非常好用)。然而,在按大佬的教程配置完毕后,我发现摘要正文并没有输出,经研究后得知,因为阅读模式的存在,出现了两个摘要容器,而大佬的自定义样式不会处理多个摘要容器,导致第二个摘要容器正文无法正常显示(当时不知道(⊙︿⊙))。本文将详细记录这一问题的排查与解决方案,并在文末给出增强后的完整自定义样式代码。
插件文章链接:Typecho添加文章AI摘要功能(Handsome等全主题适配)By xuan
问题描述
使用环境
- 博客平台: Typecho
- 博客主题: bearsimple
具体问题现象
摘要正文未能正常加载
排查过程
分析文章页的HTML文档
可以看到,同时存在两个摘要容器,分别位于阅读模式页面处和文章页面处,并且阅读模式的容器为1号位,且1号位容器内容正常加载,而2号位为空值。故产生的现象为文章处摘要不加载,而阅读模式不会自动被使用,本人对该主题也是刚接触使用,不甚熟悉,故对本人的排查造成了不小的困扰。

分析源代码
主要的逻辑在executeAiSummaryTyping()中,其只处理了第一个容器
function executeAiSummaryTyping() {
// 【未增强版本】
// 使用 document.querySelector() 只会获取页面上第一个匹配 '.ai-typewriter-text' 的元素。
// 这意味着如果页面上有多个AI摘要容器,只有第一个摘要会执行打字机效果。
const typewriterElement = document.querySelector('.ai-typewriter-text');
// 获取隐藏的源文本元素。
const sourceTextElement = document.querySelector('.ai-hidden-text');
// 定义打字速度。
const typingSpeed = 50;
// 如果没有找到对应的元素,则直接返回。
if (!typewriterElement || !sourceTextElement) return;
// 【未增强版本】
// 使用全局变量 aiSummaryTypingTimeoutId 来管理定时器。
// 当页面上有多个摘要时,如果前一个摘要的打字机效果还在进行中,新的打字机效果可能会清除掉旧的定时器,
// 导致效果异常或只显示一个摘要的打字机效果。
if (aiSummaryTypingTimeoutId) {
clearTimeout(aiSummaryTypingTimeoutId);
}
// 获取并处理源文本,添加首行缩进。
let textToType = sourceTextElement.textContent.trim();
if (textToType.length > 0) {
textToType = ' ' + textToType; // 首行缩进
}
// 清空打字机文本内容。
typewriterElement.textContent = '';
// 初始化字符索引。
let charIndex = 0;
// 定义打字机效果的内部函数。
function typeNextCharacter() {
// 如果还有字符未输出。
if (charIndex < textToType.length) {
// 逐个添加字符。
typewriterElement.textContent += textToType.charAt(charIndex);
charIndex++;
// 设置定时器,继续输出下一个字符。
// 【未增强版本】这里仍然使用全局变量来存储定时器ID。
aiSummaryTypingTimeoutId = setTimeout(typeNextCharacter, typingSpeed);
} else {
// 所有字符输出完毕,清除全局定时器ID。
aiSummaryTypingTimeoutId = null;
}
}
// 启动打字机效果。
typeNextCharacter();
}
解决方案
对函数做多容器增强
function executeAiSummaryTyping() {
// 获取页面上所有带有 'aisummary' 类的元素,这些元素代表了不同的AI摘要容器。
const aiSummaryContainers = document.querySelectorAll('.aisummary');
// 遍历每一个找到的AI摘要容器。
aiSummaryContainers.forEach(container => {
// 在当前容器内部,查找带有 'ai-typewriter-text' 类的元素,这是显示打字机效果的文本区域。
const typewriterElement = container.querySelector('.ai-typewriter-text');
// 在当前容器内部,查找带有 'ai-hidden-text' 类的元素,这是存储原始摘要文本的隐藏区域。
const sourceTextElement = container.querySelector('.ai-hidden-text');
// 定义打字速度,数值越小,打字速度越快。
const typingSpeed = 50;
// 如果在当前容器中没有找到打字机文本元素或源文本元素,则跳过当前容器,不执行打字机效果。
if (!typewriterElement || !sourceTextElement) return;
// 清除可能存在的旧定时器。
// 这里将定时器ID存储在 'typewriterElement' 自身的属性 'typingTimeoutId' 上,
// 这样每个打字机元素都有独立的定时器,避免相互干扰,支持多摘要同时运行。
if (typewriterElement.typingTimeoutId) {
clearTimeout(typewriterElement.typingTimeoutId);
}
// 获取源文本内容并去除首尾空格。
let textToType = sourceTextElement.textContent.trim();
// 如果源文本不为空,则在文本开头添加两个空格,实现首行缩进效果。
if (textToType.length > 0) {
textToType = ' ' + textToType;
}
// 清空打字机文本元素的当前内容,准备开始打字。
typewriterElement.textContent = '';
// 初始化字符索引,用于追踪当前打字到哪个字符。
let charIndex = 0;
// 定义核心的打字机效果函数。
const typeNextCharacter = () => {
// 如果还有未输出的字符。
if (charIndex < textToType.length) {
// 将当前字符添加到打字机文本元素中。
typewriterElement.textContent += textToType.charAt(charIndex);
// 字符索引递增。
charIndex++;
// 设置一个定时器,在指定延迟后再次调用 'typeNextCharacter' 函数,实现逐字输出。
typewriterElement.typingTimeoutId = setTimeout(typeNextCharacter, typingSpeed);
} else {
// 所有字符都已输出完毕,清除该打字机元素的定时器ID。
typewriterElement.typingTimeoutId = null;
}
};
// 启动打字机效果,开始输出第一个字符。
typeNextCharacter();
});
}
并用伪元素实现字符缩进
.ai-typewriter-text {
display: inline;
word-wrap: break-word;
white-space: pre-wrap;
}
/* 缩进样式 */
.ai-typewriter-text::before {
content: '';
width: 2em; /* 调整宽度以匹配所需的缩进 */
height: 1em; /* 高度与字体大小相匹配 */
float: left;
margin-top: -0.5em; /* 调整这个值以对齐文本基线 */
}
验证修复效果
可以看到,两个容器都能正常加载数据,且缩进会更为合适
文章链接:效果展示

总结
通过这次经历,我记住了在排查前端页面的渲染加载问题时,可以直接从HTML文档出发,从元素本身出发,这样方便我们由果溯因,从而少走弯路,节省时间和精力。再次感谢大佬,不但开发了如此好用的插件(有类似插件付费提供商),还会积极讨论解决问题,希望我的经验能够帮助到其他遇到类似问题的朋友。
完整自定义样式
<!-- AI摘要样式 - 阅读模式多摘要容器增强版 -->
<style>
/* 摘要容器样式 */
.aisummary {
background: #f7f7f9;
border-radius: 12px;
padding: 12px;
box-shadow: 0 8px 16px -4px rgba(44, 45, 48, 0.047);
border: 1px solid #e3e8f7;
margin: 25px 0 30px;
color: #333;
position: relative;
overflow: hidden;
}
/* 标题样式 */
.ai-header {
margin-bottom: 10px !important;
color: #465CEB !important;
text-align: left !important;
display: flex !important;
align-items: center !important;
text-indent: 0 !important;
font-weight: bold !important;
font-size: 17px !important;
}
.ai-header svg {
margin-right: 8px;
width: 24px;
height: 24px;
stroke: currentColor;
}
/* 文本容器样式 */
.ai-text-container {
background: #fff;
border-radius: 8px;
padding: 12px 15px;
border: 1px solid #e3e8f7;
margin-bottom: 10px;
font-size: 15px;
line-height: 1.7;
color: #333;
}
.ai-hidden-text {
display: none;
}
.ai-typewriter-text {
display: inline;
word-wrap: break-word;
white-space: pre-wrap;
}
/* 缩进样式 */
.ai-typewriter-text::before {
content: '';
width: 2em; /* 调整宽度以匹配所需的缩进 */
height: 1em; /* 高度与字体大小相匹配 */
float: left;
margin-top: -0.5em; /* 调整这个值以对齐文本基线 */
}
/* 光标样式及动画 */
.ai-cursor {
display: inline-block;
width: 2px;
height: 1em;
background-color: #465CEB;
margin-left: 3px;
animation: ai-blink 0.7s infinite;
vertical-align: middle;
}
@keyframes ai-blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
/* 页脚样式 */
.ai-footer {
font-size: 13px !important;
color: rgba(60, 60, 67, 0.65) !important;
font-style: italic !important;
margin-bottom: 0 !important;
padding: 0 5px !important;
text-align: left !important;
text-indent: 0 !important;
margin-top: 10px !important;
}
/* 响应式调整 */
@media (max-width: 768px) {
.aisummary {
padding: 10px;
margin: 20px 0 25px;
}
.ai-header {
font-size: 16px !important;
}
.ai-header svg {
width: 22px;
height: 22px;
margin-right: 6px;
}
.ai-text-container {
font-size: 14px;
padding: 10px 12px;
line-height: 1.65;
}
.ai-footer {
font-size: 12px !important;
margin-top: 8px !important;
}
}
/* 暗色模式适配 */
[data-night="night"] .aisummary,
.dark-mode .aisummary,
body.dark .aisummary,
body.night .aisummary,
.night .aisummary,
.night-mode .aisummary,
html.night .aisummary,
.theme-dark .aisummary {
background: #2c2c2e;
border-color: #38383a;
color: #d1d1d1;
box-shadow: 0 8px 16px -4px rgba(0, 0, 0, 0.15);
}
[data-night="night"] .ai-text-container,
.dark-mode .ai-text-container,
body.dark .ai-text-container,
body.night .ai-text-container,
.night .ai-text-container,
.night-mode .ai-text-container,
html.night .ai-text-container,
.theme-dark .ai-text-container {
background: #333333;
border-color: #4a4a4a;
color: #c8c8c8;
}
[data-night="night"] .ai-header,
.dark-mode .ai-header,
body.dark .ai-header,
body.night .ai-header,
.night .ai-header,
.night-mode .ai-header,
html.night .ai-header,
.theme-dark .ai-header {
color: #7c89f1 !important;
}
[data-night="night"] .ai-cursor,
.dark-mode .ai-cursor,
body.dark .ai-cursor,
body.night .ai-cursor,
.night .ai-cursor,
.night-mode .ai-cursor,
html.night .ai-cursor,
.theme-dark .ai-cursor {
background-color: #7c89f1;
}
[data-night="night"] .ai-footer,
.dark-mode .ai-footer,
body.dark .ai-footer,
body.night .ai-footer,
.night .ai-footer,
.night-mode .ai-footer,
html.night .ai-footer,
.theme-dark .ai-footer {
color: rgba(200, 200, 200, 0.6) !important;
}
/* 手动添加的暗色模式类 */
.aisummary.ai-dark-theme {
background: #2c2c2e;
border-color: #38383a;
color: #d1d1d1;
box-shadow: 0 8px 16px -4px rgba(0, 0, 0, 0.15);
}
.ai-dark-theme .ai-text-container {
background: #333333;
border-color: #4a4a4a;
color: #c8c8c8;
}
.ai-dark-theme .ai-header {
color: #7c89f1 !important;
}
.ai-dark-theme .ai-cursor {
background-color: #7c89f1;
}
.ai-dark-theme .ai-footer {
color: rgba(200, 200, 200, 0.6) !important;
}
</style>
<!-- AI摘要打字机效果脚本 -->
<script>
// 全局变量
let aiSummaryTypingTimeoutId = null;
let aiSummaryLastProcessedUrl = window.location.href;
// AI摘要主题设置函数
window.aiSummaryUser = {
setAiSummaryTheme: function(theme) {
const summaryElements = document.querySelectorAll('.aisummary');
if (summaryElements.length > 0) {
summaryElements.forEach(element => {
if (theme === 'dark') {
element.classList.add('ai-dark-theme');
} else {
element.classList.remove('ai-dark-theme');
}
});
}
}
};
// 检测并同步Handsome主题的夜间模式
function detectAndSyncTheme() {
// 检查常见的夜间模式标识
const isDarkMode =
document.body.classList.contains('night') ||
document.body.classList.contains('dark') ||
document.documentElement.classList.contains('night') ||
document.documentElement.getAttribute('data-night') === 'night' ||
document.querySelector('html').classList.contains('dark-mode') ||
document.querySelector('[data-theme="dark"]') !== null;
// 应用对应的主题
if (isDarkMode) {
window.aiSummaryUser.setAiSummaryTheme('dark');
} else {
window.aiSummaryUser.setAiSummaryTheme('light');
}
}
// 打字机效果核心逻辑
function executeAiSummaryTyping() {
// 获取所有AI摘要容器
const aiSummaryContainers = document.querySelectorAll('.aisummary');
// 遍历每个AI摘要容器
aiSummaryContainers.forEach(container => {
// 获取当前容器内的打字机文本元素和隐藏的源文本元素
const typewriterElement = container.querySelector('.ai-typewriter-text');
const sourceTextElement = container.querySelector('.ai-hidden-text');
const typingSpeed = 50; // 打字速度,数值越小越快
// 如果没有找到对应的元素,则跳过当前容器
if (!typewriterElement || !sourceTextElement) return;
// 清除可能存在的旧定时器,防止重复执行
if (typewriterElement.typingTimeoutId) {
clearTimeout(typewriterElement.typingTimeoutId);
}
// 获取并处理源文本,添加首行缩进
let textToType = sourceTextElement.textContent.trim();
if (textToType.length > 0) {
textToType = ' ' + textToType; // 首行缩进
}
// 初始化打字机文本内容和字符索引
typewriterElement.textContent = '';
let charIndex = 0;
// 定义打字机效果函数
const typeNextCharacter = () => {
// 如果还有字符未输出
if (charIndex < textToType.length) {
// 逐个添加字符到打字机文本元素
typewriterElement.textContent += textToType.charAt(charIndex);
charIndex++;
// 设置定时器,继续输出下一个字符
typewriterElement.typingTimeoutId = setTimeout(typeNextCharacter, typingSpeed);
} else {
// 所有字符输出完毕,清除定时器ID
typewriterElement.typingTimeoutId = null;
}
};
// 启动打字机效果
typeNextCharacter();
});
}
// 页面加载后执行
document.addEventListener('DOMContentLoaded', function() {
// 延迟执行,确保页面元素加载完毕
setTimeout(() => {
executeAiSummaryTyping(); // 执行打字机效果
detectAndSyncTheme(); // 检测并同步主题
}, 300);
});
// PJAX/SPA兼容处理
setInterval(function() {
if (window.location.href !== aiSummaryLastProcessedUrl) {
aiSummaryLastProcessedUrl = window.location.href;
// 延迟执行,确保新内容加载完毕
setTimeout(() => {
executeAiSummaryTyping(); // 重新执行打字机效果
detectAndSyncTheme(); // 重新检测并同步主题
}, 1000);
}
}, 100);
// 监听主题切换
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.attributeName === 'class' || mutation.attributeName === 'data-night') {
detectAndSyncTheme();
}
});
});
// 开始观察文档和body元素上的class变化
document.addEventListener('DOMContentLoaded', function() {
observer.observe(document.documentElement, { attributes: true });
observer.observe(document.body, { attributes: true });
});
// 兼容Handsome主题的夜间模式切换事件
document.addEventListener('DOMContentLoaded', function() {
// 尝试找到夜间模式切换按钮并监听点击事件
const nightModeButtons = document.querySelectorAll('[data-toggle-theme], .theme-toggle, #nightmode, .night-mode-btn');
if (nightModeButtons.length > 0) {
nightModeButtons.forEach(button => {
button.addEventListener('click', function() {
// 延迟检测,确保主题切换完成
setTimeout(detectAndSyncTheme, 100);
});
});
}
});
// 如果存在全局主题切换函数,拦截它们以同步状态
if (typeof window.switchNightMode === 'function') {
const originalSwitchNightMode = window.switchNightMode;
window.switchNightMode = function() {
originalSwitchNightMode.apply(this, arguments);
setTimeout(detectAndSyncTheme, 100);
};
}
</script>
评论区(1条评论)
可以,看来黑暗模式也能兼容