feat: 优化代码块样式并实现点击复制功能

This commit is contained in:
seahi committed 2026-07-12 13:33:57 +08:00
1 parent e73af54b2f
commit ade9ea9598
2 files changed
+66 -15

No files matched your search

+12 -6
View File
@@ -316,25 +316,31 @@ body.single-col .post-list {
margin: 1.5rem 0; margin: 1.5rem 0;
border-radius: 4px; border-radius: 4px;
overflow: hidden; overflow: hidden;
background-color: var(--code-background-color, #f5f5f5); background-color: #f5f5f5;
} }
.code-block-header { .code-block-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
background-color: var(--code-header-bg, #e6e6e6); background-color: #ededed;
padding: 0.4rem 0.8rem; padding: 0.4rem 0.8rem;
font-size: 0.85rem; font-size: 0.85rem;
color: var(--text-muted); color: var(--text-color);
} }
.code-title { .code-title {
font-family: var(--font-sans); font-family: "Source Code Pro", Menlo, Consolas, Monaco, monospace;
font-weight: 600; font-weight: 500;
color: var(--text-color); color: var(--text-color);
} }
.code-header-actions {
display: flex;
align-items: center;
gap: 1rem;
}
.copy-code-button { .copy-code-button {
background: none; background: none;
border: none; border: none;
@@ -355,7 +361,7 @@ body.single-col .post-list {
padding: 1rem; padding: 1rem;
overflow: auto; overflow: auto;
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
line-height: 1.5; line-height: 1.6;
font-size: 0.875rem; font-size: 0.875rem;
font-family: "Source Code Pro", Menlo, Consolas, Monaco, monospace; font-family: "Source Code Pro", Menlo, Consolas, Monaco, monospace;
background-color: transparent !important; background-color: transparent !important;
+54 -9
View File
@@ -1,20 +1,46 @@
<script lang="ts"> <script lang="ts">
import type { PageData } from './$types'; import type { PageData } from './$types';
import { marked } from 'marked'; import { Marked } from 'marked';
import { markedHighlight } from "marked-highlight";
import hljs from 'highlight.js'; import hljs from 'highlight.js';
import 'highlight.js/styles/github.css'; import 'highlight.js/styles/github.css';
import DOMPurify from 'dompurify'; import DOMPurify from 'dompurify';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
// Configure marked for code highlighting // Create a new Marked instance to avoid HMR leaks and double-escaping issues
marked.use(markedHighlight({ const markedInstance = new Marked();
langPrefix: 'hljs language-', markedInstance.use({
highlight(code, lang) { renderer: {
code(token) {
const code = token.text;
const lang = (token.lang || '').split(' ')[0];
const language = hljs.getLanguage(lang) ? lang : 'plaintext'; const language = hljs.getLanguage(lang) ? lang : 'plaintext';
return hljs.highlight(code, { language }).value; const highlighted = hljs.highlight(code, { language }).value;
const encodedCode = encodeURIComponent(code);
const copyIcon = '<svg viewBox="0 0 448 512" fill="currentColor" height="1em" width="1em" style="font-size: 1.1em"><path d="M433.941 65.941l-51.882-51.882A48 48 0 0 0 348.118 0H176c-26.51 0-48 21.49-48 48v48H48c-26.51 0-48 21.49-48 48v320c0 26.51 21.49 48 48 48h224c26.51 0 48-21.49 48-48v-48h80c26.51 0 48-21.49 48-48V99.882a48 48 0 0 0-14.059-33.941zM266 464H54a6 6 0 0 1-6-6V150a6 6 0 0 1 6-6h74v224c0 26.51 21.49 48 48 48h96v42a6 6 0 0 1-6 6zm128-96H182a6 6 0 0 1-6-6V54a6 6 0 0 1 6-6h106v88c0 13.255 10.745 24 24 24h88v200a6 6 0 0 1-6 6zm6-256h-64V48h9.632c1.591 0 3.117.632 4.243 1.757l48.368 48.368a6 6 0 0 1 1.757 4.243V112z"></path></svg>';
const chevronIcon = '<svg viewBox="0 0 512 512" fill="currentColor" height="1em" width="1em" style="margin-right: 0.5rem;"><path d="M256 294.1L383 167c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9l-144 144c-9.4 9.4-24.6 9.4-33.9 0l-144-144c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0L256 294.1z"></path></svg>';
const listOlIcon = '<svg viewBox="0 0 512 512" fill="currentColor" height="1em" width="1em"><path d="M61.77 401l17.5-20.15a19.92 19.92 0 0 0 5.07-14.19v-3.31C84.34 356 80.5 352 73 352H16a8 8 0 0 0-8 8v16a8 8 0 0 0 8 8h34.71L15.32 424.64A29.85 29.85 0 0 0 8 444v16c0 26.51 21.49 48 48 48h24a8 8 0 0 0 8-8v-16a8 8 0 0 0-8-8H56c-8.84 0-16-7.16-16-16v-4.52a6.01 6.01 0 0 1 1.76-4.24l19.24-19.24a48.28 48.28 0 0 0 7.58 3.06L64 448c0 35.35 28.65 64 64 64s64-28.65 64-64-28.65-64-64-64c-9.67 0-18.77 2.21-27.05 6.04L75.64 361.7A29.8 29.8 0 0 0 56 352h-7.66c10.37-4.14 17.66-14.24 17.66-26.34v-3.31c0-15.46-12.54-28-28-28H16a8 8 0 0 0-8 8v16a8 8 0 0 0 8 8h22c2.21 0 4 1.79 4 4v3.31c0 2.21-1.79 4-4 4H16a8 8 0 0 0-8 8v16a8 8 0 0 0 8 8h15.4c17.21 0 33.15 6.94 44.97 19.46l5.4-6.23a43.83 43.83 0 0 1 7.23-7.19h-15zM400 352H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h224a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zM176 112h224a16 16 0 0 0 16-16V64a16 16 0 0 0-16-16H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16zM400 208H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h224a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zM88 32H16C7.16 32 0 39.16 0 48v20c0 8.84 7.16 16 16 16h16v64c0 8.84 7.16 16 16 16h16c8.84 0 16-7.16 16-16V48c0-8.84-7.16-16-16-16h-48c0-8.84 7.16-16 16-16zm64 160c0-35.35-28.65-64-64-64s-64 28.65-64 64 28.65 64 64 64 64-28.65 64-64zm-32 0c0 17.67-14.33 32-32 32s-32-14.33-32-32 14.33-32 32-32 32 14.33 32 32zm0-24c0 17.67-14.33 32-32 32s-32-14.33-32-32 14.33-32 32-32 32 14.33 32 32zm0-24c0 17.67-14.33 32-32 32s-32-14.33-32-32 14.33-32 32-32 32 14.33 32 32z"></path></svg>';
const barsIcon = '<svg viewBox="0 0 448 512" fill="currentColor" height="1em" width="1em"><path d="M16 132h416c8.837 0 16-7.163 16-16V76c0-8.837-7.163-16-16-16H16C7.163 60 0 67.163 0 76v40c0 8.837 7.163 16 16 16zm0 160h416c8.837 0 16-7.163 16-16v-40c0-8.837-7.163-16-16-16H16c-8.837 0-16 7.163-16 16v40c0 8.837 7.163 16 16 16zm0 160h416c8.837 0 16-7.163 16-16v-40c0-8.837-7.163-16-16-16H16c-8.837 0-16 7.163-16 16v40c0 8.837 7.163 16 16 16z"></path></svg>';
return `
<div class="code-block highlight">
<div class="code-block-header">
<div style="display: flex; align-items: center;">
${chevronIcon}
<span class="code-title">${lang || language}</span>
</div>
<div class="code-header-actions">
<span style="opacity: 0.5;">${listOlIcon}</span>
<span style="opacity: 0.5;">${barsIcon}</span>
<button class="copy-code-button" title="Copy code" data-code="${encodedCode}">
${copyIcon}
</button>
</div>
</div>
<pre><code class="hljs language-${language}">${highlighted}</code></pre>
</div>`;
} }
})); }
});
let { data }: { data: PageData } = $props(); let { data }: { data: PageData } = $props();
@@ -68,9 +94,28 @@
// Parse markdown synchronously so it renders on the server (SSR) // Parse markdown synchronously so it renders on the server (SSR)
// and doesn't rely on client-side effects which were causing the empty content. // and doesn't rely on client-side effects which were causing the empty content.
const htmlContent = marked.parse(preprocessedContent) as string; const htmlContent = markedInstance.parse(preprocessedContent) as string;
onMount(() => { onMount(() => {
// Initialize copy code buttons
document.querySelectorAll('.copy-code-button').forEach((btn) => {
const buttonEl = btn as HTMLElement;
buttonEl.addEventListener('click', (e) => {
e.preventDefault();
const code = buttonEl.getAttribute('data-code');
if (code) {
navigator.clipboard.writeText(decodeURIComponent(code)).then(() => {
// Temporarily change icon to a checkmark or just "Copied!" text if we had a check icon
// For now, let's just make it green temporarily
buttonEl.style.color = '#4CAF50';
setTimeout(() => {
buttonEl.style.color = '';
}, 2000);
});
}
});
});
// Initialize details expand/collapse animation logic (DoIt style) // Initialize details expand/collapse animation logic (DoIt style)
document.querySelectorAll('.details').forEach((detailsEl) => { document.querySelectorAll('.details').forEach((detailsEl) => {
const summaryEl = detailsEl.querySelector('.details-summary') as HTMLElement; const summaryEl = detailsEl.querySelector('.details-summary') as HTMLElement;