Skip to content

实现 代码截图生成 工具

分类:developer | 标签:代码、截图、highlight 将代码转换为精美图片分享

2.1 功能说明

将代码转换为精美图片分享

2.1.1 使用指南

功能说明

将代码转换为精美的图片,适合分享到博客、社交媒体或文档。

  • 语法高亮:基于 highlight.js,支持 190+ 语言自动识别。
  • 主题切换:深色(Atom One Dark)/ 浅色(Atom One Light)。
  • 窗口样式:可选 macOS 风格红/黄/绿圆点标题栏,可选背景渐变。
  • 导出格式:SVG(矢量、可缩放)或 PNG(栅格化,便于分享)。
  • 注意:PNG 导出使用 SVG foreignObject + canvas,部分浏览器可能因安全策略无法栅格化,此时会自动降级为 SVG 下载。

2.2 代码实现

2.2.1 组件结构

本工具是一个基于 Vue 3 <script setup> 语法的单文件组件(SFC),统一包裹在 ToolPageShell 组件内,由它提供页面标题、工具 ID、分类与「使用指南」插槽等通用外壳;核心业务逻辑(响应式状态、计算属性、事件处理函数)全部写在 <script setup> 中,输入/输出通过 el-inputel-button 等 Element Plus 组件与用户交互,所有数据均在浏览器本地处理,不会上传服务器。

2.2.2 核心逻辑概览

<script setup> 中定义的主要函数/方法:

  • greet()
  • escapeHtml()
  • getSvgString()
  • exportSvg()
  • exportPng()

2.2.3 关键实现代码

以下是该工具的完整 <script setup> 实现(核心代码)。模板(<template>)与样式(<style>)已省略。

vue
import { ref, computed, watch, nextTick } from 'vue'
import ToolPageShell from '@/components/ToolPageShell.vue'
import hljs from '@/vendor/highlight.js/highlight.min.js'

const code = ref('// 在此输入代码\nconst greet = (name) => {\n  console.log(`Hello, ${name}!`);\n};\ngreet("World");')
const lang = ref('javascript')
const theme = ref('dark')
const showTitleBar = ref(true)
const showGradient = ref(true)
const gradColor1 = ref('#667eea')
const gradColor2 = ref('#764ba2')
const fontSize = ref(14)
const padding = ref(32)
const previewRef = ref(null)

const themeBg = computed(() => theme.value === 'dark' ? '#282c34' : '#fafafa')
const themeFg = computed(() => theme.value === 'dark' ? '#abb2bf' : '#383a42')

// Inline the highlight.js theme CSS (atom-one-dark / atom-one-light)
const darkCss = `.hljs{color:#abb2bf;background:#282c34}.hljs-comment,.hljs-quote{color:#5c6370;font-style:italic}.hljs-doctag,.hljs-formula,.hljs-keyword{color:#c678dd}.hljs-deletion,.hljs-name,.hljs-section,.hljs-selector-tag,.hljs-subst{color:#e06c75}.hljs-literal{color:#56b6c2}.hljs-addition,.hljs-attribute,.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#98c379}.hljs-attr,.hljs-number,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-pseudo,.hljs-template-variable,.hljs-type,.hljs-variable{color:#d19a66}.hljs-bullet,.hljs-link,.hljs-meta,.hljs-selector-id,.hljs-symbol,.hljs-title{color:#61aeee}.hljs-built_in,.hljs-class .hljs-title,.hljs-title.class_{color:#e6c07b}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}.hljs-link{text-decoration:underline}`

const lightCss = `.hljs{color:#383a42;background:#fafafa}.hljs-comment,.hljs-quote{color:#a0a1a7;font-style:italic}.hljs-doctag,.hljs-formula,.hljs-keyword{color:#a626a4}.hljs-deletion,.hljs-name,.hljs-section,.hljs-selector-tag,.hljs-subst{color:#e45649}.hljs-literal{color:#0184bb}.hljs-addition,.hljs-attribute,.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#50a14f}.hljs-attr,.hljs-number,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-pseudo,.hljs-template-variable,.hljs-type,.hljs-variable{color:#986801}.hljs-bullet,.hljs-link,.hljs-meta,.hljs-selector-id,.hljs-symbol,.hljs-title{color:#4078f2}.hljs-built_in,.hljs-class .hljs-title,.hljs-title.class_{color:#c18401}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}.hljs-link{text-decoration:underline}`

