Merge pull request #244 from nagisa77/codex/add-copy-button-to-markdown-code-blocks

This commit is contained in:
Tim
2025-07-21 22:48:15 +08:00
committed by GitHub
5 changed files with 61 additions and 13 deletions

View File

@@ -101,6 +101,24 @@ body {
padding: 8px 12px; padding: 8px 12px;
border-radius: 4px; border-radius: 4px;
line-height: 0.8; line-height: 0.8;
position: relative;
}
.copy-code-btn {
position: absolute;
top: 4px;
right: 4px;
font-size: 12px;
padding: 2px 6px;
border: none;
border-radius: 4px;
background-color: var(--primary-color);
color: #fff;
cursor: pointer;
}
.copy-code-btn:hover {
background-color: var(--primary-color-hover);
} }
.info-content-text code { .info-content-text code {

View File

@@ -21,7 +21,7 @@
</DropdownMenu> </DropdownMenu>
</div> </div>
</div> </div>
<div class="info-content-text" v-html="renderMarkdown(comment.text)" @click="handleImageClick"></div> <div class="info-content-text" v-html="renderMarkdown(comment.text)" @click="handleContentClick"></div>
<div class="article-footer-container"> <div class="article-footer-container">
<ReactionsGroup v-model="comment.reactions" content-type="comment" :content-id="comment.id"> <ReactionsGroup v-model="comment.reactions" content-type="comment" :content-id="comment.id">
<div class="make-reaction-item comment-reaction" @click="toggleEditor"> <div class="make-reaction-item comment-reaction" @click="toggleEditor">
@@ -69,7 +69,7 @@ import { ref, watch, computed } from 'vue'
import VueEasyLightbox from 'vue-easy-lightbox' import VueEasyLightbox from 'vue-easy-lightbox'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import CommentEditor from './CommentEditor.vue' import CommentEditor from './CommentEditor.vue'
import { renderMarkdown } from '../utils/markdown' import { renderMarkdown, handleMarkdownClick } from '../utils/markdown'
import TimeManager from '../utils/time' import TimeManager from '../utils/time'
import BaseTimeline from './BaseTimeline.vue' import BaseTimeline from './BaseTimeline.vue'
import { API_BASE_URL, toast } from '../main' import { API_BASE_URL, toast } from '../main'
@@ -196,7 +196,8 @@ const CommentItem = {
toast.success('已复制') toast.success('已复制')
}) })
} }
const handleImageClick = e => { const handleContentClick = e => {
handleMarkdownClick(e)
if (e.target.tagName === 'IMG') { if (e.target.tagName === 'IMG') {
const container = e.target.parentNode const container = e.target.parentNode
const imgs = [...container.querySelectorAll('img')].map(i => i.src) const imgs = [...container.querySelectorAll('img')].map(i => i.src)
@@ -205,7 +206,7 @@ const CommentItem = {
lightboxVisible.value = true lightboxVisible.value = true
} }
} }
return { showReplies, toggleReplies, showEditor, toggleEditor, submitReply, copyCommentLink, renderMarkdown, isWaitingForReply, commentMenuItems, deleteComment, lightboxVisible, lightboxIndex, lightboxImgs, handleImageClick, loggedIn } return { showReplies, toggleReplies, showEditor, toggleEditor, submitReply, copyCommentLink, renderMarkdown, isWaitingForReply, commentMenuItems, deleteComment, lightboxVisible, lightboxIndex, lightboxImgs, handleContentClick, loggedIn }
} }
} }

View File

