Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
水玉婷
ai-wechat
Commits
54a022a9
Commit
54a022a9
authored
Dec 02, 2025
by
水玉婷
Browse files
feat:md添加图片,优化iframe高度自动获取
parent
5e75750d
Changes
6
Hide whitespace changes
Inline
Side-by-side
src/views/Home.vue
View file @
54a022a9
...
@@ -40,8 +40,8 @@
...
@@ -40,8 +40,8 @@
stage
:
'
wechat-demo
'
,
stage
:
'
wechat-demo
'
,
};
};
const
dialogSessionId
=
'
20251127180914709-00043912
'
;
//
const dialogSessionId = '20251127180914709-00043912';
//
const dialogSessionId = '';
const
dialogSessionId
=
''
;
const
detailData
=
ref
({
const
detailData
=
ref
({
title
:
'
国械小智
'
,
title
:
'
国械小智
'
,
});
});
...
...
src/views/components/AiChat.vue
View file @
54a022a9
...
@@ -106,7 +106,6 @@ import VoiceRecognition from './VoiceRecognition.vue'; // 导入语音识别组
...
@@ -106,7 +106,6 @@ import VoiceRecognition from './VoiceRecognition.vue'; // 导入语音识别组
import
AudioPlayer
from
'
./AudioPlayer.vue
'
;
// 导入音频播放器组件
import
AudioPlayer
from
'
./AudioPlayer.vue
'
;
// 导入音频播放器组件
import
{
createSSEService
,
type
SSEData
}
from
'
./utils/sseService
'
;
// 导入SSE服务
import
{
createSSEService
,
type
SSEData
}
from
'
./utils/sseService
'
;
// 导入SSE服务
import
{
createContentTemplateService
,
type
Message
}
from
'
./utils/contentTemplateService
'
;
// 导入模板服务
import
{
createContentTemplateService
,
type
Message
}
from
'
./utils/contentTemplateService
'
;
// 导入模板服务
import
{
init
}
from
'
echarts/types/src/echarts.all.js
'
;
// 定义组件属性接口
// 定义组件属性接口
interface
Props
{
interface
Props
{
...
@@ -390,39 +389,39 @@ const sendMessage = async (type: MessageType = 'text', params: MessageParams = {
...
@@ -390,39 +389,39 @@ const sendMessage = async (type: MessageType = 'text', params: MessageParams = {
scrollToBottom
();
scrollToBottom
();
try
{
try
{
// 调用外部传入的消息发送函数
// 调用外部传入的消息发送函数
if
(
props
.
onMessageSend
)
{
if
(
props
.
onMessageSend
)
{
const
sendContent
=
type
===
'
audio
'
?
audioUrl
!
:
messageContent
;
const
sendContent
=
type
===
'
audio
'
?
audioUrl
!
:
messageContent
;
console
.
log
(
`调用外部发送函数`
,
sendContent
);
console
.
log
(
`调用外部发送函数`
,
sendContent
);
await
props
.
onMessageSend
(
sendContent
);
await
props
.
onMessageSend
(
sendContent
);
}
else
{
}
else
{
// 默认的API调用逻辑
// 默认的API调用逻辑
console
.
log
(
`默认API调用逻辑`
,
dialogSessionId
.
value
);
console
.
log
(
`默认API调用逻辑`
,
dialogSessionId
.
value
);
const
requestData
=
type
===
'
audio
'
?
{
const
requestData
=
type
===
'
audio
'
?
{
questionLocalAudioFilePath
:
audioUrl
,
questionLocalAudioFilePath
:
audioUrl
,
audioDuration
:
durationTime
,
audioDuration
:
durationTime
,
...
props
.
params
,
...
props
.
params
,
}
:
{
}
:
{
question
:
messageContent
,
question
:
messageContent
,
...
props
.
params
,
...
props
.
params
,
};
};
const
response
=
await
fetch
(
`
${
props
.
apiBaseUrl
}
/aiService/ask/app/
${
props
.
params
?.
appId
}
`
,
{
const
response
=
await
fetch
(
`
${
props
.
apiBaseUrl
}
/aiService/ask/app/
${
props
.
params
?.
appId
}
`
,
{
method
:
'
POST
'
,
method
:
'
POST
'
,
headers
:
{
headers
:
{
'
Content-Type
'
:
'
application/json
'
,
'
Content-Type
'
:
'
application/json
'
,
'
token
'
:
props
.
token
,
'
token
'
:
props
.
token
,
'
x-session-id
'
:
props
.
token
,
'
x-session-id
'
:
props
.
token
,
'
x-app-code
'
:
props
.
appCode
||
''
'
x-app-code
'
:
props
.
appCode
||
''
},
},
body
:
JSON
.
stringify
(
requestData
)
body
:
JSON
.
stringify
(
requestData
)
});
});
const
data
=
await
response
.
json
();
const
data
=
await
response
.
json
();
if
(
data
.
code
===
0
)
{
if
(
data
.
code
===
0
)
{
console
.
log
(
`发送成功`
);
console
.
log
(
`发送成功`
);
}
}
}
}
}
catch
(
e
)
{
}
catch
(
e
)
{
console
.
error
(
`发送失败:`
,
e
);
console
.
error
(
`发送失败:`
,
e
);
}
finally
{
}
finally
{
...
@@ -478,7 +477,6 @@ const getChatRecord = async (dialogSessionId: string) => {
...
@@ -478,7 +477,6 @@ const getChatRecord = async (dialogSessionId: string) => {
const
response
=
await
fetch
(
`
${
props
.
apiBaseUrl
}
/aiService/ask/list/chat/
${
dialogSessionId
}
`
,
{
const
response
=
await
fetch
(
`
${
props
.
apiBaseUrl
}
/aiService/ask/list/chat/
${
dialogSessionId
}
`
,
{
method
:
'
GET
'
,
method
:
'
GET
'
,
headers
:
{
headers
:
{
'
Content-Type
'
:
'
application/json
'
,
'
token
'
:
props
.
token
,
'
token
'
:
props
.
token
,
'
x-session-id
'
:
props
.
token
,
'
x-session-id
'
:
props
.
token
,
'
x-app-code
'
:
props
.
appCode
||
''
'
x-app-code
'
:
props
.
appCode
||
''
...
...
src/views/components/style.less
View file @
54a022a9
...
@@ -627,7 +627,7 @@ li {
...
@@ -627,7 +627,7 @@ li {
iframe {
iframe {
width: 100%;
width: 100%;
height: 100%;
height: 100%;
min-height:
8
00px;
min-height:
6
00px;
border: none;
border: none;
border-radius: 8px;
border-radius: 8px;
background-color: @gray-1;
background-color: @gray-1;
...
@@ -930,6 +930,7 @@ li {
...
@@ -930,6 +930,7 @@ li {
line-height: 1.6;
line-height: 1.6;
color: @gray-7;
color: @gray-7;
background: none;
background: none;
white-space: initial;
// 标题样式
// 标题样式
h1,
h1,
...
@@ -1039,9 +1040,45 @@ li {
...
@@ -1039,9 +1040,45 @@ li {
margin-bottom: 8px;
margin-bottom: 8px;
}
}
// Markdown图片容器样式
.markdown-image-container {
img {
max-width: 100%;
height: auto;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
transition: transform 0.3s ease, box-shadow 0.3s ease;
&:hover {
transform: scale(1.02);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.2);
}
}
.image-error {
border: 1px solid #ff4d4f;
padding: 12px;
background-color: #fff2f0;
border-radius: 6px;
margin: 8px 0;
span {
color: #ff4d4f;
font-size: 14px;
}
}
.image-caption {
font-size: 13px;
color: #666;
font-style: italic;
line-height: 1.4;
}
}
// 引用块样式
// 引用块样式
blockquote {
blockquote {
border-left: 4px solid @
primary-color
;
border-left: 4px solid @
gray-2-3
;
margin-bottom: 8px;
margin-bottom: 8px;
padding: 0 16px;
padding: 0 16px;
font-style: italic;
font-style: italic;
...
@@ -1093,6 +1130,7 @@ li {
...
@@ -1093,6 +1130,7 @@ li {
overflow-x: auto;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
-webkit-overflow-scrolling: touch;
font-size:0;
font-size:0;
margin-bottom:8px;
// 滚动条样式
// 滚动条样式
&::-webkit-scrollbar {
&::-webkit-scrollbar {
...
...
src/views/components/utils/contentTemplateService.ts
View file @
54a022a9
...
@@ -126,9 +126,9 @@ export class ContentTemplateService {
...
@@ -126,9 +126,9 @@ export class ContentTemplateService {
`
;
`
;
},
},
// 简化的iframe模板
- 移除全屏功能,设置宽高100%固定
// 简化的iframe模板
iframe
:
(
iframeData
:
any
)
=>
{
iframe
:
(
iframeData
:
any
)
=>
{
const
{
tips
,
title
,
url
,
height
}
=
iframeData
||
{};
const
{
tips
,
title
,
url
}
=
iframeData
||
{};
return
`<div class="message-iframe iframe-loading">
return
`<div class="message-iframe iframe-loading">
<!-- 加载状态 -->
<!-- 加载状态 -->
<div class="iframe-loading">
<div class="iframe-loading">
...
@@ -145,12 +145,12 @@ export class ContentTemplateService {
...
@@ -145,12 +145,12 @@ export class ContentTemplateService {
<iframe
<iframe
src="
${
url
}
"
src="
${
url
}
"
width="100%"
width="100%"
height="
${
height
}
"
height="
100%
"
frameborder="0"
frameborder="0"
sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox"
sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox"
scrolling="no"
scrolling="no"
style="overflow: hidden;"
style="overflow: hidden;"
onload="this.parentElement.classList.add('iframe-loaded');
this.parentElement.classList.remove('iframe-loading');"
onload="
(function(){
this.parentElement.classList.add('iframe-loaded');this.parentElement.classList.remove('iframe-loading');
try{let iframe=this.contentDocument.getElementById('iframe_view');if(iframe&&iframe.contentDocument&&iframe.contentDocument.body){let nestedHeight=iframe.contentDocument.body.clientHeight + 30;if(nestedHeight>0){this.style.height=nestedHeight+'px';}}}catch(error){console.warn('无法获取嵌套iframe高度:',error);}}).call(this)
"
onerror="console.error('iframe加载失败:', this.src)"
onerror="console.error('iframe加载失败:', this.src)"
></iframe>
></iframe>
</div>`
;
</div>`
;
...
...
src/views/components/utils/markdownTemplate.ts
View file @
54a022a9
...
@@ -82,13 +82,17 @@ export const parseMarkdown = (text: string): string => {
...
@@ -82,13 +82,17 @@ export const parseMarkdown = (text: string): string => {
text
=
String
(
text
||
''
);
text
=
String
(
text
||
''
);
}
}
// 先进行HTML转义,防止XSS攻击
// 处理引用块(必须在HTML转义之前,避免>被转义为>)
text
=
text
text
=
text
.
replace
(
/^>
\s
*
(
.*
)
$/gim
,
'
<blockquote>$1</blockquote>
'
);
.
replace
(
/&/g
,
'
&
'
)
.
replace
(
/</g
,
'
<
'
)
// 处理图片(必须在任何格式处理之前,避免图片格式被破坏)
.
replace
(
/>/g
,
'
>
'
)
text
=
text
.
replace
(
/!
\[([^\]]
*
)\]\(([^\)]
+
)\)
/g
,
(
match
,
alt
,
src
)
=>
{
.
replace
(
/"/g
,
'
"
'
)
const
altText
=
alt
||
'
图片
'
;
.
replace
(
/'/g
,
'
'
'
);
return
`<div class="markdown-image-container">
<img src="
${
src
}
" alt="
${
altText
}
">
${
alt
?
`<div class="image-caption">
${
alt
}
</div>`
:
''
}
</div>`
;
});
// 处理基本的Markdown格式(在表格解析之前处理,确保表格单元格中的格式也能被处理)
// 处理基本的Markdown格式(在表格解析之前处理,确保表格单元格中的格式也能被处理)
text
=
processMarkdownFormat
(
text
);
text
=
processMarkdownFormat
(
text
);
...
@@ -147,14 +151,14 @@ export const parseMarkdown = (text: string): string => {
...
@@ -147,14 +151,14 @@ export const parseMarkdown = (text: string): string => {
// 使用统一的Markdown格式处理函数处理基础格式(包括标题)
// 使用统一的Markdown格式处理函数处理基础格式(包括标题)
text
=
processMarkdownFormat
(
text
);
text
=
processMarkdownFormat
(
text
);
// 处理代码块(支持语言标识)
// 处理代码块(支持语言标识)
- 必须在换行处理之前
text
=
text
.
replace
(
/```
(\w
+
)?\n([\s\S]
*
?)
```/gim
,
'
<pre><code class="language-$1">$2</code></pre>
'
);
text
=
text
.
replace
(
/```
(\w
+
)?\n([\s\S]
*
?)
```/gim
,
'
<pre><code class="language-$1">$2</code></pre>
'
);
text
=
text
.
replace
(
/```
([\s\S]
*
?)
```/gim
,
'
<pre><code>$1</code></pre>
'
);
text
=
text
.
replace
(
/```
([\s\S]
*
?)
```/gim
,
'
<pre><code>$1</code></pre>
'
);
// 处理链接(支持相对路径和绝对路径)
// 处理链接(支持相对路径和绝对路径)
text
=
text
.
replace
(
/
\[([^\]]
+
)\]\(([^
)
]
+
)\)
/gim
,
(
match
,
text
,
url
)
=>
{
text
=
text
.
replace
(
/
\[([^\]]
+
)\]\(([^
)
]
+
)\)
/gim
,
(
match
,
text
,
url
)
=>
{
// 验证URL格式
// 验证URL格式
const
isValidUrl
=
/^
(
https
?
|ftp
)
:
\/\/[^\s/
$.
?
#
]
.
[^\s]
*$/i
.
test
(
url
);
const
isValidUrl
=
/^
(
https
?
|ftp
)
:
\/\/[^\s/
$.#
?
]
.
[^\s]
*$/i
.
test
(
url
);
const
target
=
isValidUrl
?
'
target="_blank" rel="noopener noreferrer"
'
:
''
;
const
target
=
isValidUrl
?
'
target="_blank" rel="noopener noreferrer"
'
:
''
;
return
`<a href="
${
url
}
"
${
target
}
>
${
text
}
</a>`
;
return
`<a href="
${
url
}
"
${
target
}
>
${
text
}
</a>`
;
});
});
...
@@ -168,9 +172,6 @@ export const parseMarkdown = (text: string): string => {
...
@@ -168,9 +172,6 @@ export const parseMarkdown = (text: string): string => {
text
=
text
.
replace
(
/^
[
-*+
]\s
+
(
.*
)
$/gim
,
'
<li>$1</li>
'
);
text
=
text
.
replace
(
/^
[
-*+
]\s
+
(
.*
)
$/gim
,
'
<li>$1</li>
'
);
text
=
text
.
replace
(
/
(
<li>.*<
\/
li>
)
/gim
,
'
<ul>$1</ul>
'
);
text
=
text
.
replace
(
/
(
<li>.*<
\/
li>
)
/gim
,
'
<ul>$1</ul>
'
);
// 处理引用块
text
=
text
.
replace
(
/^>
\s
+
(
.*
)
$/gim
,
'
<blockquote>$1</blockquote>
'
);
// 处理水平分割线
// 处理水平分割线
text
=
text
.
replace
(
/^
\s
*---
\s
*$/gim
,
'
<hr />
'
);
text
=
text
.
replace
(
/^
\s
*---
\s
*$/gim
,
'
<hr />
'
);
text
=
text
.
replace
(
/^
\s
*
\*\*\*\s
*$/gim
,
'
<hr />
'
);
text
=
text
.
replace
(
/^
\s
*
\*\*\*\s
*$/gim
,
'
<hr />
'
);
...
@@ -210,11 +211,28 @@ export const markdownTemplate = (content: any, isStreaming: boolean = false): st
...
@@ -210,11 +211,28 @@ export const markdownTemplate = (content: any, isStreaming: boolean = false): st
// 根据是否流式处理选择不同的解析方式
// 根据是否流式处理选择不同的解析方式
const
htmlContent
=
isStreaming
?
processStreamingMarkdown
(
content
)
:
parseMarkdown
(
content
);
const
htmlContent
=
isStreaming
?
processStreamingMarkdown
(
content
)
:
parseMarkdown
(
content
);
// 清理HTML内容:移除br标签和空p段落
// 清理HTML内容:移除
不必要的
br标签和空p段落
const
cleanHtml
=
htmlContent
const
cleanHtml
=
htmlContent
.
trim
()
.
trim
()
// 移除所有<br>标签
// 保留blockquote和code块内的换行符,移除其他不必要的br标签
.
replace
(
/<br
\s
*
\/?
>/gi
,
''
)
.
replace
(
/<br
\s
*
\/?
>/gi
,
(
match
,
offset
,
originalString
)
=>
{
// 检查当前br标签是否在blockquote或code/pre标签内
const
beforeContent
=
originalString
.
substring
(
0
,
offset
);
const
afterContent
=
originalString
.
substring
(
offset
+
match
.
length
);
// 检查前后是否有未闭合的blockquote标签
const
openBlockquotesBefore
=
(
beforeContent
.
match
(
/<blockquote/g
)
||
[]).
length
;
const
closeBlockquotesBefore
=
(
beforeContent
.
match
(
/<
\/
blockquote>/g
)
||
[]).
length
;
const
openBlockquotesAfter
=
(
afterContent
.
match
(
/<blockquote/g
)
||
[]).
length
;
const
closeBlockquotesAfter
=
(
afterContent
.
match
(
/<
\/
blockquote>/g
)
||
[]).
length
;
// 检查是否在code/pre标签内
const
isInCodeBlock
=
beforeContent
.
includes
(
'
<pre>
'
)
||
beforeContent
.
includes
(
'
<code>
'
);
const
isInBlockquote
=
(
openBlockquotesBefore
-
closeBlockquotesBefore
)
>
0
;
return
(
isInBlockquote
||
isInCodeBlock
)
?
match
:
''
;
})
// 移除空的<p>段落(只包含空格或换行符)
// 移除空的<p>段落(只包含空格或换行符)
.
replace
(
/<p
[^
>
]
*>
\s
*<
\/
p>/gi
,
''
)
.
replace
(
/<p
[^
>
]
*>
\s
*<
\/
p>/gi
,
''
)
// 移除只包含空格的<p>段落
// 移除只包含空格的<p>段落
...
...
src/views/components/utils/sseService.ts
View file @
54a022a9
...
@@ -51,7 +51,7 @@ export class SSEService {
...
@@ -51,7 +51,7 @@ export class SSEService {
'
x-app-code
'
:
this
.
config
.
appCode
||
''
,
'
x-app-code
'
:
this
.
config
.
appCode
||
''
,
},
},
withCredentials
:
true
,
withCredentials
:
true
,
connectionTimeout
:
3
0000
,
connectionTimeout
:
6
0000
,
});
});
this
.
eventSource
.
onopen
=
(
event
)
=>
{
this
.
eventSource
.
onopen
=
(
event
)
=>
{
...
...
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment