vue實(shí)現(xiàn)pre代碼塊組件

在做一個(gè)文檔頁(yè)面時(shí),我遇到一個(gè)高頻需求:
后端返回的是 HTML 富文本(v-html 渲染)
里面有 <pre><code></code></pre> 的代碼示例
希望有更好的代碼塊體驗(yàn):行號(hào)、主題色、橫向滾動(dòng)、統(tǒng)一樣式
于是我抽了一個(gè)公共組件 CodeBlock,并額外提供一個(gè)函數(shù)給富文本場(chǎng)景做相關(guān)處理,把原生pre > code自動(dòng)轉(zhuǎn)成同一套 UI 結(jié)構(gòu)。

效果

  • 支持 行號(hào)
  • 支持 主題切換:default / dark / blue / green
  • 支持 自定義覆蓋顏色
  • 支持普通頁(yè)面直接 <CodeBlock />使用
  • 支持富文本v-html場(chǎng)景自動(dòng)裝飾(保持一致樣式)

目錄結(jié)構(gòu)

src/components/CodeBlock/
├─ index.vue
├─ themes.ts
└─ decorateArticleCodeBlocks.ts

主題定義:themes.ts

export type CodeBlockThemeId = "default" | "dark" | "blue" | "green";

export interface CodeBlockThemePalette {
  backgroundColor: string;
  borderColor: string;
  textColor: string;
  gutterColor: string;
  gutterBorderColor: string;
}

export const CODE_BLOCK_THEMES: Record<CodeBlockThemeId, CodeBlockThemePalette> = {
  default: {
    backgroundColor: "#f6f8fa",
    borderColor: "#e8eaed",
    textColor: "#1a1a1a",
    gutterColor: "#8c959f",
    gutterBorderColor: "#e1e4e8",
  },
  dark: {
    backgroundColor: "#1e1e1e",
    borderColor: "#3c3c3c",
    textColor: "#d4d4d4",
    gutterColor: "#858585",
    gutterBorderColor: "#3c3c3c",
  },
  blue: {
    backgroundColor: "#f0f7ff",
    borderColor: "#c8e1ff",
    textColor: "#1a1a1a",
    gutterColor: "#6b7c93",
    gutterBorderColor: "#b8d4f0",
  },
  green: {
    backgroundColor: "#f6faf6",
    borderColor: "#d8ead8",
    textColor: "#1a1a1a",
    gutterColor: "#6b8f6b",
    gutterBorderColor: "#c8dcc8",
  },
};

export function getCodeBlockThemePalette(theme: CodeBlockThemeId): CodeBlockThemePalette {
  return CODE_BLOCK_THEMES[theme] ?? CODE_BLOCK_THEMES.default;
}

組件實(shí)現(xiàn):index.vue

<template>
  <pre
    class="cd-code-block"
    :class="[{ 'cd-code-block--numbered': showLineNumbers }, languageClass]"
    :data-theme="theme"
    :style="preStyle"
  >
    <span
      v-if="showLineNumbers"
      class="cd-code-block__gutter"
      aria-hidden="true"
      :style="gutterStyle"
    >{{ gutterText }}</span>
    <code class="cd-code-block__code" :class="codeClass">{{ content }}</code>
  </pre>
</template>

<script setup lang="ts">
import { computed } from "vue";
import { type CodeBlockThemeId, getCodeBlockThemePalette, type CodeBlockThemePalette } from "./themes";

const props = withDefaults(
  defineProps<{
    content: string;
    theme?: CodeBlockThemeId;
    showLineNumbers?: boolean;
    backgroundColor?: string;
    borderColor?: string;
    gutterColor?: string;
    gutterBorderColor?: string;
    textColor?: string;
    languageClass?: string;
    codeClass?: string;
  }>(),
  {
    theme: "default",
    showLineNumbers: true,
    languageClass: "",
    codeClass: "",
  }
);

const palette = computed((): CodeBlockThemePalette => {
  const base = getCodeBlockThemePalette(props.theme);
  return {
    backgroundColor: props.backgroundColor ?? base.backgroundColor,
    borderColor: props.borderColor ?? base.borderColor,
    textColor: props.textColor ?? base.textColor,
    gutterColor: props.gutterColor ?? base.gutterColor,
    gutterBorderColor: props.gutterBorderColor ?? base.gutterBorderColor,
  };
});

const lineCount = computed(() => Math.max(props.content.split(/\n/).length, 1));
const gutterText = computed(() =>
  Array.from({ length: lineCount.value }, (_, i) => String(i + 1)).join("\n")
);

const gutterStyle = computed(() => ({
  minWidth: `${String(lineCount.value).length + 0.5}ch`,
  color: palette.value.gutterColor,
  borderRight: `1px solid ${palette.value.gutterBorderColor}`,
}));

const preStyle = computed(() => ({
  backgroundColor: palette.value.backgroundColor,
  border: `1px solid ${palette.value.borderColor}`,
  color: palette.value.textColor,
}));
</script>

