feat: 添加 Markdown 渲染组件,支持代码高亮和格式化

This commit is contained in:
Timothy Yin 2025-03-21 18:23:35 +08:00
parent e876a4d88b
commit 8b87de67c1
Signed by: HoshinoSuzumi
GPG Key ID: 4052E565F04B122A
5 changed files with 122 additions and 3 deletions

View File

@ -26,7 +26,10 @@ const props = defineProps({
<span class="text-sm font-medium">{{ name }}</span>
</div>
<div class="rounded-lg bg-white/50 p-2 text-sm dark:bg-neutral-800/50">
<span v-if="message">{{ message }}</span>
<span v-if="message">
<Markdown :source="message" />
<!-- {{ message }} -->
</span>
<span v-else class="flex items-center gap-1.5">
<UIcon name="svg-spinners:3-dots-scale" class="text-lg -mt-0.5" />
<p class="text-xs">思考中</p>

View File

@ -38,7 +38,8 @@ defineProps({
class="rounded-lg bg-white/50 p-2 text-sm dark:bg-neutral-800/50 break-all text-justify"
>
<div v-if="message.message" class="prose prose-sm">
{{ message.message }}
<!-- {{ message.message }} -->
<Markdown :source="message.message" />
</div>
<span v-else class="flex items-center gap-1.5">
<UIcon name="svg-spinners:3-dots-scale" class="text-lg -mt-0.5" />

43
components/Markdown.vue Normal file
View File

@ -0,0 +1,43 @@
<script setup lang="ts">
import md from "markdown-it";
import hljs from "highlight.js";
import "highlight.js/styles/github-dark-dimmed.min.css";
const renderer = md({
html: true,
linkify: true,
typographer: true,
breaks: true,
highlight: function (str, lang) {
if (lang && hljs.getLanguage(lang)) {
try {
return `<pre class="hljs" style="overflow-x: auto"><code>${
hljs.highlight(str, { language: lang, ignoreIllegals: true }).value
}</code></pre>`;
} catch (_) {}
}
return (
'<pre class="hljs"><code>' + md().utils.escapeHtml(str) + "</code></pre>"
);
},
});
const props = defineProps({
source: {
type: String,
required: true,
},
});
</script>
<template>
<article
class="prose dark:prose-invert max-w-none prose-sm prose-neutral"
v-html="
renderer.render(source.replaceAll('\t', '&nbsp;&nbsp;&nbsp;&nbsp;'))
"
></article>
</template>
<style scoped></style>

View File

@ -15,6 +15,8 @@
"@tailwindcss/vite": "^4.0.14",
"@uniiem/uuid": "^0.2.1",
"dotenv": "^16.4.7",
"highlight.js": "^11.11.1",
"markdown-it": "^14.1.0",
"nuxt": "^3.16.0",
"pinia": "^3.0.1",
"tailwindcss": "^4.0.14",
@ -23,6 +25,7 @@
},
"packageManager": "pnpm@9.15.3+sha512.1f79bc245a66eb0b07c5d4d83131240774642caaa86ef7d0434ab47c0d16f66b04e21e0c086eb61e62c77efc4d7f7ec071afad3796af64892fae66509173893a",
"devDependencies": {
"@nuxt/ui": "^3.0.0"
"@nuxt/ui": "^3.0.0",
"@types/markdown-it": "^14.1.2"
}
}

69
pnpm-lock.yaml generated
View File

@ -23,6 +23,12 @@ importers:
dotenv:
specifier: ^16.4.7
version: 16.4.7
highlight.js:
specifier: ^11.11.1
version: 11.11.1
markdown-it:
specifier: ^14.1.0
version: 14.1.0
nuxt:
specifier: ^3.16.0
version: 3.16.0(@parcel/watcher@2.5.1)(@types/node@22.13.10)(db0@0.3.1)(ioredis@5.6.0)(lightningcss@1.29.2)(magicast@0.3.5)(rollup@4.35.0)(terser@5.39.0)(typescript@5.8.2)(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0)
@ -42,6 +48,9 @@ importers:
'@nuxt/ui':
specifier: ^3.0.0
version: 3.0.0(@babel/parser@7.26.9)(change-case@5.4.4)(db0@0.3.1)(embla-carousel@8.5.2)(ioredis@5.6.0)(magicast@0.3.5)(typescript@5.8.2)(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.0)(yaml@2.7.0))
'@types/markdown-it':
specifier: ^14.1.2
version: 14.1.2
packages:
@ -1064,6 +1073,15 @@ packages:
'@types/http-proxy@1.17.16':
resolution: {integrity: sha512-sdWoUajOB1cd0A8cRRQ1cfyWNbmFKLAqBB89Y8x5iYyG/mkJHc0YUH8pdWBy2omi9qtCpiIgGjuwO0dQST2l5w==}
'@types/linkify-it@5.0.0':
resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==}
'@types/markdown-it@14.1.2':
resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==}
'@types/mdurl@2.0.0':
resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==}
'@types/node@22.13.10':
resolution: {integrity: sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==}
@ -1981,6 +1999,10 @@ packages:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
highlight.js@11.11.1:
resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==}
engines: {node: '>=12.0.0'}
hookable@5.5.3:
resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
@ -2273,6 +2295,9 @@ packages:
resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
engines: {node: '>=14'}
linkify-it@5.0.0:
resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==}
listhen@1.9.0:
resolution: {integrity: sha512-I8oW2+QL5KJo8zXNWX046M134WchxsXC7SawLPvRQpogCbkyQIaFxPE89A2HiwR7vAK2Dm2ERBAmyjTYGYEpBg==}
hasBin: true
@ -2324,6 +2349,10 @@ packages:
magicast@0.3.5:
resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==}
markdown-it@14.1.0:
resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==}
hasBin: true
mdn-data@2.0.28:
resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==}
@ -2333,6 +2362,9 @@ packages:
mdn-data@2.12.2:
resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==}
mdurl@2.0.0:
resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==}
merge-stream@2.0.0:
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
@ -2829,6 +2861,10 @@ packages:
protocols@2.0.2:
resolution: {integrity: sha512-hHVTzba3wboROl0/aWRRG9dMytgH6ow//STBZh43l/wQgmMhYhOFi0EHWAPtoCz9IAUymsyP0TSBHkhgMEGNnQ==}
punycode.js@2.3.1:
resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==}
engines: {node: '>=6'}
quansync@0.2.8:
resolution: {integrity: sha512-4+saucphJMazjt7iOM27mbFCk+D9dd/zmgMDCzRZ8MEoBfYp7lAvoN38et/phRQF6wOPMy/OROBGgoWeSKyluA==}
@ -3183,6 +3219,9 @@ packages:
engines: {node: '>=14.17'}
hasBin: true
uc.micro@2.1.0:
resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==}
ufo@1.5.4:
resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==}
@ -4821,6 +4860,15 @@ snapshots:
dependencies:
'@types/node': 22.13.10
'@types/linkify-it@5.0.0': {}
'@types/markdown-it@14.1.2':
dependencies:
'@types/linkify-it': 5.0.0
'@types/mdurl': 2.0.0
'@types/mdurl@2.0.0': {}
'@types/node@22.13.10':
dependencies:
undici-types: 6.20.0
@ -5799,6 +5847,8 @@ snapshots:
dependencies:
function-bind: 1.1.2
highlight.js@11.11.1: {}
hookable@5.5.3: {}
http-errors@2.0.0:
@ -6033,6 +6083,10 @@ snapshots:
lilconfig@3.1.3: {}
linkify-it@5.0.0:
dependencies:
uc.micro: 2.1.0
listhen@1.9.0:
dependencies:
'@parcel/watcher': 2.5.1
@ -6106,12 +6160,23 @@ snapshots:
'@babel/types': 7.26.9
source-map-js: 1.2.1
markdown-it@14.1.0:
dependencies:
argparse: 2.0.1
entities: 4.5.0
linkify-it: 5.0.0
mdurl: 2.0.0
punycode.js: 2.3.1
uc.micro: 2.1.0
mdn-data@2.0.28: {}
mdn-data@2.0.30: {}
mdn-data@2.12.2: {}
mdurl@2.0.0: {}
merge-stream@2.0.0: {}
merge2@1.4.1: {}
@ -6764,6 +6829,8 @@ snapshots:
protocols@2.0.2: {}
punycode.js@2.3.1: {}
quansync@0.2.8: {}
queue-microtask@1.2.3: {}
@ -7133,6 +7200,8 @@ snapshots:
typescript@5.8.2: {}
uc.micro@2.1.0: {}
ufo@1.5.4: {}
ultrahtml@1.5.3: {}