在做一個(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 | greenshowLineNumbers: 是否顯示行號(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)覆蓋。