说说笔记卡片
该组件是为增强中间件及自己发布文章的进一步关联,不同于RSS组件的展示,它使用的是说说笔记的原生API来加载显示,理论上,扩展性更强,可改造型更强。
使用前请先查看说说笔记开源仓库:https://github.com/rcy1314/echo-noise
说说笔记一键部署
docker run -d \
--name Ech0-Noise \
--platform linux/amd64 \
-p 1314:1314 \
-v /opt/data:/app/data \
noise233/echo-noise
其中请确保/opt/data文件夹中包含你原有的数据库文件noise.db,如果没有,可以去掉这个挂载命令,它也会自动创建
你也可以使用-v /opt/data/noise.db:/app/data/noise.db 来只挂载原有数据库,
默认用户名:admin
默认用户密码:admin
原始web组件可访问仓库中的htmlwidgets文件夹内。
远程数据库部署及fly.io无服务器部署请查看仓库中的说明
卡片功能:
浮动效果,可浮空文字组件保持了一致性,都可以双击关闭组件
折叠查看内容,支持收回折叠,同样内容支持了原始web组件的一些特性
如:支持对b站视频、YouTube、qq音乐、网易云音乐、github代码仓库的解析预览,
支持图片灯箱效果、支持标签路由及搜索功能等
独立于整个页面,展开内容上下滑动不会影响其它元素和组件
⚠️:目前和首页有冲突的在于tag元素标签的显示,如果添加了该组件,首页头像处的标签将和该组件效果保持一致
配置使用
首页html中添加
<!-- 说说卡片 -->
<link rel="stylesheet" href="./css/card-styles.css">
<link rel="stylesheet" href="./css/expand-card.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
<script src="https://unpkg.com/medium-zoom/dist/medium-zoom.min.js"></script>
<!-- 预加载关键资源 -->
<link rel="preload" href="./js/note.js" as="script">
<link rel="preload" href="./css/card-styles.css" as="style">
<div id="note-expand-card" class="expand-card-container delayed-slide-in">
<div class="expand-card-stack float-animate" id="expand-card-stack" ondblclick="hideCardPermanently()">
<div class="expand-card park-sec park-sec1">
<div class="park-inside">
<a class="avatar-link" id="expand-avatar" title="查看说说">
<img src="https://s2.loli.net/2025/03/24/HnSXKvibAQlosIW.png" alt="">
</a>
<div class="content-sec">
<a href="https://note.noisework.cn/" target="_blank" class="note-title-link">
<h2 style="margin-bottom: 2px; font-size: 16px;">「说说笔记」</h2>
</a>
<span id="typewriter-text">右侧展开查看,双击可关闭卡片</span>
</div>
</div>
<span class="date" id="expand-toggle-btn">展开 <span class="expand-arrow">▼</span></span>
</div>
<div class="expand-card park-sec park-sec2"></div>
<div class="expand-card park-sec park-sec3"></div>
</div>
<div id="expand-content" style="display:none;">
<div id="note">
<div class="note-header">
<div class="search-container">
<input type="text" id="tag-search" placeholder="搜索标签 #..." />
<button id="search-btn" target="_self">搜索</button>
</div>
</div>
<div class="note-container">
<div class="loading-wrapper">加载中...</div>
</div>
</div>
<div class="expand-bottom-divider"></div>
</div>
<!-- 确保所有脚本在内容加载后执行 -->
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="./js/expand-card.js"></script>
<script src="./js/jquery-3.2.1.js"></script>
<script>
window.note = {
host: 'https://note.noisework.cn', // 修改为自己的服务器地址
limit: '10',
domId: '#note',
sourceName: '「说说笔记」',
loadingTimeout: 10000 // 添加加载超时时间配置
};
// 添加加载超时处理
setTimeout(() => {
const loadingWrapper = document.querySelector('.loading-wrapper');
if (loadingWrapper && loadingWrapper.style.display !== 'none') {
loadingWrapper.textContent = '加载失败,请刷新重试';
}
}, window.note.loadingTimeout);
marked.setOptions({
breaks: true,
gfm: true,
});
</script>
<script src="./js/note.js"></script>
注意,上面代码是针对首页html中已引入了一些关键js和css文件而添加的
原始如下(不要使用,除非你的首页没有如APlayer.min.css等文件):
✅ 原始【点击查看】
<link rel="stylesheet" href="./css/APlayer.min.css">
<link rel="stylesheet" href="./css/styles.css">
<link rel="stylesheet" href="./css/waline.css">
<link rel="stylesheet" href="./css/expand-card.css">
<base target="_blank">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
<script src="https://unpkg.com/medium-zoom/dist/medium-zoom.min.js"></script>
<!-- 添加代码高亮支持 -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.24.1/themes/prism-tomorrow.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.24.1/prism.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.24.1/components/prism-javascript.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.24.1/components/prism-css.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.24.1/components/prism-python.min.js"></script>
<!-- 预加载关键资源 -->
<link rel="preload" href="./js/note.js" as="script">
<link rel="preload" href="./css/styles.css" as="style">
<!-- 按需加载非关键资源 -->
<link rel="stylesheet" href="./css/APlayer.min.css" media="print" onload="this.media='all'">
</head>
<body>
<div id="note-expand-card" class="expand-card-container">
<div class="expand-card-stack" id="expand-card-stack">
<div class="expand-card park-sec park-sec1">
<div class="park-inside">
<img src="https://s2.loli.net/2025/03/24/HnSXKvibAQlosIW.png" alt="">
<div class="content-sec">
<h2>说说笔记</h2>
<span>来自noise的说说笔记</span>
</div>
</div>
<span class="date" id="expand-toggle-btn">展开 <span class="expand-arrow">▼</span></span>
</div>
<div class="expand-card park-sec park-sec2"></div>
<div class="expand-card park-sec park-sec3"></div>
</div>
<div id="expand-content" style="display:none;">
<div id="note">
<div class="note-header">
<div class="search-container">
<input type="text" id="tag-search" placeholder="搜索标签 #..." />
<button id="search-btn" target="_self">搜索</button>
</div>
</div>
<div class="note-container">
<div class="loading-wrapper">加载中...</div>
</div>
</div>
</div>
</div>
<!-- 确保所有脚本在内容加载后执行 -->
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="./js/jquery-3.2.1.js"></script>
<script src="./js/APlayer.min.js"></script>
<script src="./js/Meting.min.js"></script>
<script src="./js/expand-card.js"></script>
<script>
window.note = {
host: 'https://note.noisework.cn',
limit: '10',
domId: '#note',
commentServer: 'https://app-produ.up.railway.app',
sourceName: '「说说笔记」',
loadingTimeout: 10000 // 添加加载超时时间配置
};
// 添加加载超时处理
setTimeout(() => {
const loadingWrapper = document.querySelector('.loading-wrapper');
if (loadingWrapper && loadingWrapper.style.display !== 'none') {
loadingWrapper.textContent = '加载失败,请刷新重试';
}
}, window.note.loadingTimeout);
marked.setOptions({
breaks: true,
gfm: true,
});
</script>
<script src="./js/note.js"></script>
你只需要修改 host: 'https://note.noisework.cn', 地址为自己的服务器地址即可显示说说笔记的内容
不要忘记确实引入了相关js和css文件到项目中,其中一些css和js已改造适配首页显示,和原始文件不同
✅ 引入的文件【点击查看】
card-styles.css:
.note-header {
padding: 16px;
position: sticky;
top: 0;
z-index: 100;
}
.search-container {
display: flex;
gap: 8px;
max-width: 600px;
margin: 0 auto;
}
#tag-search {
flex: 1;
padding: 8px 12px;
border: 1px solid rgba(63, 60, 60, 0.529);
border-radius: 6px;
font-size: 14px;
background: rgba(255, 255, 255, 0.1);
color: #ffffff;
}
#search-btn {
padding: 8px 16px;
background: #1a73e8;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
transition: background 0.2s;
}
#search-btn:hover {
background: #2a405d;
}
.note-container {
max-width: 600px; /* 原为750px,缩小容器宽度 */
min-width: 200px; /* 可适当缩小 */
width: 88%; /* 原为92%,缩小整体宽度 */
margin: 0 auto;
padding: 12px; /* 原为16px,缩小内边距 */
overflow: hidden;
}
/* 针对Webkit浏览器隐藏滚动条 */
body::-webkit-scrollbar {
display: none;
}
.notecard {
background: #2b3038;
border-radius: 12px;
padding: 12px; /* 原为16px,缩小卡片内边距 */
margin-bottom: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
transition: transform 0.2s;
overflow: hidden;
}
.notecard-description {
font-size: 14px;
color: #e0e0e0;
line-height: 1.6;
word-wrap: break-word;
overflow-wrap: break-word;
overflow: hidden;
transition: max-height 0.3s ease-in-out;
}
.notecard-description img {
max-width: 100%;
height: auto;
display: block;
margin: 10px 0;
border-radius: 6px;
}
/* 媒体容器样式 */
.note-container .video-wrapper,
.note-container .music-wrapper,
.note-container .spotify-wrapper {
max-width: 100%;
margin: 6px 0;
}
.notecard:hover {
transform: translateY(-2px);
}
.notecard-title {
font-size: 16px;
background: linear-gradient(45deg, #ff6b6b, #4ecdc4);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
margin: 0 0 8px 0;
font-weight: bold;
}
/* 添加图片自适应样式 */
.notecard-description img {
max-width: 100%;
height: auto;
border-radius: 8px;
margin: 10px 0;
}
/* 优化代码块样式 */
.notecard-description pre {
background: #1e1e1e;
border-radius: 8px;
padding: 16px;
overflow-x: auto;
margin: 10px 0;
}
.notecard-description code {
font-family: 'Fira Code', monospace;
font-size: 14px;
color: #d4d4d4;
background: #1e1e1e;
padding: 2px 6px;
border-radius: 4px;
}
/* 行内代码样式 */
.notecard-description p code {
background: rgba(255, 255, 255, 0.1);
color: #e0e0e0;
}
/* 优化遮罩渐变效果 */
.content-mask {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 120px;
pointer-events: none;
display: none;
}
/* 优化展开按钮样式 */
.expand-btn {
position: relative;
z-index: 2;
margin: -40px auto 0;
padding: 8px 16px;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(5px);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 20px;
color: #e0e0e0;
font-size: 13px;
cursor: pointer;
transition: all 0.3s ease;
display: none;
width: fit-content;
}
.expand-btn:hover {
background: rgba(255, 255, 255, 0.2);
transform: translateY(-1px);
}
.tag {
color: rgb(120, 118, 226); /* 文本橙色 */
cursor: pointer;
}
.back-to-list, .load-more {
background: linear-gradient(145deg, #f0f0f0, #e6e6e6);
border: none;
padding: 12px 24px;
border-radius: 25px;
color: #eb761c;
font-size: 14px;
cursor: pointer;
margin: 20px auto;
display: block;
transition: all 0.3s ease;
}
.back-to-list:hover, .load-more:hover {
background: linear-gradient(145deg, #e6e6e6, #f0f0f0);
color: #ff8c00;
transform: translateY(-2px);
}
.back-to-list:active, .load-more:active {
transform: translateY(1px);
}
.load-more:hover {
background: linear-gradient(145deg, #e6e6e6, #f0f0f0);
color: #333;
transform: translateY(-2px);
}
.load-more:active {
transform: translateY(1px);
}
.loaded-all {
text-align: center;
color: #999;
padding: 20px;
font-size: 14px;
}
.loading-wrapper {
text-align: center;
color: #e0e0e0;
padding: 20px;
}
@media (max-width: 768px) {
.note-container {
width: 96%;
padding: 8px; /* 原为12px,移动端进一步缩小 */
}
.notecard {
padding: 8px; /* 原为12px,移动端进一步缩小 */
}
}
.video-wrapper {
position: relative;
padding-bottom: 56.25%; /* 16:9 比例 */
height: 0;
overflow: hidden;
margin: 10px 0;
}
.video-wrapper iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.music-wrapper {
margin: 10px 0;
}
.spotify-wrapper {
margin: 10px 0;
}
.notecard-description a {
color: #ffa500; /* 橙色链接 */
text-decoration: none;
transition: color 0.2s;
}
.notecard-description a:hover {
color: #ff8c00; /* 悬停时更深的橙色 */
text-decoration: underline;
}
.notecard-title .fas {
margin-left: 4px;
color: #1a73e8;
font-size: 0.9em;
}
.note-footer {
border-top: 1px solid #413f3f;
padding-top: 10px;
margin-top: 15px;
display: flex;
justify-content: space-between;
align-items: center;
}
.comment-button {
cursor: pointer;
color: #666;
}
.comment-button:hover {
color: #1e90ff;
}
.comment-box {
margin-top: 15px;
padding: 15px;
background: #f9f9f9;
border-radius: 5px;
}
.post-time {
color: #666;
font-size: 0.9em;
}
.source-link {
color: #1e8fffb0;
text-decoration: none;
transition: color 0.2s ease;
}
.source-link:hover {
color: #0056b3;
text-decoration: underline;
}
.zoom-image {
cursor: zoom-in;
transition: transform 0.3s ease-in-out;
}
.zoom-image:hover {
transform: scale(1.02);
}
.medium-zoom-overlay {
z-index: 999;
}
.medium-zoom-image--opened {
z-index: 1000;
}
.github-card {
border: 1px solid #30363d;
border-radius: 8px;
background: #161b22;
color: #c9d1d9;
margin: 1em 0;
padding: 16px;
width: 100%;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
font-size: 15px;
box-sizing: border-box;
min-width: 0;
overflow: hidden;
}
.github-card-header {
display: flex;
align-items: flex-start;
gap: 12px;
min-width: 0;
}
.github-card-avatar {
width: 48px;
height: 48px;
border-radius: 50%;
flex-shrink: 0;
margin-right: 0;
object-fit: cover;
background: #222;
}
.github-card-header > div {
flex: 1 1 0%;
min-width: 0;
display: flex;
flex-direction: column;
justify-content: center;
}
.github-card-title {
font-weight: bold;
color: #58a6ff;
text-decoration: none;
font-size: 17px;
word-break: break-all;
white-space: pre-line;
overflow-wrap: anywhere;
}
.github-card-desc {
color: #8b949e;
margin-top: 4px;
font-size: 14px;
word-break: break-all;
white-space: pre-line;
overflow-wrap: anywhere;
}
.github-card-footer {
margin-top: 12px;
display: flex;
gap: 16px;
color: #8b949e;
font-size: 13px;
flex-wrap: wrap;
}
.github-card-loading {
color: #8b949e;
font-style: italic;
}
@media (max-width: 520px) {
.github-card {
padding: 10px;
font-size: 14px;
}
.github-card-avatar {
width: 36px;
height: 36px;
}
.github-card-title {
font-size: 15px;
}
}
expand-card.css:
/* expand-card.css 用于实现顶部堆叠卡片的独立样式和动画 */
body {
position: relative;
}
#note-expand-card.hidden {
display: none !important;
}
#expand-content.hidden {
display: none !important;
}
.expand-card-container {
position: fixed; /* 由 absolute 改为 fixed */
top: 80px;
left: 0;
width: 100vw;
display: flex;
flex-direction: column;
align-items: flex-start;
padding-left: 52vw;
box-sizing: border-box;
z-index: 100;
}
.expand-card-stack {
position: relative;
width: 480px;
height: 120px;
transition: box-shadow 0.3s;
z-index: 2;
margin-right: 0;
}
.expand-card {
position: absolute;
left: 0;
width: 100%;
height: 100px;
border-radius: 32px;
background-color: #222222;
border-radius: 20px;
backdrop-filter: blur(8px);
display: flex;
align-items: center;
transition: transform 0.4s cubic-bezier(.4,2,.6,1), box-shadow 0.4s;
overflow: hidden;
}
/* 添加新的滑入动画 */
@keyframes slideIn {
0% {
transform: translateY(-100px);
opacity: 0;
}
100% {
transform: translateY(0);
opacity: 1;
}
}
/* 添加延迟动画类 */
.delayed-slide-in {
animation: slideIn 0.5s ease-out 2.5s forwards;
opacity: 0;
}
.park-sec1 { z-index: 3; transform: translateY(0) scale(1); }
.park-sec2 { z-index: 2; transform: translateY(18px) scale(0.97); opacity: 0.7; }
.park-sec3 { z-index: 1; transform: translateY(36px) scale(0.94); opacity: 0.5; }
.expand-card .park-inside {
display: flex;
align-items: center;
padding: 0 24px;
}
.expand-card img {
width: 56px;
height: 56px;
border-radius: 50%;
margin-right: 18px;
object-fit: cover;
border: 2px solid #fff;
}
.expand-card .content-sec h2 {
margin: 0;
font-size: 1.5rem;
font-weight: bold;
color: #f7f3f3;
}
.expand-card .content-sec span {
color: #888;
font-size: 1rem;
}
.expand-card .date {
margin-left: auto;
margin-right: 32px;
color: #f1f2f6;
font-size: 1.1rem;
background: rgba(0,0,0,0.18);
padding: 4px 16px;
border-radius: 16px;
cursor: pointer;
}
.expand-card-stack.expanded .park-sec1 {
transform: translateY(-20px) scale(1.04);
box-shadow: 0 16px 48px 0 rgba(31,38,135,0.22);
}
.expand-card-stack.expanded .park-sec2 {
transform: translateY(0) scale(1);
opacity: 0.85;
}
.expand-card-stack.expanded .park-sec3 {
transform: translateY(18px) scale(0.97);
opacity: 0.7;
}
.btn-grp {
margin-top: 32px;
display: flex;
justify-content: center;
}
.btn {
background: #ffffffb1;
color: #444444;
border: none;
border-radius: 32px;
padding: 16px 48px;
font-size: 1.4rem;
font-weight: 600;
box-shadow: 0 2px 8px 0 rgba(31,38,135,0.10);
cursor: pointer;
transition: background 0.2s, color 0.2s;
}
.btn:hover {
background-color: #222222;
border-radius: 20px;
}
.expand-arrow {
font-size: 1.2em;
margin-left: 8px;
transition: transform 0.3s;
}
/* 添加在适当位置 */
#typewriter-text {
overflow: hidden;
border-right: 2px solid #333;
white-space: pre-wrap;
margin: 0 auto;
letter-spacing: 2px;
animation:
typing 3.5s steps(40, end),
blink-caret .75s step-end infinite;
}
@keyframes typing {
from { width: 0 }
to { width: 100% }
}
@keyframes blink-caret {
from, to { border-color: transparent }
50% { border-color: #333 }
}
#expand-content {
width: 500px;
margin: 0;
margin-top: 3px;
background: transparent;
border-radius: 24px;
box-shadow: none;
padding: 24px 0 0 0;
transition: max-height 0.5s cubic-bezier(.4,2,.6,1), opacity 0.5s;
max-height: 800px;
overflow-y: auto;
overscroll-behavior: contain;
/* 隐藏滚动条,兼容主流浏览器 */
scrollbar-width: none; /* Firefox */
}
#expand-content::-webkit-scrollbar {
display: none; /* Chrome/Safari/Edge */
}
@media (max-width: 500px) {
.expand-card-stack, #expand-content {
width: 88vw;
min-width: 0;
max-width: 100vw;
}
.expand-card {
height: 80px;
}
.btn {
padding: 12px 24px;
font-size: 1.1rem;
}
#expand-content {
max-height: 80vh;
scrollbar-width: none;
}
#expand-content::-webkit-scrollbar {
display: none;
}
}
.expand-bottom-divider {
position: sticky;
bottom: 0;
left: 0;
width: 100%;
height: 2px;
background: linear-gradient(to right, #e0e0e0 30%, #bdbdbd 70%);
opacity: 0.7;
z-index: 2;
margin-top: 24px;
border-radius: 1px;
pointer-events: none;
}
/* 上下浮动动画 */
@keyframes float {
0% { transform: translateY(0);}
50% { transform: translateY(-18px);}
100% { transform: translateY(0);}
}
.float-animate {
animation: float 2.8s ease-in-out infinite;
}
#note-expand-card .avatar-link img {
transition: transform 0.25s cubic-bezier(.4,2,.6,1);
}
#note-expand-card .avatar-link img:hover {
transform: scale(1.18);
box-shadow: 0 4px 16px rgba(0,0,0,0.18);
}
#note-expand-card .note-title-link {
color: inherit;
text-decoration: none;
}
@media (max-width: 600px) {
#note-expand-card {
display: none !important;
}
}
note.js:
// Note Widget Configuration and Implementation
document.addEventListener('DOMContentLoaded', function() {
// Default configuration
const config = window.note || {
host: 'https://note.noisework.cn', //修改为你的域名
limit: '10',
domId: '#note'
};
const container = document.querySelector('#note .note-container');
const searchInput = document.querySelector('#tag-search');
const searchBtn = document.querySelector('#search-btn');
let currentPage = 1;
let isLoading = false;
let hasMore = true;
let currentTag = '';
// Create UI elements
const loadMoreBtn = document.createElement('button');
loadMoreBtn.id = 'load-more-note';
loadMoreBtn.className = 'load-more';
loadMoreBtn.textContent = '加载更多';
loadMoreBtn.style.display = 'none';
const loadedAll = document.createElement('div');
loadedAll.id = 'loaded-all-note';
loadedAll.className = 'loaded-all';
loadedAll.textContent = '已加载全部';
loadedAll.style.display = 'none';
// 在文件开头的 UI 元素创建部分添加
const backToListBtn = document.createElement('button');
backToListBtn.id = 'back-to-list';
backToListBtn.className = 'back-to-list';
backToListBtn.textContent = '返回列表';
backToListBtn.style.display = 'none';
container.appendChild(loadMoreBtn);
container.appendChild(loadedAll);
container.appendChild(backToListBtn);
// 修改 handleSearch 函数
function handleSearch() {
const searchValue = searchInput.value.trim();
currentTag = searchValue.startsWith('#') ? searchValue.substring(1) : '';
resetState();
// 确保在搜索时显示加载状态
container.querySelector('.loading-wrapper').style.display = 'block';
loadInitialContent();
if (searchValue !== '') {
backToListBtn.style.display = 'block';
} else {
backToListBtn.style.display = 'none';
}
}
// 修改 resetState 函数
function resetState() {
currentPage = 1;
hasMore = true;
isLoading = false;
loadMoreBtn.style.display = 'none';
loadedAll.style.display = 'none';
clearMessages();
// 重置时显示加载状态
container.querySelector('.loading-wrapper').style.display = 'block';
}
// 修改 loadInitialContent 函数中的错误处理
async function loadInitialContent() {
try {
const url = buildApiUrl();
console.log('请求URL:', url);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP错误! 状态码: ${response.status}`);
}
const result = await response.json();
console.log('API响应数据:', result);
if (result && result.code === 1 && result.data) {
// 修改这里以适应新的响应格式
const items = Array.isArray(result.data) ? result.data : (result.data.items || []);
const sortedData = items.sort((a, b) =>
new Date(b.created_at) - new Date(a.created_at)
);
renderMessages(sortedData);
updateLoadMoreState(items.length);
} else {
console.error('API返回数据格式不符:', result);
showNoContent();
}
} catch (error) {
console.error('加载内容失败:', error);
showLoadError();
} finally {
// 确保无论成功失败都隐藏加载状态
container.querySelector('.loading-wrapper').style.display = 'none';
}
}
// 添加返回列表的处理函数
// Event listeners
loadMoreBtn.addEventListener('click', loadMoreContent);
searchBtn.addEventListener('click', handleSearch);
searchInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') handleSearch();
});
backToListBtn.addEventListener('click', () => {
searchInput.value = '';
currentTag = '';
backToListBtn.style.display = 'none';
resetState();
loadInitialContent();
});
// Initial load
loadInitialContent();
function handleSearch() {
const searchValue = searchInput.value.trim();
currentTag = searchValue.startsWith('#') ? searchValue.substring(1) : '';
resetState();
loadInitialContent();
if (searchValue !== '') {
backToListBtn.style.display = 'block';
} else {
backToListBtn.style.display = 'none';
}
}
function filterByTag(tag) {
searchInput.value = `#${tag}`;
currentTag = tag;
backToListBtn.style.display = 'block';
resetState();
loadInitialContent();
}
function resetState() {
currentPage = 1;
hasMore = true;
isLoading = false;
loadMoreBtn.style.display = 'none';
loadedAll.style.display = 'none';
clearMessages();
const loadingWrapper = container.querySelector('.loading-wrapper');
if (loadingWrapper) {
loadingWrapper.style.display = 'block';
}
}
function clearMessages() {
const messages = container.querySelectorAll('.notecard');
messages.forEach(msg => msg.remove());
}
function buildApiUrl() {
let url;
if (currentTag) {
// 使用标签搜索路由
url = `${config.host}/api/messages/tags/${encodeURIComponent(currentTag)}?page=${currentPage}&pageSize=${config.limit}`;
} else if (searchInput.value.trim() !== '') {
// 使用普通搜索路由
url = `${config.host}/api/messages/search?keyword=${encodeURIComponent(searchInput.value.trim())}&page=${currentPage}&pageSize=${config.limit}`;
} else {
// 无搜索词时使用普通分页路由
url = `${config.host}/api/messages/page?page=${currentPage}&pageSize=${config.limit}`;
}
return url;
}
async function loadInitialContent() {
try {
const url = buildApiUrl();
console.log('请求URL:', url);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP错误! 状态码: ${response.status}`);
}
const result = await response.json();
console.log('API响应数据:', result);
if (result && result.code === 1 && result.data) {
// 修改这里以适应新的响应格式
const items = Array.isArray(result.data) ? result.data : (result.data.items || []);
const sortedData = items.sort((a, b) =>
new Date(b.created_at) - new Date(a.created_at)
);
renderMessages(sortedData);
updateLoadMoreState(items.length);
} else {
console.error('API返回数据格式不符:', result);
showNoContent();
}
} catch (error) {
console.error('加载内容失败:', error);
showLoadError();
} finally {
container.querySelector('.loading-wrapper').style.display = 'none';
}
}
async function loadMoreContent() {
if (isLoading || !hasMore) return;
isLoading = true;
loadMoreBtn.textContent = '加载中...';
currentPage++;
try {
const url = buildApiUrl();
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result && result.code === 1 && result.data) {
const items = Array.isArray(result.data) ? result.data : (result.data.items || []);
const sortedData = items.sort((a, b) =>
new Date(b.created_at) - new Date(a.created_at)
);
renderMessages(sortedData);
updateLoadMoreState(items.length);
}
} catch (error) {
console.error('加载更多内容失败:', error);
currentPage--;
} finally {
isLoading = false;
loadMoreBtn.textContent = '加载更多';
}
}
// 首先引入 marked 库
const marked = window.marked || {
parse: (text) => text
};
function parseContent(content) {
// 先解析 Markdown
content = marked.parse(content);
// 为所有图片添加 zoom-image 类
content = content.replace(/<img/g, '<img class="zoom-image"');
// 定义媒体平台的正则表达式
const BILIBILI_REG = /<a href="https:\/\/www\.bilibili\.com\/video\/((av[\d]{1,10})|(BV([\w]{10})))\/?">.*?<\/a>/g;
const QQMUSIC_REG = /<a href="https:\/\/y\.qq\.com\/.*(\/[0-9a-zA-Z]+)(\.html)?">.*?<\/a>/g;
const QQVIDEO_REG = /<a href="https:\/\/v\.qq\.com\/.*\/([a-zA-Z0-9]+)\.html">.*?<\/a>/g;
const SPOTIFY_REG = /<a href="https:\/\/open\.spotify\.com\/(track|album)\/([\s\S]+)">.*?<\/a>/g;
const YOUKU_REG = /<a href="https:\/\/v\.youku\.com\/.*\/id_([a-zA-Z0-9=]+)\.html">.*?<\/a>/g;
const YOUTUBE_REG = /<a href="https:\/\/(www\.youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]{11})">.*?<\/a>/g;
const NETEASE_MUSIC_REG = /<a href="https:\/\/music\.163\.com\/.*?id=(\d+)">.*?<\/a>/g;
// 修改正则,避免匹配图片链接
const GITHUB_REPO_REG = /<a href="https:\/\/github\.com\/([\w-]+)\/([\w.-]+)(?:\/[^\s"]*)?"[^>]*>(?!<img)[\s\S]*?<\/a>/g;
// 处理标签(在 Markdown 解析后)
content = content.replace(/<p>(.*?)<\/p>/g, (match, p) => {
return '<p>' + p.replace(/#([^\s#<>]+)/g, '<span class="tag" onclick="filterByTag(\'$1\')">#$1</span>') + '</p>';
});
// 处理各种媒体链接
content = content
.replace(BILIBILI_REG, "<div class='video-wrapper'><iframe src='https://www.bilibili.com/blackboard/html5mobileplayer.html?bvid=$1&as_wide=1&high_quality=1&danmaku=0' scrolling='no' border='0' frameborder='no' framespacing='0' allowfullscreen='true' style='position:absolute;height:100%;width:100%;'></iframe></div>")
.replace(YOUTUBE_REG, "<div class='video-wrapper'><iframe src='https://www.youtube.com/embed/$2' title='YouTube video player' frameborder='0' allow='accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture' allowfullscreen></iframe></div>")
.replace(NETEASE_MUSIC_REG, "<div class='music-wrapper'><meting-js auto='https://music.163.com/#/song?id=$1'></meting-js></div>")
.replace(QQMUSIC_REG, "<div class='music-wrapper'><meting-js auto='https://y.qq.com/n/yqq/song$1.html'></meting-js></div>")
.replace(QQVIDEO_REG, "<div class='video-wrapper'><iframe src='//v.qq.com/iframe/player.html?vid=$1' allowFullScreen='true' frameborder='no'></iframe></div>")
.replace(SPOTIFY_REG, "<div class='spotify-wrapper'><iframe style='border-radius:12px' src='https://open.spotify.com/embed/$1/$2?utm_source=generator&theme=0' width='100%' frameBorder='0' allowfullscreen='' allow='autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture' loading='lazy'></iframe></div>")
.replace(YOUKU_REG, "<div class='video-wrapper'><iframe src='https://player.youku.com/embed/$1' frameborder=0 'allowfullscreen'></iframe></div>")
.replace(GITHUB_REPO_REG, (match, owner, repo) => {
const cardId = `github-card-${owner}-${repo}-${Math.random().toString(36).slice(2, 8)}`;
setTimeout(() => fetchGitHubRepoInfo(owner, repo, cardId), 0);
return `<div class="github-card" id="${cardId}" data-owner="${owner}" data-repo="${repo}">
<div class="github-card-loading">Loading GitHub Repo...</div>
</div>`;
});
return content;
}
function updateLoadMoreState(itemCount) {
if (itemCount >= config.limit) {
loadMoreBtn.style.display = 'block';
loadedAll.style.display = 'none';
} else {
loadMoreBtn.style.display = 'none';
loadedAll.style.display = 'block';
hasMore = false;
}
}
function showNoContent() {
container.querySelector('.loading-wrapper').textContent = '暂无内容';
hasMore = false;
}
function showLoadError() {
container.querySelector('.loading-wrapper').textContent = '加载失败,请刷新重试';
}
function renderMessages(messages) {
const loadingWrapper = container.querySelector('.loading-wrapper');
if (loadingWrapper) {
loadingWrapper.style.display = 'none';
}
messages.forEach(message => {
const messageElement = createMessageElement(message);
container.insertBefore(messageElement, loadMoreBtn);
});
}
// 将 toggleCommentBox 和 initWaline 函数暴露到全局作用域
window.toggleCommentBox = function(host) {
const commentBox = document.getElementById(`comment-box-${host}`);
if (commentBox) {
if (commentBox.style.display === "none") {
commentBox.style.display = "block";
initWaline(commentBox, host);
} else {
commentBox.style.display = "none";
}
}
};
window.initWaline = function(container, host) {
const commentId = `waline-${host}`;
container.innerHTML = `<div id="${commentId}"></div>`;
import('https://unpkg.com/@waline/client@v3/dist/waline.js').then(({ init }) => {
const uid = host.split('-').pop();
init({
el: `#${commentId}`,
serverURL: window.note.commentServer || 'https://ment.noisework.cn', // 使用配置中的评论服务器地址
reaction: 'true',
meta: ['nick', 'mail', 'link'],
requiredMeta: ['mail', 'nick'],
pageview: true,
search: false,
wordLimit: 200,
pageSize: 5,
avatar: 'monsterid',
emoji: [
'https://unpkg.com/@waline/emojis@1.2.0/tieba',
],
imageUploader: false,
copyright: false,
path: `${config.host}/#/messages/${uid}`,
});
});
};
function createMessageElement(message) {
const messageDiv = document.createElement('div');
messageDiv.className = 'notecard';
const contentDiv = document.createElement('div');
contentDiv.className = 'notecard-content';
const title = document.createElement('h3');
title.className = 'notecard-title';
title.innerHTML = `${message.username || '匿名用户'}<i class="fas fa-certificate" style="color: rgb(26, 81, 232) font-size: 0.8em;"></i>`;
const description = document.createElement('div');
description.className = 'notecard-description';
let processedContent = message.content || '无内容';
processedContent = parseContent(processedContent);
description.innerHTML = processedContent;
// 初始化图片灯箱效果
const zoomImages = description.querySelectorAll('.zoom-image');
mediumZoom(zoomImages, {
margin: 24,
background: 'rgba(0, 0, 0, 0.9)',
scrollOffset: 0,
});
// 添加渐变遮罩
const contentMask = document.createElement('div');
contentMask.className = 'content-mask';
description.appendChild(contentMask);
// 添加展开按钮
const expandBtn = document.createElement('button');
expandBtn.className = 'expand-btn';
expandBtn.textContent = '展开全文';
// 修改展开按钮的检测逻辑
const checkHeight = () => {
const images = description.getElementsByTagName('img');
const allImagesLoaded = Array.from(images).every(img => img.complete);
if (allImagesLoaded) {
const actualHeight = description.scrollHeight;
if (actualHeight > 680) {
description.style.maxHeight = '680px'; // 添加这行
contentMask.style.display = 'block';
expandBtn.style.display = 'block';
} else {
description.style.maxHeight = 'none'; // 添加这行
contentMask.style.display = 'none';
expandBtn.style.display = 'none';
}
} else {
// 如果图片未加载完,等待所有图片加载完成后再次检查
Promise.all(Array.from(images).map(img => {
if (img.complete) return Promise.resolve();
return new Promise(resolve => {
img.onload = resolve;
img.onerror = resolve;
});
})).then(checkHeight);
}
};
// 初始检查(处理无图片的情况)
setTimeout(checkHeight, 100);
// 展开按钮点击事件
expandBtn.addEventListener('click', () => {
if (description.classList.contains('expanded')) {
description.classList.remove('expanded');
description.style.maxHeight = '680px'; // 添加这行
expandBtn.textContent = '展开全文';
contentMask.style.display = 'block';
// 滚动到卡片顶部
messageDiv.scrollIntoView({ behavior: 'smooth' });
} else {
description.classList.add('expanded');
description.style.maxHeight = 'none'; // 添加这行
expandBtn.textContent = '收起全文';
contentMask.style.display = 'none';
}
});
if (message.image_url) {
const img = document.createElement('img');
img.src = message.image_url.startsWith('http') ?
message.image_url :
config.host + message.image_url;
img.style.maxWidth = '100%';
img.style.borderRadius = '2px';
img.style.marginTop = '2px';
description.appendChild(img);
}
contentDiv.appendChild(title);
contentDiv.appendChild(description);
contentDiv.appendChild(expandBtn);
// 添加底部分割线和信息
const footerDiv = document.createElement('div');
footerDiv.className = 'note-footer';
// 左侧时间和来源
const timeDiv = document.createElement('small');
timeDiv.className = 'post-time';
const date = new Date(message.created_at);
timeDiv.textContent = `${date.getFullYear()}年${date.getMonth() + 1}月${date.getDate()}日 ${date.getHours()}:${String(date.getMinutes()).padStart(2, '0')} · 来自 `;
// 修改链接生成逻辑
const sourceLink = document.createElement('a');
sourceLink.href = `${config.host}/#/messages/${message.id}`;
sourceLink.textContent = config.sourceName || '「说说笔记」';
sourceLink.className = 'source-link';
sourceLink.target = '_blank'; // 修改为在新标签页打开
timeDiv.appendChild(sourceLink);
// 右侧评论按钮
const commentDiv = document.createElement('small');
commentDiv.className = 'comment-button';
commentDiv.dataset.host = `note-${message.id}`;
// commentDiv.innerHTML = '📮 评论';
commentDiv.onclick = function() {
window.toggleCommentBox(`note-${message.id}`);
};
footerDiv.appendChild(timeDiv);
footerDiv.appendChild(commentDiv);
// 添加评论框容器
const commentBoxDiv = document.createElement('div');
commentBoxDiv.id = `comment-box-note-${message.id}`;
commentBoxDiv.className = 'comment-box';
commentBoxDiv.style.display = 'none';
contentDiv.appendChild(footerDiv);
contentDiv.appendChild(commentBoxDiv);
messageDiv.appendChild(contentDiv);
return messageDiv;
}
// 将filterByTag函数暴露到全局作用域
window.filterByTag = filterByTag;
});
// 新增:异步拉取GitHub仓库信息并填充卡片
function fetchGitHubRepoInfo(owner, repo, cardId) {
fetch(`https://api.github.com/repos/${owner}/${repo}`)
.then(res => res.ok ? res.json() : null)
.then(data => {
if (!data) return;
const card = document.getElementById(cardId);
if (card) {
card.innerHTML = `
<div class="github-card-header">
<img src="${data.owner.avatar_url}" class="github-card-avatar" />
<div>
<a href="${data.html_url}" target="_blank" class="github-card-title">${data.full_name}</a>
<div class="github-card-desc">${data.description || ''}</div>
</div>
</div>
<div class="github-card-footer">
<span>⭐ ${data.stargazers_count}</span>
<span>🍴 ${data.forks_count}</span>
<span>🛠️ ${data.language || ''}</span>
</div>
`;
}
});
}
expand-card.js:
// 展开/收起卡片交互逻辑
document.addEventListener('DOMContentLoaded', function() {
var expandBtn = document.getElementById('expand-toggle-btn');
var expandContent = document.getElementById('expand-content');
var expandArrow = expandBtn.querySelector('.expand-arrow');
var stack = document.getElementById('expand-card-stack');
var expanded = false;
expandBtn.addEventListener('click', function() {
expanded = !expanded;
if (expanded) {
expandContent.style.display = 'block';
expandBtn.innerHTML = '收起 <span class="expand-arrow">▲</span>';
stack.classList.add('expanded');
} else {
expandContent.style.display = 'none';
expandBtn.innerHTML = '展开 <span class="expand-arrow">▼</span>';
stack.classList.remove('expanded');
}
});
});
function hideCardPermanently() {
const card = document.getElementById('note-expand-card');
const content = document.getElementById('expand-content');
// 添加hidden类来隐藏元素
card.classList.add('hidden');
content.classList.add('hidden');
// 存储隐藏状态到localStorage
// localStorage.setItem('cardHidden', 'true');
}
// 页面加载时检查隐藏状态
document.addEventListener('DOMContentLoaded', function() {
// if (localStorage.getItem('cardHidden') === 'true') {
// hideCardPermanently();
// }
// 确保卡片默认显示
const card = document.getElementById('note-expand-card');
const content = document.getElementById('expand-content');
card.classList.remove('hidden');
content.classList.remove('hidden');
});