
赛博羊圈装修代码分享(一)
- 发布于2026-08-05
- 分类博客教程
- 总字数1947 字
- 阅读时间5 分钟
起初是看夏夏翻友链装修日志找一键copy代码没翻到,感觉自己博客可以写写这方面的内容,于是就有了这篇博文!又临时凑了几个加在一起,所以这篇文章大概没什么文字,纯粹是一个羊圈装修用代码仓库,大家喜欢搬走就可以了!
一键copy代码
效果
testtesttest
代码(js)
// 复制按钮用到的两个图标(复制中 / 已复制),内联 SVG 免得额外发请求
const ICON_COPY = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="11" height="11" rx="2"/><path d="M5 15V5a2 2 0 0 1 2-2h8"/></svg>`;
const ICON_DONE = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg>`;
function writeClipboard(text: string): Promise<void> {
if (navigator.clipboard && window.isSecureContext) {
return navigator.clipboard.writeText(text);
}
// 兜底:http 或者不支持 Clipboard API 的旧浏览器
return new Promise((resolve, reject) => {
const ta = document.createElement('textarea');
ta.value = text;
ta.style.cssText = 'position:fixed;top:-9999px;opacity:0';
document.body.appendChild(ta);
ta.select();
const ok = document.execCommand('copy');
ta.remove();
ok ? resolve() : reject(new Error('execCommand failed'));
});
}
// 查找所有代码块,给每个都加个复制按钮
function initCopyButtons() {
document.querySelectorAll<HTMLPreElement>('.prose pre').forEach((pre) => {
// 已经处理过就跳过(保险起见,避免重复插入)
if (pre.closest('.code-wrapper[data-copy]')) return;
// Astro 内置的 Shiki 高亮直接输出 <pre>,不带外层包装容器,
// 这里自己包一层,好让按钮用 position:absolute 相对这层定位
const wrapper = document.createElement('div');
pre.parentNode?.insertBefore(wrapper, pre);
wrapper.appendChild(pre);
wrapper.classList.add('code-wrapper');
wrapper.dataset.copy = 'true';
const button = document.createElement('button');
button.type = 'button';
button.className = 'copy-code-button';
button.setAttribute('aria-label', '复制代码');
button.innerHTML = `${ICON_COPY}<span>Copy</span>`;
wrapper.appendChild(button);
const code = pre.querySelector('code') ?? pre;
let timer: ReturnType<typeof setTimeout>;
button.addEventListener('click', async () => {
try {
await writeClipboard((code as HTMLElement).innerText.replace(/\n$/, ''));
button.classList.add('is-copied');
button.innerHTML = `${ICON_DONE}<span>Copied</span>`;
clearTimeout(timer);
timer = setTimeout(() => {
button.classList.remove('is-copied');
button.innerHTML = `${ICON_COPY}<span>Copy</span>`;
}, 1600);
} catch (err) {
console.error('Copy failed!', err);
button.innerHTML = `${ICON_COPY}<span>Failed</span>`;
}
});
});
}
initCopyButtons();
代码(css)
/* =========================================================
代码块「复制」按钮样式
配套 JS:给每个 pre 外面套一层 .code-wrapper,
并在里面插入一个 .copy-code-button
没有做浅色主题适配
========================================================= */
/* ---------- 1. 定位容器 ---------- */
:global(.code-wrapper) {
position: relative;
/* 给按钮提供绝对定位的坐标原点,没有这行按钮会飞到页面角落 */
}
:global(.code-wrapper > pre) {
padding-top: 1.5em;
/* 代码区顶部留白,防止按钮压住第一行代码 */
}
/* ---------- 2. 按钮本体 ---------- */
:global(.copy-code-button) {
position: absolute;
/* 脱离文档流,相对 .code-wrapper 定位 */
top: 0.6em;
/* 距代码块顶边的距离 */
left: 0.6em;
/* 距代码块左边的距离(改成 right 即可放到右上角) */
z-index: 2;
/* 层级抬高,压在代码内容之上,避免被遮住 */
display: inline-flex;
/* 弹性布局,让图标和文字能并排且垂直居中 */
align-items: center;
/* 图标与文字在竖直方向对齐居中 */
gap: 0.4em;
/* 图标和文字之间的间距 */
margin: 0;
/* 清掉浏览器给 button 的默认外边距 */
padding: 0.32em 0.7em;
/* 按钮内边距:上下 0.32em,左右 0.7em,决定按钮大小 */
font: inherit;
/* 继承页面字体,覆盖 button 默认的系统字体 */
font-size: 0.75rem;
/* 按钮文字大小(图标尺寸不受影响,单独设) */
font-weight: 500;
/* 字重,中等粗细,比正文略突出一点 */
line-height: 1.4;
/* 行高,影响按钮的实际高度 */
letter-spacing: 0.01em;
/* 字间距,微微拉开一点更清爽 */
color: rgba(255, 255, 255, 0.62);
/* 文字和图标颜色(图标用 currentColor 跟随此值) */
background: rgba(255, 255, 255, 0.06);
/* 按钮底色:极淡的白,浮在深色代码块上 */
border: 1px solid rgba(255, 255, 255, 0.1);
/* 1px 细描边,勾出按钮轮廓 */
border-radius: 7px;
/* 圆角大小,数值越大越圆 */
backdrop-filter: blur(6px);
/* 毛玻璃效果:模糊按钮背后的代码 */
-webkit-backdrop-filter: blur(6px);
/* 同上,Safari 需要的私有前缀 */
cursor: pointer;
/* 鼠标移上去变成小手,提示可点击 */
opacity: 0;
/* 默认完全透明(即隐藏),悬停代码块时才显形 */
transform: translateY(-2px);
/* 默认向上偏移 2px,配合淡入做「滑下来」的动效 */
transition:
opacity 0.18s ease,
/* 显隐的淡入淡出时长 */ transform 0.18s ease,
/* 位移/缩放的过渡时长 */ color 0.18s ease,
/* 文字变色的过渡 */ background-color 0.18s ease,
/* 底色变化的过渡 */ border-color 0.18s ease;
/* 描边变色的过渡 */
}
:global(.copy-code-button svg) {
width: 13px;
/* 图标宽度 */
height: 13px;
/* 图标高度 */
flex: none;
/* 禁止图标被弹性布局压扁或拉伸 */
}
/* ---------- 3. 显形时机:悬停代码块、键盘聚焦、复制成功后 ---------- */
:global(.code-wrapper:hover .copy-code-button),
/* 鼠标移到代码块任意位置时 */
:global(.copy-code-button:focus-visible),
/* 用 Tab 键聚焦到按钮时(无障碍) */
:global(.copy-code-button.is-copied) {
/* 刚复制完、正显示 Copied 时保持可见 */
opacity: 1;
/* 完全不透明,按钮出现 */
transform: translateY(0);
/* 回到原位,完成下滑动画 */
}
/* ---------- 4. 交互状态 ---------- */
:global(.copy-code-button:hover) {
color: rgba(255, 255, 255, 0.95);
/* 悬停时文字变亮,反馈「可以点了」 */
background: rgba(255, 255, 255, 0.12);
/* 悬停时底色加深一点 */
border-color: rgba(255, 255, 255, 0.2);
/* 悬停时描边变明显 */
}
:global(.copy-code-button:active) {
transform: scale(0.96);
/* 按下瞬间轻微缩小,做出「按下去」的手感 */
}
:global(.copy-code-button:focus-visible) {
outline: 2px solid rgba(120, 170, 255, 0.55);
/* 键盘聚焦时的蓝色 focus 环(鼠标点击不会触发) */
outline-offset: 2px;
/* focus 环与按钮边缘的间隙 */
}
:global(.copy-code-button.is-copied) {
color: #4ade80;
/* 复制成功后文字和对勾图标变绿 */
background: rgba(74, 222, 128, 0.12);
/* 成功态的淡绿底色 */
border-color: rgba(74, 222, 128, 0.32);
/* 成功态的绿色描边 */
}
/* ---------- 5. 触屏设备适配 ---------- */
@media (hover: none) {
/* 手机/平板等没有鼠标悬停的设备 */
:global(.copy-code-button) {
opacity: 1;
/* 按钮常驻显示,否则触屏用户永远看不到它 */
transform: none;
/* 取消位移动画,直接停在最终位置 */
}
}
阅读进度条代码
效果展示略过,正在看这篇文章的时候估计就已经显示在大家屏幕/浏览器的顶部了。
代码
<div id="progress-bar"></div>
<style>
#progress-bar {
position: fixed;
top: 0;
left: 0;
height: 3px;
width: 0%;
background: #375a7f; /* 换成你想要的颜色 */
z-index: 2000;
transition: width 0.1s ease;
}
</style>
<script>
function updateProgressBar() {
var bar = document.getElementById('progress-bar');
if (!bar) return;
var scrollTop = window.scrollY;
var docHeight = document.documentElement.scrollHeight - window.innerHeight;
var percent = docHeight > 0 ? (scrollTop / docHeight) * 100 : 0;
bar.style.width = percent + '%';
}
document.addEventListener('DOMContentLoaded', function () {
updateProgressBar(); // 页面刚加载时先校正一次(比如从锚点直接跳到页面中间打开的情况)
window.addEventListener('scroll', updateProgressBar);
});
</script>
菜单栏按钮鼠标移动至上方时出现下划线
效果也省略了,可以直接参考电脑端鼠标移动到菜单栏按钮时的效果。 其中.navbar__title, .navbar__menu, #search-container 是羊圈的菜单栏标题、菜单项、搜索框的类名,大家可以根据自己博客的实际情况修改。
代码
.navbar__title>a::after,
.navbar__menu>a::after,
#search-container::after {
content: '';
position: absolute;
left: 0;
right: 0;
bottom: -4px;
height: 2px;
background-color: var(--underline);
transform: scaleX(0);
transform-origin: center;
transition: transform 0.3s ease;
}
.navbar__title>a:hover::after,
.navbar__menu>a:hover::after,
#search-container:hover::after,
.navbar__menu>a:focus-visible::after,
.navbar__title>a.is-pressed::after,
.navbar__menu>a.is-pressed::after,
.navbar__title>a.active::after,
.navbar__menu>a.active::after {
transform: scaleX(1);
}
博文卡片上的已阅小印章
效果
懒得引用了,还要改数据……放张效果图好了。想知道具体操作起来是什么样的话,可以菜单栏点击Posts或者Tags,回到文章卡片列表页,点几个玩玩看。