<style lang="less">
.cd-code-block {
  box-sizing: border-box;
  margin: 12px 0;
  padding: 14px 16px;
  font-size: 13px;
  line-height: 1.55;
  border-radius: 8px;
  -webkit-overflow-scrolling: touch;
}
.cd-code-block:not(.cd-code-block--numbered) { overflow-x: auto; }

.cd-code-block--numbered {
  display: flex;
  align-items: stretch;
  padding: 12px 14px 12px 12px;
}

.cd-code-block__gutter {
  flex: 0 0 auto;
  padding-right: 12px;
  margin-right: 12px;
  text-align: right;
  user-select: none;
  white-space: pre;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  border-right: 1px solid #e1e4e8;
}

.cd-code-block__code {
  display: block;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  white-space: pre;
  word-break: normal;
  overflow-wrap: normal;
  background: transparent;
}
.cd-code-block--numbered .cd-code-block__code {
  flex: 1 1 auto;
  min-width: 0;
  overflow-x: auto;
}
</style>

富文本場(chǎng)景支持:decorateArticleCodeBlocks.ts

  • 當(dāng)你只能用 v-html 渲染后端 HTML 時(shí),這個(gè)函數(shù)很有用。
import { type CodeBlockThemeId, getCodeBlockThemePalette } from "./themes";

export interface DecorateArticleCodeBlocksOptions {
  theme?: CodeBlockThemeId;
  backgroundColor?: string;
  borderColor?: string;
  textColor?: string;
  gutterColor?: string;
  gutterBorderColor?: string;
}

export function decorateArticleCodeBlocks(root: HTMLElement | null, options?: DecorateArticleCodeBlocksOptions) {
  if (!root) return;

  const base = getCodeBlockThemePalette(options?.theme ?? "default");
  const colors = {
    backgroundColor: options?.backgroundColor ?? base.backgroundColor,
    borderColor: options?.borderColor ?? base.borderColor,
    textColor: options?.textColor ?? base.textColor,
    gutterColor: options?.gutterColor ?? base.gutterColor,
    gutterBorderColor: options?.gutterBorderColor ?? base.gutterBorderColor,
  };

  root.querySelectorAll("pre").forEach((preEl) => {
    const pre = preEl as HTMLPreElement;
    if (pre.dataset.lineNumbersApplied === "1") return;

    const code = pre.querySelector(":scope > code");
    if (!code) return;

    const raw = code.textContent ?? "";
    const lineCount = Math.max(raw.split(/\n/).length, 1);
    const gutterText = Array.from({ length: lineCount }, (_, i) => String(i + 1)).join("\n");

    pre.dataset.lineNumbersApplied = "1";
    pre.classList.add("cd-code-block", "cd-code-block--numbered");
    pre.style.backgroundColor = colors.backgroundColor;
    pre.style.border = `1px solid ${colors.borderColor}`;
    pre.style.color = colors.textColor;

    const gutter = document.createElement("span");
    gutter.className = "cd-code-block__gutter";
    gutter.setAttribute("aria-hidden", "true");
    gutter.textContent = gutterText;
    gutter.style.minWidth = `${String(lineCount).length + 0.5}ch`;
    gutter.style.color = colors.gutterColor;
    gutter.style.borderRight = `1px solid ${colors.gutterBorderColor}`;

    const newCode = document.createElement("code");
    newCode.className = `cd-code-block__code ${(code as HTMLElement).className || ""}`.trim();
    newCode.textContent = raw;

    pre.replaceChildren(gutter, newCode);
  });
}

如何使用

  • 普通組件場(chǎng)景
<CodeBlock theme="dark" :content="snippet" language-class="language-bash" />
  • 富文本場(chǎng)景(v-html)
import { nextTick } from "vue";
import "@/components/CodeBlock/index.vue"; // 引入樣式
import { decorateArticleCodeBlocks } from "@/components/CodeBlock/decorateArticleCodeBlocks";

await nextTick();
decorateArticleCodeBlocks(containerRef.value, { theme: "default" });

參數(shù)說(shuō)明(簡(jiǎn)版)

  • content: 代碼字符串(必填)

  • theme: default | dark | blue | green

  • showLineNumbers: 是否顯示行號(hào)

  • backgroundColor / borderColor / textColor / gutterColor / gutterBorderColor: 可覆蓋主題顏色

  • languageClass / codeClass: 透?jìng)黝?lèi)名,方便和語(yǔ)法高亮庫(kù)配合

  • 如果頁(yè)面是后端富文本,建議統(tǒng)一走 decorateArticleCodeBlocks,能保持視覺(jué)一致。

  • 如果是前端自渲染文檔,直接用<CodeBlock />更清晰。

  • 主題優(yōu)先走 theme,只在品牌風(fēng)格要求時(shí)做單項(xiàng)覆蓋。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容