@@ -1,15 +1,39 @@
import MarkdownIt from 'markdown-it' import MarkdownIt from 'markdown-it'
import hljs from 'highlight.js'
import 'highlight.js/styles/github.css'
import { toast } from '../main'
const md = new MarkdownIt({ const md = new MarkdownIt({
html: false, html: false,
linkify: true, linkify: true,
breaks: true breaks: true,
highlight: (str, lang) => {
let code = ''
if (lang && hljs.getLanguage(lang)) {
code = hljs.highlight(str, { language: lang, ignoreIllegals: true }).value
} else {
code = hljs.highlightAuto(str).value
}
return `<pre class="code-block"><button class="copy-code-btn">复制</button><code class="hljs language-${lang || ''}">${code}</code></pre>`
}
}) })
export function renderMarkdown(text) { export function renderMarkdown(text) {
return md.render(text || '') return md.render(text || '')
} }
export function handleMarkdownClick(e) {
if (e.target.classList.contains('copy-code-btn')) {
const pre = e.target.closest('pre')
const codeEl = pre && pre.querySelector('code')
if (codeEl) {
navigator.clipboard.writeText(codeEl.innerText).then(() => {
toast.success('已复制')
})
}
}
}
export function stripMarkdown(text) { export function stripMarkdown(text) {
const html = md.render(text || '') const html = md.render(text || '')
const el = document.createElement('div') const el = document.createElement('div')

View File

@@ -10,7 +10,7 @@
<div class="about-tabs-item-label">{{ tab.label }}</div> <div class="about-tabs-item-label">{{ tab.label }}</div>
</div> </div>
</div> </div>
<div class="about-content" v-html="renderMarkdown(content)"></div> <div class="about-content" v-html="renderMarkdown(content)" @click="handleContentClick"></div>
<div class="about-loading" v-if="isFetching"> <div class="about-loading" v-if="isFetching">
<l-hatch-spinner size="100" stroke="10" speed="1" color="var(--primary-color)" /> <l-hatch-spinner size="100" stroke="10" speed="1" color="var(--primary-color)" />
</div> </div>
@@ -19,7 +19,7 @@
<script> <script>
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { renderMarkdown } from '../utils/markdown' import { renderMarkdown, handleMarkdownClick } from '../utils/markdown'
import { hatch } from 'ldrs' import { hatch } from 'ldrs'
hatch.register() hatch.register()
@@ -61,7 +61,11 @@ export default {
loadContent(tabs[0].file) loadContent(tabs[0].file)
}) })
return { tabs, selectedTab, content, renderMarkdown, selectTab, isFetching } const handleContentClick = e => {
handleMarkdownClick(e)
}
return { tabs, selectedTab, content, renderMarkdown, selectTab, isFetching, handleContentClick }
} }
} }
</script> </script>

View File

@@ -51,7 +51,7 @@
<div class="user-name">{{ author.username }}</div> <div class="user-name">{{ author.username }}</div>
<div class="post-time">{{ postTime }}</div> <div class="post-time">{{ postTime }}</div>
</div> </div>
<div class="info-content-text" v-html="renderMarkdown(postContent)" @click="handleImageClick"></div> <div class="info-content-text" v-html="renderMarkdown(postContent)" @click="handleContentClick"></div>
<div class="article-footer-container"> <div class="article-footer-container">
<ReactionsGroup v-model="postReactions" content-type="post" :content-id="postId"> <ReactionsGroup v-model="postReactions" content-type="post" :content-id="postId">
@@ -113,7 +113,7 @@ import ArticleTags from '../components/ArticleTags.vue'
import ArticleCategory from '../components/ArticleCategory.vue' import ArticleCategory from '../components/ArticleCategory.vue'
import ReactionsGroup from '../components/ReactionsGroup.vue' import ReactionsGroup from '../components/ReactionsGroup.vue'
import DropdownMenu from '../components/DropdownMenu.vue' import DropdownMenu from '../components/DropdownMenu.vue'
import { renderMarkdown } from '../utils/markdown' import { renderMarkdown, handleMarkdownClick } from '../utils/markdown'
import { API_BASE_URL, toast } from '../main' import { API_BASE_URL, toast } from '../main'
import { getToken, authState } from '../utils/auth' import { getToken, authState } from '../utils/auth'
import TimeManager from '../utils/time' import TimeManager from '../utils/time'
@@ -237,7 +237,8 @@ export default {
return false return false
} }
const handleImageClick = e => { const handleContentClick = e => {
handleMarkdownClick(e)
if (e.target.tagName === 'IMG') { if (e.target.tagName === 'IMG') {
const container = e.target.parentNode const container = e.target.parentNode
const imgs = [...container.querySelectorAll('img')].map(i => i.src) const imgs = [...container.querySelectorAll('img')].map(i => i.src)
@@ -511,7 +512,7 @@ export default {
lightboxVisible, lightboxVisible,
lightboxIndex, lightboxIndex,
lightboxImgs, lightboxImgs,
handleImageClick, handleContentClick,
isMobile isMobile
} }
} }