代码(文章列表页)
<ul class="post-list">
<li>
<a class="post-card" href="/posts/post-1.html" data-post-slug="post-1">
<div class="post-card-meta">
<time class="post-date">2026-01-01</time>
</div>
<h3 class="post-title">示例文章标题</h3>
<p class="post-desc">这是一段示例简介。</p>
<!-- 已读图章:默认藏起来,JS 判断这篇文章读过之后才会显示 -->
<span class="read-stamp" aria-hidden="true">
<img src="/images/read-stamp.webp" alt="" width="180" height="163" loading="lazy" />
</span>
</a>
</li>
<!-- 更多 <li> 照抄这个结构就行 -->
</ul>
<style>
.post-list {
list-style: none;
margin: 1.5rem 0 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 1rem;
}
.post-card {
position: relative; /* 已读图章要相对这个卡片做绝对定位 */
overflow: hidden; /* 图章旋转后一部分会探出卡片边缘,裁掉 */
display: block;
padding: 1.25rem;
border-radius: 1rem;
border: 1px solid #e5e5e5; /* 换成你自己的边框色 */
background-color: rgba(255, 255, 255, 0.8); /* 换成你自己的卡片底色 */
text-decoration: none;
color: inherit;
transition: transform 0.3s ease-in-out;
}
/* 鼠标悬停 / 手指按下时卡片轻轻上浮,做出"可以点"的反馈 */
.post-card:hover,
.post-card.is-pressed {
transform: translateY(-10px);
}
.post-date {
font-size: 0.875rem;
color: #888;
}
.post-title {
margin: 0.5rem 0 0;
font-size: 1.125rem;
font-weight: 600;
}
.post-desc {
margin: 0.5rem 0 0;
font-size: 0.875rem;
line-height: 1.6;
color: #555;
}
/* 图源裁掉透明边之后是 180x163(按展示尺寸 90px 的 2 倍图导出,兼顾高分屏),
用 aspect-ratio 锁住这个真实比例,宽度定死、高度跟着比例走,避免图章被拉伸变形。 */
.read-stamp {
position: absolute;
top: -19px;
right: -15px;
width: 90px;
aspect-ratio: 180 / 163;
transform: rotate(-12deg) scale(0.85);
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease, transform 0.2s ease;
}
.read-stamp img {
width: 100%;
height: 100%;
display: block;
}
/* 卡片一旦被 JS 加上 .is-read,图章就淡入 + 转正到最终角度和大小 */
.post-card.is-read .read-stamp {
opacity: 0.92;
transform: rotate(-12deg) scale(1);
}
@media (prefers-reduced-motion: reduce) {
.post-card,
.read-stamp {
transition: none;
}
}
</style>
<script>
// ---------- 1. 已读状态存取:全部放一个 localStorage key 里 ----------
// 用 { slug: 已读时间戳 } 这样一个对象存所有已读记录,而不是一篇文章一个
// key,方便列表页一次性读出全部已读 slug。
var STORAGE_KEY = 'read-posts';
function readStore() {
try {
var raw = localStorage.getItem(STORAGE_KEY);
return raw ? JSON.parse(raw) : {};
// 隐私浏览模式下 localStorage 可能直接抛异常(Safari 历史上就是这样),
// 读失败就当没有任何已读记录,不影响正常浏览。
} catch (e) {
return {};
}
}
function getReadSlugs() {
return Object.keys(readStore());
}
// ---------- 2. 给每张卡片标记已读/未读 ----------
function markReadCards() {
var readSlugs = getReadSlugs();
document.querySelectorAll('.post-card').forEach(function (card) {
var slug = card.dataset.postSlug;
var isRead = slug && readSlugs.indexOf(slug) !== -1;
card.classList.toggle('is-read', !!isRead);
});
}
// ---------- 3. 按下时的视觉反馈(鼠标和触屏通用) ----------
function attachPressFeedback() {
document.querySelectorAll('.post-card').forEach(function (card) {
var press = function () { card.classList.add('is-pressed'); };
var release = function () { card.classList.remove('is-pressed'); };
card.addEventListener('pointerdown', press);
card.addEventListener('pointerup', release);
card.addEventListener('pointerleave', release);
card.addEventListener('pointercancel', release);
});
}
document.addEventListener('DOMContentLoaded', function () {
markReadCards();
attachPressFeedback();
});
</script>
代码(文章详情页)
<script>
(function () {
var STORAGE_KEY = 'read-posts';
// 每篇详情页页面自己填当前这篇文章的 slug(跟列表页 data-post-slug 对应上)
var currentSlug = 'post-1';
try {
var raw = localStorage.getItem(STORAGE_KEY);
var store = raw ? JSON.parse(raw) : {};
if (!store[currentSlug]) {
store[currentSlug] = Date.now();
localStorage.setItem(STORAGE_KEY, JSON.stringify(store));
}
} catch (e) {
// 忽略:多半是隐私浏览模式下 localStorage 不可写,不影响正常阅读
}
})();
</script>
博文卡片的悬浮动画效果
效果
代码
<div class="hover-lift-demo">
<a class="demo-card" href="#">
<time class="demo-date">2026-01-01</time>
<h3 class="demo-title">鼠标移上来试试</h3>
<p class="demo-desc">卡片会往上浮起来一点,移开又会回到原位。</p>
</a>
</div>
<style>
.demo-card {
display: block;
padding: 1.25rem;
border-radius: 1rem;
border: 1px solid #e5e5e5; /* 换成你自己喜欢的边框色 */
background-color: rgba(255, 255, 255, 0.8); /* 换成你自己的卡片底色 */
text-decoration: none;
color: inherit;
/* 核心效果就这一行:告诉浏览器 transform 属性发生变化的时候,
用 0.3 秒时间、ease-in-out 的缓动曲线做平滑过渡,
而不是瞬间跳过去。少了这一行,下面的位移会变成"哐"一下瞬移。 */
transition: transform 0.3s ease-in-out;
}
/* 鼠标悬停时往上位移 10px(原来 Tailwind 写的是 -translate-y-2.5,
Tailwind 一个间距单位是 0.25rem = 4px,2.5 个单位换算出来就是 10px)。
故意用 transform 而不是改 margin-top 或者 top,是因为 transform
不会触发浏览器重新计算页面布局(reflow),改起来更流畅、不卡顿,
这也是做这类"悬浮"动效时的标准做法。 */
.demo-card:hover {
transform: translateY(-10px);
}
/* 触屏设备没有真正意义上的"鼠标悬停",手指点下去的瞬间触发的是
:active 而不是 :hover,所以这里也单独绑一份同样的效果,
保证触屏用户点一下也能看到反馈,不是只有用鼠标的人才有动效。 */
.demo-card:active {
transform: translateY(-10px);
}
.demo-date {
font-size: 0.875rem;
color: #888;
}
.demo-title {
margin: 0.5rem 0 0;
font-size: 1.125rem;
font-weight: 600;
}
.demo-desc {
margin: 0.5rem 0 0;
font-size: 0.875rem;
line-height: 1.6;
color: #555;
}
/* 尊重"减少动态效果"这项系统设置——有些人因为前庭功能等原因
不希望页面出现位移/缩放类动画,检测到这个设置就直接关掉过渡,
卡片还是会上浮,只是不再有"滑动"的过程,瞬间到位。 */
@media (prefers-reduced-motion: reduce) {
.demo-card {
transition: none;
}
}
</style>
我能想起来的大概就是这么多啦!还有什么想看的可以随时嘟嘟跟我说,或者评论区留言!尽量有求必应嘿嘿。