function escapeHtml(str) {
  return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
}

const svgHtml = computed(() => {
  if (!code.value.trim()) return '<div style="color:#999;text-align:center;padding:40px">输入代码后自动预览</div>'

  let highlighted
  try {
    if (lang.value) {
      highlighted = hljs.highlight(code.value, { language: lang.value }).value
    } else {
      highlighted = hljs.highlightAuto(code.value).value
    }
  } catch (e) {
    highlighted = escapeHtml(code.value)
  }

  const themeCss = theme.value === 'dark' ? darkCss : lightCss
  const bg = themeBg.value
  const fg = themeFg.value

  // Title bar dots
  const titleBar = showTitleBar.value
    ? `<div style="display:flex;align-items:center;padding:10px 16px;background:${bg};border-bottom:1px solid rgba(255,255,255,0.06)">
         <span style="display:inline-block;width:12px;height:12px;border-radius:50%;background:#ff5f56;margin-right:8px"></span>
         <span style="display:inline-block;width:12px;height:12px;border-radius:50%;background:#ffbd2e;margin-right:8px"></span>
         <span style="display:inline-block;width:12px;height:12px;border-radius:50%;background:#27c93f;margin-right:8px"></span>
         <span style="color:${fg};opacity:0.6;font-size:12px;margin-left:12px;font-family:monospace">${lang.value || 'auto'}</span>
       </div>`
    : ''

  const gradientStyle = showGradient.value
    ? `background:linear-gradient(135deg, ${gradColor1.value}, ${gradColor2.value});`
    : `background:${bg};`

  // Build inner HTML
  const inner = `<div style="${gradientStyle}padding:${padding.value}px;border-radius:8px;display:inline-block">
    <div style="background:${bg};border-radius:6px;overflow:hidden;box-shadow:0 8px 32px rgba(0,0,0,0.2)">
      ${titleBar}
      <pre style="margin:0;padding:16px 20px;overflow-x:auto"><code class="hljs" style="font-family:'Courier New',monospace;font-size:${fontSize.value}px;line-height:1.6">${highlighted}</code></pre>
    </div>
  </div>`

  // SVG with foreignObject
  const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="680" height="400" viewBox="0 0 680 400">
    <foreignObject width="100%" height="100%">
      <div xmlns="http://www.w3.org/1999/xhtml">
        <style>${themeCss}</style>
        ${inner}
      </div>
    </foreignObject>
  </svg>`

  return svg
})

function getSvgString() {
  return svgHtml.value
}

function exportSvg() {
  const svg = getSvgString()
  const blob = new Blob([svg], { type: 'image/svg+xml;charset=utf-8' })
  const url = URL.createObjectURL(blob)
  const a = document.createElement('a')
  a.href = url
  a.download = 'code-screenshot.svg'
  a.click()
  URL.revokeObjectURL(url)
}

async function exportPng() {
  const svg = getSvgString()
  const blob = new Blob([svg], { type: 'image/svg+xml;charset=utf-8' })
  const url = URL.createObjectURL(blob)
  const img = new Image()
  img.crossOrigin = 'anonymous'

  await new Promise((resolve, reject) => {
    img.onload = resolve
    img.onerror = reject
    img.src = url
  }).catch(() => {})

  const canvas = document.createElement('canvas')
  const scale = 2
  canvas.width = 680 * scale
  canvas.height = 400 * scale
  const ctx = canvas.getContext('2d')
  ctx.fillStyle = '#ffffff'
  ctx.fillRect(0, 0, canvas.width, canvas.height)

  try {
    ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
    canvas.toBlob((pngBlob) => {
      if (pngBlob) {
        const pngUrl = URL.createObjectURL(pngBlob)
        const a = document.createElement('a')
        a.href = pngUrl
        a.download = 'code-screenshot.png'
        a.click()
        URL.revokeObjectURL(pngUrl)
        URL.revokeObjectURL(url)
      } else {
        // Fallback to SVG
        exportSvg()
      }
    }, 'image/png')
  } catch (e) {
    // Canvas tainted, fallback to SVG
    exportSvg()
    URL.revokeObjectURL(url)
  }
}

2.3 效果截图

代码截图生成 效果截图

工具访问地址:https://www.i91tools.com/tools/code-image