From 0e6fde08c7483f1082cb7b2b99751a07888ccaac Mon Sep 17 00:00:00 2001
From: xuxin <15279969124@163.com>
Date: Thu, 28 May 2026 17:41:23 +0800
Subject: [PATCH] =?UTF-8?q?=E9=A1=B5=E9=9D=A2=E5=8F=B3=E4=BE=A7=E8=81=8A?=
=?UTF-8?q?=E5=A4=A9=E5=8A=A9=E6=89=8B=E7=9A=84ai=E8=BE=93=E5=87=BA?=
=?UTF-8?q?=E7=BB=93=E6=9E=9C=E5=8A=A0=E4=B8=8Amd=E6=96=87=E6=A1=A3?=
=?UTF-8?q?=E6=A0=BC=E5=BC=8F=E6=98=BE=E7=A4=BA?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
package.json | 2 +
pnpm-lock.yaml | 71 +++++++++++++++++++
src/assets/styles/components/ai-chat.scss | 84 +++++++++++++++++++++++
src/components/AiChat.vue | 14 +++-
tsconfig.json | 1 +
5 files changed, 170 insertions(+), 2 deletions(-)
diff --git a/package.json b/package.json
index 6187a6e..8325a3d 100644
--- a/package.json
+++ b/package.json
@@ -13,12 +13,14 @@
"axios": "^1.13.6",
"element-plus": "^2.13.3",
"html2pdf.js": "^0.14.0",
+ "markdown-it": "^14.2.0",
"sass": "^1.97.3",
"vue": "^3.5.25",
"vue-router": "^4.6.4",
"vuex": "^4.1.0"
},
"devDependencies": {
+ "@types/markdown-it": "^14.1.2",
"@types/node": "^24.10.1",
"@vitejs/plugin-vue": "^6.0.2",
"@vue/tsconfig": "^0.8.1",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 4a18fdf..b24d05d 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -20,6 +20,9 @@ importers:
html2pdf.js:
specifier: ^0.14.0
version: 0.14.0
+ markdown-it:
+ specifier: ^14.2.0
+ version: 14.2.0
sass:
specifier: ^1.97.3
version: 1.97.3
@@ -33,6 +36,9 @@ importers:
specifier: ^4.1.0
version: 4.1.0(vue@3.5.29(typescript@5.9.3))
devDependencies:
+ '@types/markdown-it':
+ specifier: ^14.1.2
+ version: 14.1.2
'@types/node':
specifier: ^24.10.1
version: 24.11.0
@@ -487,12 +493,21 @@ packages:
'@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
+ '@types/linkify-it@5.0.0':
+ resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==}
+
'@types/lodash-es@4.17.12':
resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==}
'@types/lodash@4.17.24':
resolution: {integrity: sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==}
+ '@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@24.11.0':
resolution: {integrity: sha512-fPxQqz4VTgPI/IQ+lj9r0h+fDR66bzoeMGHp8ASee+32OSGIkeASsoZuJixsQoVef1QJbeubcPBxKk22QVoWdw==}
@@ -587,6 +602,9 @@ packages:
alien-signals@3.1.2:
resolution: {integrity: sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw==}
+ argparse@2.0.1:
+ resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
+
async-validator@4.2.5:
resolution: {integrity: sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==}
@@ -658,6 +676,10 @@ packages:
peerDependencies:
vue: ^3.3.0
+ entities@4.5.0:
+ resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
+ engines: {node: '>=0.12'}
+
entities@7.0.1:
resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==}
engines: {node: '>=0.12'}
@@ -783,6 +805,9 @@ packages:
jspdf@4.2.1:
resolution: {integrity: sha512-YyAXyvnmjTbR4bHQRLzex3CuINCDlQnBqoSYyjJwTP2x9jDLuKDzy7aKUl0hgx3uhcl7xzg32agn5vlie6HIlQ==}
+ linkify-it@5.0.1:
+ resolution: {integrity: sha512-wVoTjP4Q6R0NW5hiZkVJaFZPWgtXfoGF+6LucL3/FtiNjmcHhYjEr5f1Kqjirc1nBW07J/ZuRFumqr2oqccEWg==}
+
local-pkg@1.1.2:
resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==}
engines: {node: '>=14'}
@@ -803,10 +828,17 @@ packages:
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
+ markdown-it@14.2.0:
+ resolution: {integrity: sha512-1TGiQiJVRQ3NPmZH6sx5Cfnmg6GQm9jvC1ch4TK511NjSJvjzKLzn5pPfZRNZkRPZP0HqCioSndqH8v2nRaWVQ==}
+ hasBin: true
+
math-intrinsics@1.1.0:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
+ mdurl@2.0.0:
+ resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==}
+
memoize-one@6.0.0:
resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==}
@@ -870,6 +902,10 @@ packages:
proxy-from-env@1.1.0:
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
+ punycode.js@2.3.1:
+ resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==}
+ engines: {node: '>=6'}
+
quansync@0.2.11:
resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==}
@@ -931,6 +967,9 @@ packages:
engines: {node: '>=14.17'}
hasBin: true
+ uc.micro@2.1.0:
+ resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==}
+
ufo@1.6.3:
resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==}
@@ -1328,12 +1367,21 @@ snapshots:
'@types/estree@1.0.8': {}
+ '@types/linkify-it@5.0.0': {}
+
'@types/lodash-es@4.17.12':
dependencies:
'@types/lodash': 4.17.24
'@types/lodash@4.17.24': {}
+ '@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@24.11.0':
dependencies:
undici-types: 7.16.0
@@ -1460,6 +1508,8 @@ snapshots:
alien-signals@3.1.2: {}
+ argparse@2.0.1: {}
+
async-validator@4.2.5: {}
asynckit@0.4.0: {}
@@ -1553,6 +1603,8 @@ snapshots:
transitivePeerDependencies:
- '@vue/composition-api'
+ entities@4.5.0: {}
+
entities@7.0.1: {}
es-define-property@1.0.1: {}
@@ -1702,6 +1754,10 @@ snapshots:
dompurify: 3.3.3
html2canvas: 1.4.1
+ linkify-it@5.0.1:
+ dependencies:
+ uc.micro: 2.1.0
+
local-pkg@1.1.2:
dependencies:
mlly: 1.8.0
@@ -1722,8 +1778,19 @@ snapshots:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
+ markdown-it@14.2.0:
+ dependencies:
+ argparse: 2.0.1
+ entities: 4.5.0
+ linkify-it: 5.0.1
+ mdurl: 2.0.0
+ punycode.js: 2.3.1
+ uc.micro: 2.1.0
+
math-intrinsics@1.1.0: {}
+ mdurl@2.0.0: {}
+
memoize-one@6.0.0: {}
mime-db@1.52.0: {}
@@ -1783,6 +1850,8 @@ snapshots:
proxy-from-env@1.1.0: {}
+ punycode.js@2.3.1: {}
+
quansync@0.2.11: {}
raf@3.4.1:
@@ -1864,6 +1933,8 @@ snapshots:
typescript@5.9.3: {}
+ uc.micro@2.1.0: {}
+
ufo@1.6.3: {}
undici-types@7.16.0: {}
diff --git a/src/assets/styles/components/ai-chat.scss b/src/assets/styles/components/ai-chat.scss
index a936e18..06ab267 100644
--- a/src/assets/styles/components/ai-chat.scss
+++ b/src/assets/styles/components/ai-chat.scss
@@ -94,6 +94,90 @@
font-size: 0.13rem;
line-height: 1.6;
max-width: 85%;
+
+ // ===== Markdown 渲染样式 =====
+ p {
+ margin: 0 0 0.08rem;
+ &:last-child { margin-bottom: 0; }
+ }
+
+ h1, h2, h3, h4, h5, h6 {
+ margin: 0.12rem 0 0.06rem;
+ font-weight: 600;
+ line-height: 1.4;
+ }
+ h1 { font-size: 0.18rem; }
+ h2 { font-size: 0.16rem; }
+ h3 { font-size: 0.14rem; }
+
+ ul, ol {
+ padding-left: 0.2rem;
+ margin: 0.06rem 0;
+ }
+
+ li {
+ margin-bottom: 0.04rem;
+ }
+
+ code {
+ background: #f1f5f9;
+ padding: 0.01rem 0.04rem;
+ border-radius: 0.03rem;
+ font-size: 0.12rem;
+ font-family: 'Courier New', monospace;
+ }
+
+ pre {
+ background: #1e293b;
+ color: #e2e8f0;
+ padding: 0.1rem 0.12rem;
+ border-radius: 0.06rem;
+ overflow-x: auto;
+ margin: 0.08rem 0;
+
+ code {
+ background: none;
+ padding: 0;
+ color: inherit;
+ font-size: 0.11rem;
+ }
+ }
+
+ blockquote {
+ border-left: 3px solid #cbd5e1;
+ padding-left: 0.1rem;
+ margin: 0.08rem 0;
+ color: #64748b;
+ }
+
+ a {
+ color: #2563eb;
+ text-decoration: underline;
+ }
+
+ table {
+ border-collapse: collapse;
+ margin: 0.08rem 0;
+ width: 100%;
+ font-size: 0.12rem;
+
+ th, td {
+ border: 1px solid #e5e7eb;
+ padding: 0.04rem 0.08rem;
+ text-align: left;
+ }
+
+ th {
+ background: #f8fafc;
+ font-weight: 600;
+ }
+ }
+
+ hr {
+ border: none;
+ border-top: 1px solid #e5e7eb;
+ margin: 0.1rem 0;
+ }
}
&__msg--ai &__msg-bubble {
diff --git a/src/components/AiChat.vue b/src/components/AiChat.vue
index 1bce7f2..6538b3c 100644
--- a/src/components/AiChat.vue
+++ b/src/components/AiChat.vue
@@ -74,12 +74,22 @@
import { ref, computed, watch, nextTick, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { useStore } from 'vuex'
+import markdownit from 'markdown-it'
import MemberDialog from '@/components/MemberDialog.vue'
import AiThinkingIndicator from '@/components/tools/AiThinkingIndicator.vue'
import { sendNovaChat } from '@/utils/aiRequest'
import type { NovaChatHistoryItem } from '@/utils/aiRequest'
import { fetchResumeList } from '@/api/resume'
+// ==================== Markdown 渲染实例 ====================
+
+/** markdown-it 实例,用于将 AI 回复的 Markdown 文本渲染为 HTML */
+const md = markdownit({
+ html: false, // 禁止原始 HTML(防 XSS)
+ breaks: true, // 将换行符转为
+ linkify: true, // 自动识别链接
+})
+
// ==================== Props ====================
/** 组件属性 */
@@ -180,9 +190,9 @@ const userQuestions = computed(() => quickQuestions.value)
// ==================== 内容格式化 ====================
-/** 将消息中的换行符转为 HTML 换行标签 */
+/** 将消息内容通过 markdown-it 渲染为 HTML */
function formatContent(content: string): string {
- return content.replace(/\n/g, '
')
+ return md.render(content)
}
// ==================== 滚动控制 ====================
diff --git a/tsconfig.json b/tsconfig.json
index dac5905..1412e6d 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -9,6 +9,7 @@
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
+ "allowSyntheticDefaultImports": true,
"noEmit": true,
"jsx": "preserve",
"strict": true,