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
1dd66d7e
Commit
1dd66d7e
authored
Nov 27, 2025
by
水玉婷
Browse files
feat:语音时长调试
parent
6d548eb4
Changes
3
Hide whitespace changes
Inline
Side-by-side
src/views/components/AiChat.vue
View file @
1dd66d7e
...
@@ -6,7 +6,7 @@
...
@@ -6,7 +6,7 @@
<img
:src=
"props.logoUrl || defaultAvatar"
alt=
"avatar"
class=
"avatar-image"
/>
<img
:src=
"props.logoUrl || defaultAvatar"
alt=
"avatar"
class=
"avatar-image"
/>
</div>
</div>
<div
class=
"header-info"
>
<div
class=
"header-info"
>
<h2>
{{
props
.
dialogSessionId
?
props
?.
detailData
?.
title
||
'
继续对话
'
:
'
新建对话
'
}}
</h2>
<h2>
{{
props
.
dialogSessionId
?
props
?.
detailData
?.
title
||
'
继续对话
'
:
'
新建对话
'
}}
</h2>
</div>
</div>
</div>
</div>
...
@@ -48,7 +48,7 @@
...
@@ -48,7 +48,7 @@
<div
v-if=
"item.hasThinkBox"
class=
"think-box-wrapper"
>
<div
v-if=
"item.hasThinkBox"
class=
"think-box-wrapper"
>
<div
class=
"think-box-toggle"
@
click=
"toggleThinkBox(index, i)"
>
{{
<div
class=
"think-box-toggle"
@
click=
"toggleThinkBox(index, i)"
>
{{
item
.
thinkBoxExpanded
?
'
▲ 收起思考过程
'
:
'
▼ 展开思考过程
'
item
.
thinkBoxExpanded
?
'
▲ 收起思考过程
'
:
'
▼ 展开思考过程
'
}}
</div>
}}
</div>
<div
v-if=
"item.thinkBoxExpanded"
class=
"think-box-content"
<div
v-if=
"item.thinkBoxExpanded"
class=
"think-box-content"
v-html=
"contentTemplates.thinking(item.thinkContent || '')"
></div>
v-html=
"contentTemplates.thinking(item.thinkContent || '')"
></div>
</div>
</div>
...
@@ -87,7 +87,7 @@
...
@@ -87,7 +87,7 @@
import
{
EventSourcePolyfill
}
from
'
event-source-polyfill
'
;
import
{
EventSourcePolyfill
}
from
'
event-source-polyfill
'
;
import
{
ref
,
onMounted
,
onBeforeUnmount
,
nextTick
,
watch
}
from
'
vue
'
;
import
{
ref
,
onMounted
,
onBeforeUnmount
,
nextTick
,
watch
}
from
'
vue
'
;
import
dayjs
from
'
dayjs
'
;
import
dayjs
from
'
dayjs
'
;
import
{
post
,
get
}
from
'
@/utils/axios.js
'
;
// 导入axios的post方法
import
{
post
,
get
}
from
'
@/utils/axios.js
'
;
// 导入axios的post方法
import
{
tableTemplate
}
from
'
./tableTemplate
'
;
import
{
tableTemplate
}
from
'
./tableTemplate
'
;
import
{
SendOutlined
,
UserOutlined
}
from
'
@ant-design/icons-vue
'
;
import
{
SendOutlined
,
UserOutlined
}
from
'
@ant-design/icons-vue
'
;
import
defaultAvatar
from
'
@/assets/logo.png
'
;
import
defaultAvatar
from
'
@/assets/logo.png
'
;
...
@@ -119,6 +119,11 @@ const props = defineProps({
...
@@ -119,6 +119,11 @@ const props = defineProps({
type
:
String
,
type
:
String
,
default
:
''
default
:
''
},
},
// 对话详情数据
detailData
:
{
type
:
Object
,
default
:
()
=>
({})
},
onMessageSend
:
{
onMessageSend
:
{
type
:
Function
,
type
:
Function
,
default
:
undefined
default
:
undefined
...
@@ -218,9 +223,9 @@ const contentTemplates = {
...
@@ -218,9 +223,9 @@ const contentTemplates = {
></iframe>
></iframe>
</div>`
;
</div>`
;
},
},
// 音频消息模板
// 音频消息模板
- 简化版本,移除audio-icon
audio
:
(
audioData
:
any
)
=>
{
audio
:
(
audioData
:
any
)
=>
{
const
{
audioUrl
,
audioBlob
}
=
audioData
;
const
{
audioUrl
,
audioBlob
,
durationTime
}
=
audioData
;
let
src
=
audioUrl
;
let
src
=
audioUrl
;
// 如果提供了Blob对象,创建对象URL
// 如果提供了Blob对象,创建对象URL
...
@@ -233,10 +238,6 @@ const contentTemplates = {
...
@@ -233,10 +238,6 @@ const contentTemplates = {
return
`<div class="audio-message" data-audio-id="
${
audioId
}
">
return
`<div class="audio-message" data-audio-id="
${
audioId
}
">
<div class="audio-player" data-audio-src="
${
src
}
">
<div class="audio-player" data-audio-src="
${
src
}
">
<div class="audio-icon">
<span class="play-icon">▶</span>
<span class="pause-icon" style="display: none;">❚❚</span>
</div>
<div class="audio-wave">
<div class="audio-wave">
<div class="wave-bar"></div>
<div class="wave-bar"></div>
<div class="wave-bar"></div>
<div class="wave-bar"></div>
...
@@ -244,14 +245,14 @@ const contentTemplates = {
...
@@ -244,14 +245,14 @@ const contentTemplates = {
<div class="wave-bar"></div>
<div class="wave-bar"></div>
<div class="wave-bar"></div>
<div class="wave-bar"></div>
</div>
</div>
<div class="audio-duration">
0:00
</div>
<div class="audio-duration">
${
durationTime
+
'
"
'
||
'
0"
'
}
</div>
</div>
</div>
<audio id="
${
audioId
}
" src="
${
src
}
" preload="metadata" style="display: none;"></audio>
<audio id="
${
audioId
}
" src="
${
src
}
" preload="metadata" style="display: none;"></audio>
</div>`
;
</div>`
;
},
},
// Markdown模板
// Markdown模板
markdown
:
(
content
:
any
)
=>
{
markdown
:
(
content
:
any
)
=>
{
// 类型检查和默认值处理
// 类型检查和默认值处理
if
(
typeof
content
!==
'
string
'
)
{
if
(
typeof
content
!==
'
string
'
)
{
// 如果是对象,尝试转换为字符串
// 如果是对象,尝试转换为字符串
if
(
content
&&
typeof
content
===
'
object
'
)
{
if
(
content
&&
typeof
content
===
'
object
'
)
{
...
@@ -261,51 +262,51 @@ const contentTemplates = {
...
@@ -261,51 +262,51 @@ const contentTemplates = {
content
=
String
(
content
||
''
);
content
=
String
(
content
||
''
);
}
}
}
}
// 简单的Markdown解析器
// 简单的Markdown解析器
const
parseMarkdown
=
(
text
:
string
)
=>
{
const
parseMarkdown
=
(
text
:
string
)
=>
{
// 确保text是字符串
// 确保text是字符串
if
(
typeof
text
!==
'
string
'
)
{
if
(
typeof
text
!==
'
string
'
)
{
text
=
String
(
text
||
''
);
text
=
String
(
text
||
''
);
}
}
// 处理标题
// 处理标题
text
=
text
.
replace
(
/^###
(
.*$
)
/gim
,
'
<h3>$1</h3>
'
);
text
=
text
.
replace
(
/^###
(
.*$
)
/gim
,
'
<h3>$1</h3>
'
);
text
=
text
.
replace
(
/^##
(
.*$
)
/gim
,
'
<h2>$1</h2>
'
);
text
=
text
.
replace
(
/^##
(
.*$
)
/gim
,
'
<h2>$1</h2>
'
);
text
=
text
.
replace
(
/^#
(
.*$
)
/gim
,
'
<h1>$1</h1>
'
);
text
=
text
.
replace
(
/^#
(
.*$
)
/gim
,
'
<h1>$1</h1>
'
);
// 处理粗体
// 处理粗体
text
=
text
.
replace
(
/
\*\*(
.*
?)\*\*
/gim
,
'
<strong>$1</strong>
'
);
text
=
text
.
replace
(
/
\*\*(
.*
?)\*\*
/gim
,
'
<strong>$1</strong>
'
);
text
=
text
.
replace
(
/
\*(
.*
?)\*
/gim
,
'
<em>$1</em>
'
);
text
=
text
.
replace
(
/
\*(
.*
?)\*
/gim
,
'
<em>$1</em>
'
);
// 处理代码块
// 处理代码块
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
,
'
<code>$1</code>
'
);
text
=
text
.
replace
(
/`
(
.*
?)
`/gim
,
'
<code>$1</code>
'
);
// 处理链接
// 处理链接
text
=
text
.
replace
(
/
\[([^\[]
+
)\]\(([^\)]
+
)\)
/gim
,
'
<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>
'
);
text
=
text
.
replace
(
/
\[([^\[]
+
)\]\(([^\)]
+
)\)
/gim
,
'
<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>
'
);
// 处理图片
// 处理图片
text
=
text
.
replace
(
/!
\[([^\[]
+
)\]\(([^\)]
+
)\)
/gim
,
'
<img src="$2" alt="$1" style="max-width: 100%; height: auto;" />
'
);
text
=
text
.
replace
(
/!
\[([^\[]
+
)\]\(([^\)]
+
)\)
/gim
,
'
<img src="$2" alt="$1" style="max-width: 100%; height: auto;" />
'
);
// 处理列表
// 处理列表
text
=
text
.
replace
(
/^
\s
*-
\s(
.*$
)
/gim
,
'
<li>$1</li>
'
);
text
=
text
.
replace
(
/^
\s
*-
\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
(
/
\n
/gim
,
'
<br>
'
);
text
=
text
.
replace
(
/
\n
/gim
,
'
<br>
'
);
// 处理段落
// 处理段落
text
=
text
.
replace
(
/<br><br>/gim
,
'
</p><p>
'
);
text
=
text
.
replace
(
/<br><br>/gim
,
'
</p><p>
'
);
text
=
'
<p>
'
+
text
+
'
</p>
'
;
text
=
'
<p>
'
+
text
+
'
</p>
'
;
text
=
text
.
replace
(
/<p><
(
h
[
1-6
]
|ul|pre|img
)
/gim
,
'
</p><$1
'
);
text
=
text
.
replace
(
/<p><
(
h
[
1-6
]
|ul|pre|img
)
/gim
,
'
</p><$1
'
);
text
=
text
.
replace
(
/
(
<
\/(
h
[
1-6
]
|ul|pre|img
)
>
)
<p>/gim
,
'
$1</p><p>
'
);
text
=
text
.
replace
(
/
(
<
\/(
h
[
1-6
]
|ul|pre|img
)
>
)
<p>/gim
,
'
$1</p><p>
'
);
return
text
;
return
text
;
};
};
const
htmlContent
=
parseMarkdown
(
content
);
const
htmlContent
=
parseMarkdown
(
content
);
// 清理HTML内容:移除br标签和空p段落
// 清理HTML内容:移除br标签和空p段落
const
cleanHtml
=
htmlContent
const
cleanHtml
=
htmlContent
.
trim
()
.
trim
()
...
@@ -321,12 +322,12 @@ const contentTemplates = {
...
@@ -321,12 +322,12 @@ const contentTemplates = {
.
replace
(
/^
\s
*<p
[^
>
]
*>
\s
*<
\/
p>/gi
,
''
)
.
replace
(
/^
\s
*<p
[^
>
]
*>
\s
*<
\/
p>/gi
,
''
)
.
replace
(
/<p
[^
>
]
*>
\s
*<
\/
p>
\s
*$/gi
,
''
)
.
replace
(
/<p
[^
>
]
*>
\s
*<
\/
p>
\s
*$/gi
,
''
)
.
trim
();
.
trim
();
// 检查内容是否为空或只有空白
// 检查内容是否为空或只有空白
if
(
!
cleanHtml
||
cleanHtml
===
'
<p></p>
'
||
cleanHtml
===
'
<p> </p>
'
)
{
if
(
!
cleanHtml
||
cleanHtml
===
'
<p></p>
'
||
cleanHtml
===
'
<p> </p>
'
)
{
return
''
;
// 如果内容为空,返回空字符串不展示
return
''
;
// 如果内容为空,返回空字符串不展示
}
}
return
`<div class="message-markdown">
return
`<div class="message-markdown">
<div class="markdown-content">
${
cleanHtml
}
</div>
<div class="markdown-content">
${
cleanHtml
}
</div>
</div>`
;
</div>`
;
...
@@ -381,7 +382,7 @@ const timeArr = ref([]);
...
@@ -381,7 +382,7 @@ const timeArr = ref([]);
const
hasStartedConversation
=
ref
(
false
);
// 添加对话开始状态
const
hasStartedConversation
=
ref
(
false
);
// 添加对话开始状态
// 语音事件处理函数 - 修改为接收服务器返回的URL
// 语音事件处理函数 - 修改为接收服务器返回的URL
const
handleVoiceAudio
=
(
audioUrl
:
string
,
audioBlob
?:
Blob
)
=>
{
const
handleVoiceAudio
=
(
audioUrl
:
string
,
audioBlob
?:
Blob
,
durationTime
?:
number
)
=>
{
console
.
log
(
'
收到音频URL:
'
,
audioUrl
);
console
.
log
(
'
收到音频URL:
'
,
audioUrl
);
// 开始对话
// 开始对话
startConversation
();
startConversation
();
...
@@ -397,7 +398,7 @@ const handleVoiceAudio = (audioUrl: string, audioBlob?: Blob) => {
...
@@ -397,7 +398,7 @@ const handleVoiceAudio = (audioUrl: string, audioBlob?: Blob) => {
date
:
dayjs
().
format
(
'
HH:mm
'
),
date
:
dayjs
().
format
(
'
HH:mm
'
),
contentBlocks
:
[
contentBlocks
:
[
{
{
content
:
contentTemplates
.
audio
({
audioUrl
,
audioBlob
}),
content
:
contentTemplates
.
audio
({
audioUrl
,
audioBlob
,
durationTime
}),
thinkContent
:
''
,
thinkContent
:
''
,
hasThinkBox
:
false
,
hasThinkBox
:
false
,
thinkBoxExpanded
:
false
,
thinkBoxExpanded
:
false
,
...
@@ -407,7 +408,7 @@ const handleVoiceAudio = (audioUrl: string, audioBlob?: Blob) => {
...
@@ -407,7 +408,7 @@ const handleVoiceAudio = (audioUrl: string, audioBlob?: Blob) => {
// 如果有音频Blob,直接发送到服务器
// 如果有音频Blob,直接发送到服务器
if
(
audioUrl
)
{
if
(
audioUrl
)
{
sendAudioMessage
(
audioUrl
);
sendAudioMessage
(
audioUrl
,
durationTime
);
}
}
// 滚动到底部
// 滚动到底部
...
@@ -422,13 +423,15 @@ const handleVoiceError = (error: string) => {
...
@@ -422,13 +423,15 @@ const handleVoiceError = (error: string) => {
};
};
// 发送音频消息 - 简化逻辑,与sendMessage保持一致
// 发送音频消息 - 简化逻辑,与sendMessage保持一致
const
sendAudioMessage
=
async
(
audioUrl
:
string
)
=>
{
const
sendAudioMessage
=
async
(
audioUrl
:
string
,
durationTime
?:
number
)
=>
{
loading
.
value
=
true
;
loading
.
value
=
true
;
try
{
try
{
// 开始对话
// 开始对话
startConversation
();
startConversation
();
isAIResponding
.
value
=
false
;
isInThinkingMode
.
value
=
false
;
currentAIResponse
.
value
=
null
;
// 调用外部传入的消息发送函数
// 调用外部传入的消息发送函数
if
(
props
.
onMessageSend
)
{
if
(
props
.
onMessageSend
)
{
console
.
log
(
'
调用外部音频发送函数
'
);
console
.
log
(
'
调用外部音频发送函数
'
);
...
@@ -437,6 +440,7 @@ const sendAudioMessage = async (audioUrl: string) => {
...
@@ -437,6 +440,7 @@ const sendAudioMessage = async (audioUrl: string) => {
// 默认的API调用逻辑 - 使用与sendMessage相同的逻辑,只是参数不同
// 默认的API调用逻辑 - 使用与sendMessage相同的逻辑,只是参数不同
const
response
=
await
post
(
`
${
props
.
apiBaseUrl
}
/aiService/ask/app/
${
props
.
params
?.
appId
}
`
,
{
const
response
=
await
post
(
`
${
props
.
apiBaseUrl
}
/aiService/ask/app/
${
props
.
params
?.
appId
}
`
,
{
questionLocalAudioFilePath
:
audioUrl
,
questionLocalAudioFilePath
:
audioUrl
,
audioDuration
:
durationTime
,
...
props
.
params
,
...
props
.
params
,
},
{
},
{
headers
:
{
headers
:
{
...
@@ -583,13 +587,13 @@ const processSSEMessage = (
...
@@ -583,13 +587,13 @@ const processSSEMessage = (
const
mergeMarkdownContent
=
(
existingContent
:
string
,
newContent
:
string
)
=>
{
const
mergeMarkdownContent
=
(
existingContent
:
string
,
newContent
:
string
)
=>
{
// 从现有的markdown内容中提取内部内容
// 从现有的markdown内容中提取内部内容
const
existingInnerContent
=
existingContent
.
replace
(
/<div class="message-markdown">
\s
*<div class="markdown-content">
([\s\S]
*
?)
<
\/
div>
\s
*<
\/
div>/
,
'
$1
'
);
const
existingInnerContent
=
existingContent
.
replace
(
/<div class="message-markdown">
\s
*<div class="markdown-content">
([\s\S]
*
?)
<
\/
div>
\s
*<
\/
div>/
,
'
$1
'
);
// 从新的markdown内容中提取内部内容
// 从新的markdown内容中提取内部内容
const
newInnerContent
=
newContent
.
replace
(
/<div class="message-markdown">
\s
*<div class="markdown-content">
([\s\S]
*
?)
<
\/
div>
\s
*<
\/
div>/
,
'
$1
'
);
const
newInnerContent
=
newContent
.
replace
(
/<div class="message-markdown">
\s
*<div class="markdown-content">
([\s\S]
*
?)
<
\/
div>
\s
*<
\/
div>/
,
'
$1
'
);
// 合并内容并重新包裹
// 合并内容并重新包裹
const
mergedInnerContent
=
existingInnerContent
+
newInnerContent
;
const
mergedInnerContent
=
existingInnerContent
+
newInnerContent
;
return
`<div class="message-markdown">
return
`<div class="message-markdown">
<div class="markdown-content">
${
mergedInnerContent
}
</div>
<div class="markdown-content">
${
mergedInnerContent
}
</div>
</div>`
;
</div>`
;
...
@@ -599,7 +603,7 @@ const processSSEMessage = (
...
@@ -599,7 +603,7 @@ const processSSEMessage = (
case
-
1
:
// 错误信息
case
-
1
:
// 错误信息
if
(
updatedResponse
)
{
if
(
updatedResponse
)
{
updatedResponse
.
contentBlocks
.
push
({
updatedResponse
.
contentBlocks
.
push
({
content
:
contentTemplates
.
error
(
messageContent
||
''
),
content
:
contentTemplates
.
error
(
messageContent
||
'
出错了~~
'
),
hasThinkBox
:
false
,
hasThinkBox
:
false
,
thinkContent
:
''
,
thinkContent
:
''
,
thinkBoxExpanded
:
false
,
thinkBoxExpanded
:
false
,
...
@@ -652,32 +656,32 @@ const processSSEMessage = (
...
@@ -652,32 +656,32 @@ const processSSEMessage = (
}
}
}
}
break
;
break
;
case
4
:
// MD格式
case
4
:
// MD格式
if
(
updatedResponse
)
{
if
(
updatedResponse
)
{
const
markdownContent
=
contentTemplates
.
markdown
(
messageContent
||
''
);
const
markdownContent
=
contentTemplates
.
markdown
(
messageContent
||
''
);
// 检查最后一个块是否是markdown块
// 检查最后一个块是否是markdown块
if
(
isLastBlockMarkdown
())
{
if
(
isLastBlockMarkdown
())
{
// 合并到现有的markdown块
// 合并到现有的markdown块
const
lastMarkdownIndex
=
getLastMarkdownBlockIndex
();
const
lastMarkdownIndex
=
getLastMarkdownBlockIndex
();
if
(
lastMarkdownIndex
!==
-
1
)
{
if
(
lastMarkdownIndex
!==
-
1
)
{
updatedResponse
.
contentBlocks
[
lastMarkdownIndex
].
content
=
updatedResponse
.
contentBlocks
[
lastMarkdownIndex
].
content
=
mergeMarkdownContent
(
mergeMarkdownContent
(
updatedResponse
.
contentBlocks
[
lastMarkdownIndex
].
content
,
updatedResponse
.
contentBlocks
[
lastMarkdownIndex
].
content
,
markdownContent
markdownContent
);
);
}
}
else
{
// 创建新的markdown块
updatedResponse
.
contentBlocks
.
push
({
content
:
markdownContent
,
hasThinkBox
:
false
,
thinkContent
:
''
,
thinkBoxExpanded
:
false
,
});
}
}
}
else
{
// 创建新的markdown块
updatedResponse
.
contentBlocks
.
push
({
content
:
markdownContent
,
hasThinkBox
:
false
,
thinkContent
:
''
,
thinkBoxExpanded
:
false
,
});
}
}
break
;
}
break
;
default
:
// 默认处理
default
:
// 默认处理
updatedResponse
.
contentBlocks
.
push
({
updatedResponse
.
contentBlocks
.
push
({
content
:
contentTemplates
.
text
(
messageContent
||
''
),
content
:
contentTemplates
.
text
(
messageContent
||
''
),
...
@@ -962,91 +966,92 @@ onBeforeUnmount(() => {
...
@@ -962,91 +966,92 @@ onBeforeUnmount(() => {
isInThinkingMode
.
value
=
false
;
isInThinkingMode
.
value
=
false
;
});
});
// 处理历史记录数据
// 处理历史记录数据
const
processHistoryData
=
(
dataArray
:
any
[])
=>
{
const
processHistoryData
=
(
dataArray
:
any
[])
=>
{
const
result
:
Message
[]
=
[];
const
result
:
Message
[]
=
[];
dataArray
.
forEach
((
data
)
=>
{
dataArray
.
forEach
((
data
)
=>
{
let
date
=
dayjs
(
data
.
startTime
).
format
(
'
YYYY-MM-DD HH:mm:ss
'
);
let
date
=
dayjs
(
data
.
startTime
).
format
(
'
YYYY-MM-DD HH:mm:ss
'
);
// 处理问题消息
// 处理问题消息
if
(
data
.
question
||
data
.
audioPath
)
{
if
(
data
.
question
||
data
.
audioPath
)
{
let
questionContent
=
''
;
let
questionContent
=
''
;
// 检查是否为音频消息
// 检查是否为音频消息
if
(
data
.
audioPath
)
{
if
(
data
.
audioPath
)
{
// 处理音频消息
// 处理音频消息
questionContent
=
contentTemplates
.
audio
({
questionContent
=
contentTemplates
.
audio
({
audioUrl
:
data
.
audioPath
audioUrl
:
data
.
audioPath
,
durationTime
:
data
.
autioTime
||
'
0"
'
,
});
}
else
{
// 处理文本消息
questionContent
=
contentTemplates
.
text
(
data
.
question
);
}
result
.
push
({
messageType
:
'
sent
'
,
avatar
:
'
我
'
,
recordId
:
''
,
promptTokens
:
0
,
completionTokens
:
0
,
totalTokens
:
0
,
date
,
contentBlocks
:
[
{
content
:
questionContent
,
thinkContent
:
''
,
hasThinkBox
:
false
,
thinkBoxExpanded
:
false
,
},
],
});
});
}
else
{
// 处理文本消息
questionContent
=
contentTemplates
.
text
(
data
.
question
);
}
}
result
.
push
({
// 处理AI回答消息
messageType
:
'
sent
'
,
if
(
data
.
answerInfoList
&&
Array
.
isArray
(
data
.
answerInfoList
))
{
avatar
:
'
我
'
,
const
aiMessage
:
Message
=
{
recordId
:
''
,
messageType
:
'
received
'
,
promptTokens
:
0
,
avatar
:
'
AI
'
,
completionTokens
:
0
,
recordId
:
''
,
totalTokens
:
0
,
promptTokens
:
0
,
date
,
completionTokens
:
0
,
contentBlocks
:
[
totalTokens
:
0
,
{
contentBlocks
:
[],
content
:
questionContent
,
date
,
thinkContent
:
''
,
hasThinkBox
:
false
,
thinkBoxExpanded
:
false
,
},
],
});
}
// 处理AI回答消息
if
(
data
.
answerInfoList
&&
Array
.
isArray
(
data
.
answerInfoList
))
{
const
aiMessage
:
Message
=
{
messageType
:
'
received
'
,
avatar
:
'
AI
'
,
recordId
:
''
,
promptTokens
:
0
,
completionTokens
:
0
,
totalTokens
:
0
,
contentBlocks
:
[],
date
,
};
let
currentThinkingMode
=
false
;
let
currentBlockIdx
=
-
1
;
// 历史数据处理,isHistoryData设为true,思考框折叠
data
.
answerInfoList
.
forEach
((
answer
)
=>
{
const
sseData
:
SSEData
=
{
message
:
answer
.
message
||
''
,
status
:
answer
.
status
||
0
,
type
:
answer
.
type
||
''
,
};
};
const
processResult
=
processSSEMessage
(
let
currentThinkingMode
=
false
;
sseData
,
let
currentBlockIdx
=
-
1
;
aiMessage
,
currentThinkingMode
,
// 历史数据处理,isHistoryData设为true,思考框折叠
currentBlockIdx
,
data
.
answerInfoList
.
forEach
((
answer
)
=>
{
true
,
const
sseData
:
SSEData
=
{
);
message
:
answer
.
message
||
''
,
status
:
answer
.
status
||
0
,
currentThinkingMode
=
processResult
.
updatedIsThinking
;
type
:
answer
.
type
||
''
,
currentBlockIdx
=
processResult
.
updatedBlockIndex
;
};
aiMessage
.
recordId
=
processResult
.
recordId
;
aiMessage
.
promptTokens
=
processResult
.
promptTokens
;
const
processResult
=
processSSEMessage
(
aiMessage
.
completionTokens
=
processResult
.
completionTokens
;
sseData
,
aiMessage
.
totalTokens
=
processResult
.
totalTokens
;
aiMessage
,
});
currentThinkingMode
,
currentBlockIdx
,
true
,
);
if
(
aiMessage
.
contentBlocks
.
length
>
0
)
{
currentThinkingMode
=
processResult
.
updatedIsThinking
;
result
.
push
(
aiMessage
);
currentBlockIdx
=
processResult
.
updatedBlockIndex
;
aiMessage
.
recordId
=
processResult
.
recordId
;
aiMessage
.
promptTokens
=
processResult
.
promptTokens
;
aiMessage
.
completionTokens
=
processResult
.
completionTokens
;
aiMessage
.
totalTokens
=
processResult
.
totalTokens
;
});
if
(
aiMessage
.
contentBlocks
.
length
>
0
)
{
result
.
push
(
aiMessage
);
}
}
}
}
});
});
return
result
;
return
result
;
};
};
// 获取历史会话消息
// 获取历史会话消息
const
getChatRecord
=
async
(
dialogSessionId
:
string
)
=>
{
const
getChatRecord
=
async
(
dialogSessionId
:
string
)
=>
{
...
@@ -1173,15 +1178,9 @@ const setupAudioPlayers = () => {
...
@@ -1173,15 +1178,9 @@ const setupAudioPlayers = () => {
return
;
return
;
}
}
// 音频播放结束,重置
为总时长 - 已移除
// 音频播放结束,重置
状态
audioElement
.
addEventListener
(
'
ended
'
,
()
=>
{
audioElement
.
addEventListener
(
'
ended
'
,
()
=>
{
player
.
classList
.
remove
(
'
playing
'
);
player
.
classList
.
remove
(
'
playing
'
);
const
playIcon
=
player
.
querySelector
(
'
.play-icon
'
);
const
pauseIcon
=
player
.
querySelector
(
'
.pause-icon
'
);
if
(
playIcon
&&
pauseIcon
)
{
playIcon
.
style
.
display
=
'
inline
'
;
pauseIcon
.
style
.
display
=
'
none
'
;
}
});
});
// 设置播放/暂停事件
// 设置播放/暂停事件
...
@@ -1204,23 +1203,11 @@ const setupAudioPlayers = () => {
...
@@ -1204,23 +1203,11 @@ const setupAudioPlayers = () => {
// 音频播放事件
// 音频播放事件
audioElement
.
addEventListener
(
'
play
'
,
()
=>
{
audioElement
.
addEventListener
(
'
play
'
,
()
=>
{
player
.
classList
.
add
(
'
playing
'
);
player
.
classList
.
add
(
'
playing
'
);
const
playIcon
=
player
.
querySelector
(
'
.play-icon
'
);
const
pauseIcon
=
player
.
querySelector
(
'
.pause-icon
'
);
if
(
playIcon
&&
pauseIcon
)
{
playIcon
.
style
.
display
=
'
none
'
;
pauseIcon
.
style
.
display
=
'
inline
'
;
}
});
});
// 音频暂停事件
// 音频暂停事件
audioElement
.
addEventListener
(
'
pause
'
,
()
=>
{
audioElement
.
addEventListener
(
'
pause
'
,
()
=>
{
player
.
classList
.
remove
(
'
playing
'
);
player
.
classList
.
remove
(
'
playing
'
);
const
playIcon
=
player
.
querySelector
(
'
.play-icon
'
);
const
pauseIcon
=
player
.
querySelector
(
'
.pause-icon
'
);
if
(
playIcon
&&
pauseIcon
)
{
playIcon
.
style
.
display
=
'
inline
'
;
pauseIcon
.
style
.
display
=
'
none
'
;
}
});
});
});
});
};
};
...
@@ -1234,16 +1221,10 @@ const pauseAllOtherAudios = (currentAudio: HTMLAudioElement) => {
...
@@ -1234,16 +1221,10 @@ const pauseAllOtherAudios = (currentAudio: HTMLAudioElement) => {
const
player
=
audio
.
closest
(
'
.audio-message
'
)?.
querySelector
(
'
.audio-player
'
);
const
player
=
audio
.
closest
(
'
.audio-message
'
)?.
querySelector
(
'
.audio-player
'
);
if
(
player
)
{
if
(
player
)
{
player
.
classList
.
remove
(
'
playing
'
);
player
.
classList
.
remove
(
'
playing
'
);
const
playIcon
=
player
.
querySelector
(
'
.play-icon
'
);
const
pauseIcon
=
player
.
querySelector
(
'
.pause-icon
'
);
if
(
playIcon
&&
pauseIcon
)
{
playIcon
.
style
.
display
=
'
inline
'
;
pauseIcon
.
style
.
display
=
'
none
'
;
}
}
}
}
}
});
});
};
// 移除多余的括号
};
onBeforeUnmount
(()
=>
{
onBeforeUnmount
(()
=>
{
closeSSE
();
closeSSE
();
...
...
src/views/components/VoiceRecognition.vue
View file @
1dd66d7e
...
@@ -25,11 +25,6 @@
...
@@ -25,11 +25,6 @@
<span
class=
"pulse"
></span>
<span
class=
"pulse"
></span>
</span>
</span>
</button>
</button>
<!-- 语音识别状态提示 -->
<div
v-if=
"showStatus"
class=
"voice-status"
:class=
"statusClass"
>
{{
statusText
}}
</div>
</div>
</div>
</
template
>
</
template
>
...
@@ -59,19 +54,12 @@ const emit = defineEmits<{
...
@@ -59,19 +54,12 @@ const emit = defineEmits<{
// 响应式数据
// 响应式数据
const
isRecording
=
ref
(
false
)
const
isRecording
=
ref
(
false
)
const
showStatus
=
ref
(
false
)
const
statusText
=
ref
(
''
)
// MediaRecorder相关
// MediaRecorder相关
const
mediaRecorder
=
ref
<
MediaRecorder
|
null
>
(
null
)
const
mediaRecorder
=
ref
<
MediaRecorder
|
null
>
(
null
)
const
audioChunks
=
ref
<
Blob
[]
>
([])
const
audioChunks
=
ref
<
Blob
[]
>
([])
const
audioStream
=
ref
<
MediaStream
|
null
>
(
null
)
const
audioStream
=
ref
<
MediaStream
|
null
>
(
null
)
// 计算属性
const
statusClass
=
computed
(()
=>
{
return
isRecording
.
value
?
'
recording
'
:
'
idle
'
})
// 检查浏览器是否支持MediaRecorder
// 检查浏览器是否支持MediaRecorder
const
isMediaRecorderSupported
=
()
=>
{
const
isMediaRecorderSupported
=
()
=>
{
const
supported
=
'
MediaRecorder
'
in
window
;
const
supported
=
'
MediaRecorder
'
in
window
;
...
@@ -81,16 +69,6 @@ const isMediaRecorderSupported = () => {
...
@@ -81,16 +69,6 @@ const isMediaRecorderSupported = () => {
return
supported
;
return
supported
;
}
}
// 显示状态消息
const
showStatusMessage
=
(
message
:
string
)
=>
{
statusText
.
value
=
message
showStatus
.
value
=
true
setTimeout
(()
=>
{
showStatus
.
value
=
false
},
3000
)
}
// 检查麦克风权限
// 检查麦克风权限
const
checkMicrophonePermission
=
async
()
=>
{
const
checkMicrophonePermission
=
async
()
=>
{
try
{
try
{
...
@@ -101,7 +79,6 @@ const checkMicrophonePermission = async () => {
...
@@ -101,7 +79,6 @@ const checkMicrophonePermission = async () => {
}
}
if
(
permissionStatus
.
state
===
'
denied
'
)
{
if
(
permissionStatus
.
state
===
'
denied
'
)
{
showStatusMessage
(
'
麦克风权限被拒绝,请在浏览器设置中允许访问
'
);
return
false
;
return
false
;
}
}
...
@@ -114,25 +91,6 @@ const checkMicrophonePermission = async () => {
...
@@ -114,25 +91,6 @@ const checkMicrophonePermission = async () => {
}
}
}
}
// 显示权限引导提示
const
showPermissionGuide
=
()
=>
{
const
guideMessage
=
`
麦克风权限被拒绝,请按以下步骤操作:
1. 点击浏览器地址栏左侧的"锁形图标"或"不安全"标识
2. 选择"网站设置"
3. 找到"麦克风"权限,选择"允许"
4. 刷新页面后重试
`
;
showStatusMessage
(
'
麦克风权限被拒绝,请检查浏览器设置
'
);
// 在调试模式下显示详细引导
if
(
props
.
debug
)
{
console
.
warn
(
'
麦克风权限被拒绝,用户需要手动授权
'
);
console
.
log
(
guideMessage
);
}
}
// 开始录音
// 开始录音
const
startRecording
=
async
()
=>
{
const
startRecording
=
async
()
=>
{
if
(
props
.
disabled
||
isRecording
.
value
)
return
if
(
props
.
disabled
||
isRecording
.
value
)
return
...
@@ -140,14 +98,13 @@ const startRecording = async () => {
...
@@ -140,14 +98,13 @@ const startRecording = async () => {
// 检查权限
// 检查权限
const
hasPermission
=
await
checkMicrophonePermission
();
const
hasPermission
=
await
checkMicrophonePermission
();
if
(
!
hasPermission
)
{
if
(
!
hasPermission
)
{
showPermissionGuide
(
);
emit
(
'
error
'
,
'
麦克风权限被拒绝
'
);
return
;
return
;
}
}
// 检查浏览器支持
// 检查浏览器支持
if
(
!
isMediaRecorderSupported
())
{
if
(
!
isMediaRecorderSupported
())
{
const
errorMsg
=
'
您的浏览器不支持音频录制功能
'
;
const
errorMsg
=
'
您的浏览器不支持音频录制功能
'
;
showStatusMessage
(
errorMsg
);
emit
(
'
error
'
,
errorMsg
);
emit
(
'
error
'
,
errorMsg
);
return
;
return
;
}
}
...
@@ -185,7 +142,6 @@ const startRecording = async () => {
...
@@ -185,7 +142,6 @@ const startRecording = async () => {
// 开始录制
// 开始录制
mediaRecorder
.
value
.
start
(
100
);
// 每100ms收集一次数据
mediaRecorder
.
value
.
start
(
100
);
// 每100ms收集一次数据
isRecording
.
value
=
true
;
isRecording
.
value
=
true
;
showStatusMessage
(
'
正在录音...
'
);
// 通知父组件开始录音
// 通知父组件开始录音
emit
(
'
recordingStart
'
);
emit
(
'
recordingStart
'
);
...
@@ -200,12 +156,10 @@ const startRecording = async () => {
...
@@ -200,12 +156,10 @@ const startRecording = async () => {
if
(
error
&&
error
.
name
===
'
NotAllowedError
'
)
{
if
(
error
&&
error
.
name
===
'
NotAllowedError
'
)
{
errorMessage
=
'
麦克风权限被拒绝
'
;
errorMessage
=
'
麦克风权限被拒绝
'
;
showPermissionGuide
();
}
else
if
(
error
&&
error
.
name
===
'
NotFoundError
'
)
{
}
else
if
(
error
&&
error
.
name
===
'
NotFoundError
'
)
{
errorMessage
=
'
未找到麦克风设备
'
;
errorMessage
=
'
未找到麦克风设备
'
;
}
}
showStatusMessage
(
errorMessage
);
emit
(
'
error
'
,
errorMessage
);
emit
(
'
error
'
,
errorMessage
);
}
}
}
}
...
@@ -234,7 +188,7 @@ const stopRecording = () => {
...
@@ -234,7 +188,7 @@ const stopRecording = () => {
}
}
// 上传音频文件到服务器 - 修改为使用axios
// 上传音频文件到服务器 - 修改为使用axios
const
uploadAudioFile
=
async
(
audioBlob
:
Blob
):
Promise
<
string
>
=>
{
const
uploadAudioFile
=
async
(
audioBlob
:
Blob
):
Promise
<
{
filePath
:
string
,
durationTime
:
number
}
>
=>
{
try
{
try
{
const
formData
=
new
FormData
();
const
formData
=
new
FormData
();
formData
.
append
(
'
file
'
,
audioBlob
,
'
recording.wav
'
);
formData
.
append
(
'
file
'
,
audioBlob
,
'
recording.wav
'
);
...
@@ -250,7 +204,9 @@ const uploadAudioFile = async (audioBlob: Blob): Promise<string> => {
...
@@ -250,7 +204,9 @@ const uploadAudioFile = async (audioBlob: Blob): Promise<string> => {
if
(
result
.
data
.
code
===
0
)
{
if
(
result
.
data
.
code
===
0
)
{
const
filePath
=
result
.
data
.
data
.
filePath
;
const
filePath
=
result
.
data
.
data
.
filePath
;
return
filePath
;
// 计算音频时长(秒),四舍五入取整
const
durationTime
=
result
.
data
.
data
.
durationTime
;
return
{
filePath
,
durationTime
};
}
else
{
}
else
{
throw
new
Error
(
'
上传接口返回数据格式错误
'
);
throw
new
Error
(
'
上传接口返回数据格式错误
'
);
}
}
...
@@ -263,30 +219,27 @@ const uploadAudioFile = async (audioBlob: Blob): Promise<string> => {
...
@@ -263,30 +219,27 @@ const uploadAudioFile = async (audioBlob: Blob): Promise<string> => {
// 发送录制的音频
// 发送录制的音频
const
sendRecordedAudio
=
async
()
=>
{
const
sendRecordedAudio
=
async
()
=>
{
if
(
audioChunks
.
value
.
length
===
0
)
{
if
(
audioChunks
.
value
.
length
===
0
)
{
showStatusMessage
(
'
录音数据为空
'
);
emit
(
'
error
'
,
'
录音数据为空
'
);
return
;
return
;
}
}
const
audioBlob
=
new
Blob
(
audioChunks
.
value
,
{
type
:
'
audio/webm;codecs=opus
'
});
const
audioBlob
=
new
Blob
(
audioChunks
.
value
,
{
type
:
'
audio/webm;codecs=opus
'
});
try
{
try
{
showStatusMessage
(
'
正在上传音频...
'
);
// 先调用上传接口获取URL
// 先调用上传接口获取URL
const
audioUrl
=
await
uploadAudioFile
(
audioBlob
);
const
{
filePath
,
durationTime
}
=
await
uploadAudioFile
(
audioBlob
);
// 上传成功后触发audio事件,传递URL和Blob
// 上传成功后触发audio事件,传递URL和Blob
emit
(
'
audio
'
,
audioUrl
,
audioBlob
);
emit
(
'
audio
'
,
filePath
,
audioBlob
,
durationTime
);
showStatusMessage
(
'
音频已发送
'
);
if
(
props
.
debug
)
{
if
(
props
.
debug
)
{
console
.
log
(
'
音频上传成功,URL:
'
,
audioUrl
);
console
.
log
(
'
音频上传成功,URL:
'
,
filePath
);
console
.
log
(
'
音频发送完成,大小:
'
,
Math
.
round
(
audioBlob
.
size
/
1024
),
'
KB
'
);
console
.
log
(
'
音频发送完成,大小:
'
,
Math
.
round
(
audioBlob
.
size
/
1024
),
'
KB
'
);
console
.
log
(
'
音频时长:
'
,
durationTime
,
'
秒
'
);
}
}
}
catch
(
error
)
{
}
catch
(
error
)
{
console
.
error
(
'
音频上传失败:
'
,
error
);
console
.
error
(
'
音频上传失败:
'
,
error
);
const
errorMsg
=
'
音频上传失败,请重试
'
;
const
errorMsg
=
'
音频上传失败,请重试
'
;
showStatusMessage
(
errorMsg
);
emit
(
'
error
'
,
errorMsg
);
emit
(
'
error
'
,
errorMsg
);
}
finally
{
}
finally
{
// 清理录音数据
// 清理录音数据
...
@@ -390,25 +343,6 @@ defineExpose({
...
@@ -390,25 +343,6 @@ defineExpose({
}
}
}
}
.voice-status {
position: absolute;
top: -45px; /* 进一步增加距离,确保完全不会遮挡按钮 */
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.8);
color: @white;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
white-space: nowrap;
z-index: 1; /* 保持较低的z-index */
pointer-events: none; /* 禁止状态提示框接收点击事件 */
&.recording {
background: rgba(255, 0, 0, 0.8);
}
}
@keyframes pulse {
@keyframes pulse {
0% {
0% {
opacity: 1;
opacity: 1;
...
...
src/views/components/style.less
View file @
1dd66d7e
...
@@ -27,7 +27,7 @@
...
@@ -27,7 +27,7 @@
padding: 0;
padding: 0;
margin: 0;
margin: 0;
box-sizing: border-box;
box-sizing: border-box;
// Webkit浏览器滚动条样式(Chrome, Safari, Edge)
// Webkit浏览器滚动条样式(Chrome, Safari, Edge)
&::-webkit-scrollbar {
&::-webkit-scrollbar {
width: 0px; // 垂直滚动条宽度
width: 0px; // 垂直滚动条宽度
...
@@ -36,7 +36,16 @@
...
@@ -36,7 +36,16 @@
}
}
// 重置基础元素样式
// 重置基础元素样式
p, h1, h2, h3, h4, h5, h6, ul, ol, li {
p,
h1,
h2,
h3,
h4,
h5,
h6,
ul,
ol,
li {
margin: 0;
margin: 0;
padding: 0;
padding: 0;
}
}
...
@@ -54,7 +63,7 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li {
...
@@ -54,7 +63,7 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li {
display: flex;
display: flex;
flex-direction: column;
flex-direction: column;
height: 100vh;
height: 100vh;
// 居中介绍页面样式
// 居中介绍页面样式
.chat-intro-center {
.chat-intro-center {
flex: 1;
flex: 1;
...
@@ -62,33 +71,33 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li {
...
@@ -62,33 +71,33 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li {
align-items: center;
align-items: center;
justify-content: center;
justify-content: center;
padding: 40px 20px;
padding: 40px 20px;
.intro-content {
.intro-content {
text-align: center;
text-align: center;
max-width: 400px;
max-width: 400px;
width: 100%;
width: 100%;
.avatar-image {
.avatar-image {
width: 180px;
width: 180px;
height: 180px;
height: 180px;
border-radius: 50%;
border-radius: 50%;
margin-bottom: 20px;
margin-bottom: 20px;
}
}
h3 {
h3 {
font-size: 24px;
font-size: 24px;
color: @gray-7;
color: @gray-7;
margin-bottom: 12px;
margin-bottom: 12px;
font-weight: 600;
font-weight: 600;
}
}
p {
p {
font-size: 16px;
font-size: 16px;
color: @gray-6;
color: @gray-6;
line-height: 1.5;
line-height: 1.5;
margin-bottom: 30px;
margin-bottom: 30px;
}
}
.start-chat-btn {
.start-chat-btn {
background: linear-gradient(135deg, @primary-color 0%, @primary-light 100%);
background: linear-gradient(135deg, @primary-color 0%, @primary-light 100%);
color: @white;
color: @white;
...
@@ -100,24 +109,24 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li {
...
@@ -100,24 +109,24 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li {
cursor: pointer;
cursor: pointer;
transition: all 0.3s ease;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(91, 138, 254, 0.3);
box-shadow: 0 4px 15px rgba(91, 138, 254, 0.3);
&:hover {
&:hover {
transform: translateY(-2px);
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(91, 138, 254, 0.4);
box-shadow: 0 6px 20px rgba(91, 138, 254, 0.4);
}
}
&:active {
&:active {
transform: translateY(0);
transform: translateY(0);
}
}
}
}
}
}
}
}
// 聊天头部样式
// 聊天头部样式
.chat-header {
.chat-header {
display: flex;
display: flex;
align-items: center;
align-items: center;
.header-avatar {
.header-avatar {
width: 45px;
width: 45px;
height: 45px;
height: 45px;
...
@@ -129,28 +138,29 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li {
...
@@ -129,28 +138,29 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li {
font-size: 20px;
font-size: 20px;
margin-right: 15px;
margin-right: 15px;
border: 2px solid rgba(255, 255, 255, 0.3);
border: 2px solid rgba(255, 255, 255, 0.3);
img {
img {
width: 100%;
width: 100%;
height: auto;
height: auto;
}
}
}
}
.header-info {
.header-info {
flex: 1;
flex: 1;
h2 {
h2 {
font-size: 18px;
font-size: 18px;
}
}
}
}
}
}
// 消息区域
// 消息区域
.chat-messages {
.chat-messages {
flex: 1;
flex: 1;
overflow-y: auto;
overflow-y: auto;
padding: 12px;
padding: 12px;
}
}
// 输入容器
// 输入容器
.chat-input-container {
.chat-input-container {
padding: 20px;
padding: 20px;
...
@@ -175,19 +185,19 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li {
...
@@ -175,19 +185,19 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li {
margin-bottom: 20px;
margin-bottom: 20px;
flex-direction: column;
flex-direction: column;
align-items: baseline;
align-items: baseline;
&.sent {
&.sent {
align-items: flex-end;
align-items: flex-end;
.message-time {
.message-time {
text-align: right;
text-align: right;
}
}
.avatar-container {
.avatar-container {
flex-direction: row-reverse;
flex-direction: row-reverse;
justify-content: flex-end;
justify-content: flex-end;
}
}
}
}
}
}
.avatar-container {
.avatar-container {
...
@@ -209,7 +219,7 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li {
...
@@ -209,7 +219,7 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li {
color: @white;
color: @white;
border: 2px solid @white;
border: 2px solid @white;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
img {
img {
width: 100%;
width: 100%;
height: auto;
height: auto;
...
@@ -229,7 +239,7 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li {
...
@@ -229,7 +239,7 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li {
max-width: 100%;
max-width: 100%;
margin-top: 8px;
margin-top: 8px;
min-width: 150px;
min-width: 150px;
// 当包含图表、表格或iframe时,宽度为100%
// 当包含图表、表格或iframe时,宽度为100%
&:has(.message-table),
&:has(.message-table),
&:has(.message-chart),
&:has(.message-chart),
...
@@ -247,10 +257,10 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li {
...
@@ -247,10 +257,10 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li {
position: relative;
position: relative;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
white-space: pre-wrap;
white-space: pre-wrap;
.message-inner-box {
.message-inner-box {
font-size: 0;
font-size: 0;
:deep(.message-text) {
:deep(.message-text) {
font-size: 14px;
font-size: 14px;
}
}
...
@@ -313,10 +323,10 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li {
...
@@ -313,10 +323,10 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li {
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
transition: all 0.3s ease;
animation: fadeIn 0.3s ease-in-out;
animation: fadeIn 0.3s ease-in-out;
.think-content {
.think-content {
font-size: 0;
font-size: 0;
.think-line {
.think-line {
font-size: 13px;
font-size: 13px;
color: @gray-7;
color: @gray-7;
...
@@ -333,7 +343,7 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li {
...
@@ -333,7 +343,7 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li {
display: flex;
display: flex;
align-items: flex-end;
align-items: flex-end;
position: relative;
position: relative;
// 语音识别按钮容器 - 移动到右边
// 语音识别按钮容器 - 移动到右边
.voice-recognition-wrapper {
.voice-recognition-wrapper {
position: absolute;
position: absolute;
...
@@ -342,7 +352,7 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li {
...
@@ -342,7 +352,7 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li {
transform: translateY(-50%);
transform: translateY(-50%);
z-index: 10;
z-index: 10;
}
}
textarea {
textarea {
flex: 1;
flex: 1;
padding: 14px 110px 14px 16px; // 调整内边距:右侧为两个按钮留空间,左侧恢复正常
padding: 14px 110px 14px 16px; // 调整内边距:右侧为两个按钮留空间,左侧恢复正常
...
@@ -357,12 +367,12 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li {
...
@@ -357,12 +367,12 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li {
overflow: hidden;
overflow: hidden;
position: relative;
position: relative;
box-shadow: 0px 2px 8px 0px rgba(0, 0, 0, 0.12);
box-shadow: 0px 2px 8px 0px rgba(0, 0, 0, 0.12);
&:focus {
&:focus {
border-color: @primary-color;
border-color: @primary-color;
box-shadow: 0 0 0 2px rgba(91, 138, 254, 0.1);
box-shadow: 0 0 0 2px rgba(91, 138, 254, 0.1);
}
}
&:disabled {
&:disabled {
background-color: @gray-2;
background-color: @gray-2;
border-color: @gray-3;
border-color: @gray-3;
...
@@ -370,7 +380,7 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li {
...
@@ -370,7 +380,7 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li {
cursor: not-allowed;
cursor: not-allowed;
}
}
}
}
button {
button {
position: absolute;
position: absolute;
right: 12px;
right: 12px;
...
@@ -388,12 +398,12 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li {
...
@@ -388,12 +398,12 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li {
transition: color 0.2s, border-color 0.2s, background-color 0.2s;
transition: color 0.2s, border-color 0.2s, background-color 0.2s;
z-index: 10;
z-index: 10;
transform: translateY(-50%);
transform: translateY(-50%);
&:active {
&:active {
background-color: rgba(91, 138, 254, 0.1);
background-color: rgba(91, 138, 254, 0.1);
transform: translateY(-50%) scale(0.95);
transform: translateY(-50%) scale(0.95);
}
}
&:disabled {
&:disabled {
color: @gray-4;
color: @gray-4;
border-color: @gray-4;
border-color: @gray-4;
...
@@ -406,11 +416,11 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li {
...
@@ -406,11 +416,11 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li {
.operation-box {
.operation-box {
margin-top: 6px;
margin-top: 6px;
p {
p {
color: @gray-5;
color: @gray-5;
font-size: 12px;
font-size: 12px;
span {
span {
margin-right: 15px;
margin-right: 15px;
}
}
...
@@ -425,48 +435,49 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li {
...
@@ -425,48 +435,49 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li {
width: 100%;
width: 100%;
max-width: 100%;
max-width: 100%;
margin: 8px 0;
margin: 8px 0;
// 表格容器
// 表格容器
.table-container {
.table-container {
width: 100%;
width: 100%;
overflow-x: auto;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
-webkit-overflow-scrolling: touch;
border-radius: 8px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
// 滚动条样式
// 滚动条样式
&::-webkit-scrollbar {
&::-webkit-scrollbar {
height: 8px;
height: 8px;
}
}
&::-webkit-scrollbar-track {
&::-webkit-scrollbar-track {
background: #f1f1f1;
background: #f1f1f1;
border-radius: 4px;
border-radius: 4px;
}
}
&::-webkit-scrollbar-thumb {
&::-webkit-scrollbar-thumb {
background: #c1c1c1;
background: #c1c1c1;
border-radius: 4px;
border-radius: 4px;
&:hover {
&:hover {
background: #a8a8a8;
background: #a8a8a8;
}
}
}
}
}
}
.data-table {
.data-table {
width: auto;
width: auto;
min-width: 100%;
min-width: 100%;
border-collapse: collapse;
border-collapse: collapse;
background-color: @white;
background-color: @white;
table-layout: auto;
table-layout: auto;
// 列类型样式
// 列类型样式
.text-cell {
.text-cell {
text-align: left;
text-align: left;
padding-left: 12px;
padding-left: 12px;
padding-right: 8px;
padding-right: 8px;
}
}
.numeric-cell {
.numeric-cell {
text-align: right;
text-align: right;
padding-left: 8px;
padding-left: 8px;
...
@@ -474,13 +485,13 @@ background-color: @white;
...
@@ -474,13 +485,13 @@ background-color: @white;
font-family: 'Courier New', monospace;
font-family: 'Courier New', monospace;
font-weight: 500;
font-weight: 500;
}
}
.trend-cell {
.trend-cell {
text-align: center;
text-align: center;
padding-left: 8px;
padding-left: 8px;
padding-right: 8px;
padding-right: 8px;
}
}
th {
th {
background: linear-gradient(135deg, @primary-color 0%, @primary-hover 100%);
background: linear-gradient(135deg, @primary-color 0%, @primary-hover 100%);
color: @white;
color: @white;
...
@@ -493,12 +504,12 @@ background-color: @white;
...
@@ -493,12 +504,12 @@ background-color: @white;
overflow: hidden;
overflow: hidden;
text-overflow: ellipsis;
text-overflow: ellipsis;
min-width: 80px;
min-width: 80px;
&:last-child {
&:last-child {
border-right: none;
border-right: none;
}
}
}
}
td {
td {
padding: 10px 8px;
padding: 10px 8px;
font-size: 14px;
font-size: 14px;
...
@@ -507,47 +518,47 @@ background-color: @white;
...
@@ -507,47 +518,47 @@ background-color: @white;
height: 35px;
height: 35px;
white-space: nowrap;
white-space: nowrap;
overflow: hidden;
overflow: hidden;
text-overflow: ellipsis;
text-overflow: ellipsis;
vertical-align: middle;
vertical-align: middle;
min-width: 80px;
min-width: 80px;
}
}
// 奇偶行样式
// 奇偶行样式
tr:nth-child(odd) td {
tr:nth-child(odd) td {
background-color: @blue-light-2;
background-color: @blue-light-2;
}
}
tr:nth-child(even) td {
tr:nth-child(even) td {
background-color: @white;
background-color: @white;
}
}
tr:hover td {
tr:hover td {
background-color: @blue-light-1;
background-color: @blue-light-1;
}
}
tr:last-child td {
tr:last-child td {
border-bottom: none;
border-bottom: none;
}
}
}
}
// 趋势箭头样式
// 趋势箭头样式
.trend-up {
.trend-up {
color: @success-color;
color: @success-color;
font-weight: bold;
font-weight: bold;
font-size: 16px;
font-size: 16px;
}
}
.trend-down {
.trend-down {
color: @error-color;
color: @error-color;
font-weight: bold;
font-weight: bold;
font-size: 16px;
font-size: 16px;
}
}
.table-footer {
.table-footer {
margin-top: 12px;
margin-top: 12px;
font-size: 14px;
font-size: 14px;
span {
span {
color: #2eb0a1;
color: #2eb0a1;
font-weight: bold;
font-weight: bold;
}
}
...
@@ -562,14 +573,14 @@ text-overflow: ellipsis;
...
@@ -562,14 +573,14 @@ text-overflow: ellipsis;
width: 100%;
width: 100%;
max-width: 100%;
max-width: 100%;
margin: 8px 0;
margin: 8px 0;
border-radius: 8px;
border-radius: 8px;
background-color: @white;
background-color: @white;
border: 1px solid @blue-light-3;
border: 1px solid @blue-light-3;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
overflow: hidden;
overflow: hidden;
position: relative;
position: relative;
min-height: 300px;
min-height: 300px;
// iframe提示信息样式
// iframe提示信息样式
.iframe-tips {
.iframe-tips {
padding: 12px 16px;
padding: 12px 16px;
...
@@ -580,7 +591,7 @@ border-radius: 8px;
...
@@ -580,7 +591,7 @@ border-radius: 8px;
font-weight: 500;
font-weight: 500;
line-height: 1.4;
line-height: 1.4;
}
}
// iframe标题样式
// iframe标题样式
.iframe-title {
.iframe-title {
padding: 8px 16px;
padding: 8px 16px;
...
@@ -591,7 +602,7 @@ border-radius: 8px;
...
@@ -591,7 +602,7 @@ border-radius: 8px;
font-weight: 600;
font-weight: 600;
line-height: 1.4;
line-height: 1.4;
}
}
iframe {
iframe {
width: 100%;
width: 100%;
height: 100%;
height: 100%;
...
@@ -600,7 +611,7 @@ border-radius: 8px;
...
@@ -600,7 +611,7 @@ border-radius: 8px;
border-radius: 8px;
border-radius: 8px;
background-color: @gray-1;
background-color: @gray-1;
transition: height 0.5s ease-in-out, opacity 0.3s ease;
transition: height 0.5s ease-in-out, opacity 0.3s ease;
// 加载状态样式
// 加载状态样式
&[src=""] {
&[src=""] {
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
...
@@ -608,15 +619,15 @@ border-radius: 8px;
...
@@ -608,15 +619,15 @@ border-radius: 8px;
animation: loading 1.5s infinite;
animation: loading 1.5s infinite;
}
}
}
}
// 加载状态
// 加载状态
&.iframe-loading {
&.iframe-loading {
iframe {
iframe {
opacity: 0;
opacity: 0;
pointer-events: none;
pointer-events: none;
min-height: 400px;
min-height: 400px;
}
}
.iframe-loading {
.iframe-loading {
display: flex;
display: flex;
flex-direction: column;
flex-direction: column;
...
@@ -625,24 +636,24 @@ pointer-events: none;
...
@@ -625,24 +636,24 @@ pointer-events: none;
position: absolute;
position: absolute;
top: 0;
top: 0;
left: 0;
left: 0;
width: 100%;
width: 100%;
height: 100%;
height: 100%;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
z-index: 10;
z-index: 10;
}
}
}
}
// 加载完成状态
// 加载完成状态
&.iframe-loaded {
&.iframe-loaded {
iframe {
iframe {
opacity: 1;
opacity: 1;
}
}
.iframe-loading {
.iframe-loading {
display: none;
display: none;
}
}
}
}
// 加载动画
// 加载动画
.loading-spinner {
.loading-spinner {
width: 40px;
width: 40px;
...
@@ -653,21 +664,21 @@ width: 100%;
...
@@ -653,21 +664,21 @@ width: 100%;
animation: spin 1s linear infinite;
animation: spin 1s linear infinite;
margin-bottom: 16px;
margin-bottom: 16px;
}
}
.loading-text {
.loading-text {
font-size: 16px;
font-size: 16px;
color: @gray-6;
color: @gray-6;
margin-bottom: 12px;
margin-bottom: 12px;
font-weight: 500;
font-weight: 500;
}
}
.loading-progress {
.loading-progress {
width: 200px;
width: 200px;
height: 4px;
height: 4px;
background: @gray-3;
background: @gray-3;
border-radius: 2px;
border-radius: 2px;
overflow: hidden;
overflow: hidden;
.progress-bar {
.progress-bar {
height: 100%;
height: 100%;
background: linear-gradient(90deg, @primary-color, @primary-light);
background: linear-gradient(90deg, @primary-color, @primary-light);
...
@@ -685,6 +696,7 @@ width: 100%;
...
@@ -685,6 +696,7 @@ width: 100%;
0% {
0% {
background-position: 200% 0;
background-position: 200% 0;
}
}
100% {
100% {
background-position: -200% 0;
background-position: -200% 0;
}
}
...
@@ -694,6 +706,7 @@ width: 100%;
...
@@ -694,6 +706,7 @@ width: 100%;
0% {
0% {
transform: rotate(0deg);
transform: rotate(0deg);
}
}
100% {
100% {
transform: rotate(360deg);
transform: rotate(360deg);
}
}
...
@@ -703,12 +716,14 @@ width: 100%;
...
@@ -703,12 +716,14 @@ width: 100%;
0% {
0% {
transform: translateX(-100%);
transform: translateX(-100%);
}
}
50% {
50% {
transform: translateX(200%);
transform: translateX(200%);
}
}
100% {
100% {
transform: translateX(200%);
transform: translateX(200%);
}
}
}
}
// =============================================
// =============================================
...
@@ -719,20 +734,21 @@ width: 100%;
...
@@ -719,20 +734,21 @@ width: 100%;
:deep(.message-table) {
:deep(.message-table) {
.data-table {
.data-table {
font-size: 12px;
font-size: 12px;
th, td {
th,
td {
padding: 8px 4px;
padding: 8px 4px;
height: 30px;
height: 30px;
min-width: 60px;
min-width: 60px;
}
}
}
}
.table-title {
.table-title {
font-size: 14px;
font-size: 14px;
}
}
.table-summary {
.table-summary {
font-size: 12px;
font-size: 12px;
padding: 8px 10px;
padding: 8px 10px;
}
}
}
}
...
@@ -741,7 +757,9 @@ font-size: 12px;
...
@@ -741,7 +757,9 @@ font-size: 12px;
@media (max-width: 480px) {
@media (max-width: 480px) {
:deep(.message-table) {
:deep(.message-table) {
.data-table {
.data-table {
th, td {
th,
td {
min-width: 50px;
min-width: 50px;
height: 28px;
height: 28px;
}
}
...
@@ -769,7 +787,7 @@ font-size: 12px;
...
@@ -769,7 +787,7 @@ font-size: 12px;
border: 1px solid @blue-light-3;
border: 1px solid @blue-light-3;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
overflow: hidden;
overflow: hidden;
.options-tips {
.options-tips {
padding: 12px 16px;
padding: 12px 16px;
background-color: @blue-light-2;
background-color: @blue-light-2;
...
@@ -779,44 +797,44 @@ font-size: 12px;
...
@@ -779,44 +797,44 @@ font-size: 12px;
font-weight: 500;
font-weight: 500;
line-height: 1.4;
line-height: 1.4;
}
}
.options-list {
.options-list {
padding: 8px 0;
padding: 8px 0;
}
}
.option-item {
.option-item {
padding: 8px 16px;
padding: 8px 16px;
border-bottom: 1px solid @gray-2;
border-bottom: 1px solid @gray-2;
transition: background-color 0.2s ease;
transition: background-color 0.2s ease;
&:last-child {
&:last-child {
border-bottom: none;
border-bottom: none;
}
}
&:hover {
&:hover {
background-color: @blue-light-1;
background-color: @blue-light-1;
}
}
.option-content {
.option-content {
line-height: 1.4;
line-height: 1.4;
.option-number-title {
.option-number-title {
font-size: 14px;
font-size: 14px;
color: @gray-7;
color: @gray-7;
font-weight: 500;
font-weight: 500;
// 序号部分特殊样式
// 序号部分特殊样式
&::before {
&::before {
content: '';
content: '';
display: inline;
display: inline;
}
}
}
}
.option-number-title:first-letter {
.option-number-title:first-letter {
color: @primary-color;
color: @primary-color;
font-weight: 600;
font-weight: 600;
}
}
.option-url {
.option-url {
font-size: 12px;
font-size: 12px;
color: @gray-5;
color: @gray-5;
...
@@ -837,7 +855,7 @@ font-size: 12px;
...
@@ -837,7 +855,7 @@ font-size: 12px;
.option-number-title {
.option-number-title {
font-size: 14px;
font-size: 14px;
}
}
.option-url {
.option-url {
font-size: 11px;
font-size: 11px;
max-width: 100%;
max-width: 100%;
...
@@ -847,14 +865,15 @@ font-size: 12px;
...
@@ -847,14 +865,15 @@ font-size: 12px;
}
}
}
}
// 音频消息样式 -
白色主题,无背景色
// 音频消息样式 -
简化版本,移除audio-icon
:deep(.audio-message) {
:deep(.audio-message) {
display: inline-block;
display: inline-block;
width: -webkit-fill-available;
width: -webkit-fill-available;
audio {
audio {
display: none; // 隐藏原生音频控件
display: none; // 隐藏原生音频控件
}
}
.audio-player {
.audio-player {
display: flex;
display: flex;
align-items: center;
align-items: center;
...
@@ -862,56 +881,70 @@ font-size: 12px;
...
@@ -862,56 +881,70 @@ font-size: 12px;
transition: all 0.3s ease;
transition: all 0.3s ease;
user-select: none;
user-select: none;
box-sizing: border-box;
box-sizing: border-box;
padding: 4px 12px;
border-radius: 20px;
&.playing {
&.playing {
.audio-wave .wave-bar {
.audio-wave .wave-bar {
animation: waveAnimation 1.2s ease-in-out infinite;
animation: waveAnimation 1.2s ease-in-out infinite;
&:nth-child(1) { animation-delay: 0s; }
&:nth-child(1) {
&:nth-child(2) { animation-delay: 0.2s; }
animation-delay: 0s;
&:nth-child(3) { animation-delay: 0.4s; }
}
&:nth-child(4) { animation-delay: 0.6s; }
&:nth-child(5) { animation-delay: 0.8s; }
&:nth-child(2) {
}
animation-delay: 0.2s;
}
}
.audio-icon {
&:nth-child(3) {
width: 24px;
animation-delay: 0.4s;
height: 24px;
}
display: flex;
align-items: center;
&:nth-child(4) {
justify-content: center;
animation-delay: 0.6s;
margin-right: 8px;
}
color: #ffffff; // 白色图标
&:nth-child(5) {
.play-icon, .pause-icon {
animation-delay: 0.8s;
font-size: 12px;
}
font-weight: bold;
}
}
}
}
.audio-wave {
.audio-wave {
display: flex;
display: flex;
align-items: center;
align-items: center;
gap: 2px;
gap: 2px;
margin-right: 8px;
margin-right: 8px;
.wave-bar {
.wave-bar {
width: 2px;
width: 2px;
height: 12px;
height: 12px;
background: #ffffff; // 白色波形条
background: #ffffff; // 白色波形条
border-radius: 1px;
border-radius: 1px;
transition: all 0.3s ease;
transition: all 0.3s ease;
&:nth-child(1) { height: 4px; }
&:nth-child(1) {
&:nth-child(2) { height: 8px; }
height: 4px;
&:nth-child(3) { height: 12px; }
}
&:nth-child(4) { height: 8px; }
&:nth-child(5) { height: 4px; }
&:nth-child(2) {
height: 8px;
}
&:nth-child(3) {
height: 12px;
}
&:nth-child(4) {
height: 8px;
}
&:nth-child(5) {
height: 4px;
}
}
}
}
}
.audio-duration {
.audio-duration {
font-size: 12px;
font-size: 12px;
color: #ffffff; // 白色时长文字
color: #ffffff; // 白色时长文字
...
@@ -922,10 +955,13 @@ font-size: 12px;
...
@@ -922,10 +955,13 @@ font-size: 12px;
}
}
@keyframes waveAnimation {
@keyframes waveAnimation {
0%, 100% {
0%,
100% {
transform: scaleY(0.3);
transform: scaleY(0.3);
opacity: 0.5;
opacity: 0.5;
}
}
50% {
50% {
transform: scaleY(1);
transform: scaleY(1);
opacity: 1;
opacity: 1;
...
@@ -945,42 +981,47 @@ font-size: 12px;
...
@@ -945,42 +981,47 @@ font-size: 12px;
border: 1px solid @blue-light-3;
border: 1px solid @blue-light-3;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
overflow: hidden;
overflow: hidden;
.markdown-content {
.markdown-content {
padding: 12px 16px;
padding: 12px 16px;
font-size: 14px;
font-size: 14px;
line-height: 1.6;
line-height: 1.6;
color: @gray-7;
color: @gray-7;
background: none;
background: none;
// 标题样式
// 标题样式
h1, h2, h3, h4, h5, h6 {
h1,
h2,
h3,
h4,
h5,
h6 {
margin-bottom: 8px;
margin-bottom: 8px;
font-weight: 600;
font-weight: 600;
color: @gray-7;
color: @gray-7;
line-height: 1.3;
line-height: 1.3;
background: none;
background: none;
}
}
h1 {
h1 {
font-size: 20px;
font-size: 20px;
}
}
h2 {
h2 {
font-size: 18px;
font-size: 18px;
}
}
h3 {
h3 {
font-size: 16px;
font-size: 16px;
}
}
// 段落样式
// 段落样式
p {
p {
text-align: justify;
text-align: justify;
background: none;
background: none;
margin-bottom: 8px;
margin-bottom: 8px;
}
}
// 链接样式
// 链接样式
a {
a {
color: @primary-color;
color: @primary-color;
...
@@ -988,13 +1029,13 @@ font-size: 12px;
...
@@ -988,13 +1029,13 @@ font-size: 12px;
border-bottom: 1px solid transparent;
border-bottom: 1px solid transparent;
transition: all 0.2s ease;
transition: all 0.2s ease;
background: none;
background: none;
&:hover {
&:hover {
color: @primary-hover;
color: @primary-hover;
border-bottom-color: @primary-hover;
border-bottom-color: @primary-hover;
}
}
}
}
// 代码样式
// 代码样式
code {
code {
background-color: @gray-2;
background-color: @gray-2;
...
@@ -1004,7 +1045,7 @@ font-size: 12px;
...
@@ -1004,7 +1045,7 @@ font-size: 12px;
font-size: 13px;
font-size: 13px;
color: @error-color;
color: @error-color;
}
}
pre {
pre {
background-color: @gray-1;
background-color: @gray-1;
border: 1px solid @gray-3;
border: 1px solid @gray-3;
...
@@ -1012,7 +1053,7 @@ font-size: 12px;
...
@@ -1012,7 +1053,7 @@ font-size: 12px;
padding: 12px;
padding: 12px;
overflow-x: auto;
overflow-x: auto;
margin-bottom: 8px;
margin-bottom: 8px;
code {
code {
background: none;
background: none;
padding: 0;
padding: 0;
...
@@ -1021,32 +1062,33 @@ font-size: 12px;
...
@@ -1021,32 +1062,33 @@ font-size: 12px;
line-height: 1.4;
line-height: 1.4;
}
}
}
}
// 列表样式
// 列表样式
ul, ol {
ul,
ol {
padding-left: 24px;
padding-left: 24px;
margin-bottom: 8px;
margin-bottom: 8px;
}
}
ul {
ul {
list-style-type: disc;
list-style-type: disc;
}
}
ol {
ol {
list-style-type: decimal;
list-style-type: decimal;
}
}
// 粗体和斜体
// 粗体和斜体
strong {
strong {
font-weight: 600;
font-weight: 600;
color: @gray-7;
color: @gray-7;
}
}
em {
em {
font-style: italic;
font-style: italic;
color: @gray-6;
color: @gray-6;
}
}
// 图片样式
// 图片样式
img {
img {
max-width: 100%;
max-width: 100%;
...
@@ -1054,7 +1096,7 @@ font-size: 12px;
...
@@ -1054,7 +1096,7 @@ font-size: 12px;
border-radius: 4px;
border-radius: 4px;
margin-bottom: 8px;
margin-bottom: 8px;
}
}
// 引用块样式
// 引用块样式
blockquote {
blockquote {
border-left: 4px solid @primary-color;
border-left: 4px solid @primary-color;
...
@@ -1064,24 +1106,25 @@ font-size: 12px;
...
@@ -1064,24 +1106,25 @@ font-size: 12px;
color: @gray-6;
color: @gray-6;
background: none;
background: none;
}
}
// 表格样式(如果Markdown中包含表格)
// 表格样式(如果Markdown中包含表格)
table {
table {
width: 100%;
width: 100%;
border-collapse: collapse;
border-collapse: collapse;
margin-bottom: 8px;
margin-bottom: 8px;
th, td {
th,
td {
padding: 8px 12px;
padding: 8px 12px;
border: 1px solid @gray-3;
border: 1px solid @gray-3;
text-align: left;
text-align: left;
}
}
th {
th {
background-color: @blue-light-2;
background-color: @blue-light-2;
font-weight: 600;
font-weight: 600;
}
}
tr:nth-child(even) {
tr:nth-child(even) {
background-color: @gray-1;
background-color: @gray-1;
}
}
...
@@ -1095,22 +1138,22 @@ font-size: 12px;
...
@@ -1095,22 +1138,22 @@ font-size: 12px;
.markdown-content {
.markdown-content {
font-size: 13px;
font-size: 13px;
padding: 10px 12px;
padding: 10px 12px;
h1 {
h1 {
font-size: 18px;
font-size: 18px;
}
}
h2 {
h2 {
font-size: 16px;
font-size: 16px;
}
}
h3 {
h3 {
font-size: 15px;
font-size: 15px;
}
}
pre {
pre {
padding: 8px;
padding: 8px;
code {
code {
font-size: 12px;
font-size: 12px;
}
}
...
@@ -1124,7 +1167,7 @@ font-size: 12px;
...
@@ -1124,7 +1167,7 @@ font-size: 12px;
display: flex;
display: flex;
align-items: flex-end;
align-items: flex-end;
gap: 8px;
gap: 8px;
textarea {
textarea {
flex: 1;
flex: 1;
padding: 14px 110px 14px 16px; // 调整内边距:右侧为两个按钮留空间,左侧恢复正常
padding: 14px 110px 14px 16px; // 调整内边距:右侧为两个按钮留空间,左侧恢复正常
...
@@ -1139,12 +1182,12 @@ font-size: 12px;
...
@@ -1139,12 +1182,12 @@ font-size: 12px;
overflow: hidden;
overflow: hidden;
position: relative;
position: relative;
box-shadow: 0px 2px 8px 0px rgba(0, 0, 0, 0.12);
box-shadow: 0px 2px 8px 0px rgba(0, 0, 0, 0.12);
&:focus {
&:focus {
border-color: @primary-color;
border-color: @primary-color;
box-shadow: 0 0 0 2px rgba(91, 138, 254, 0.1);
box-shadow: 0 0 0 2px rgba(91, 138, 254, 0.1);
}
}
&:disabled {
&:disabled {
background-color: @gray-2;
background-color: @gray-2;
border-color: @gray-3;
border-color: @gray-3;
...
@@ -1152,7 +1195,7 @@ font-size: 12px;
...
@@ -1152,7 +1195,7 @@ font-size: 12px;
cursor: not-allowed;
cursor: not-allowed;
}
}
}
}
// 语音识别按钮样式
// 语音识别按钮样式
.voice-recognition {
.voice-recognition {
margin: 0;
margin: 0;
...
@@ -1161,21 +1204,25 @@ font-size: 12px;
...
@@ -1161,21 +1204,25 @@ font-size: 12px;
.operation-box {
.operation-box {
margin-top: 6px;
margin-top: 6px;
p {
p {
color: @gray-5;
color: @gray-5;
font-size: 12px;
font-size: 12px;
span {
span {
margin-right: 15px;
margin-right: 15px;
}
}
}
}
}
}
@keyframes waveAnimation {
@keyframes waveAnimation {
0%, 100% {
0%,
100% {
transform: scaleY(0.3);
transform: scaleY(0.3);
opacity: 0.5;
opacity: 0.5;
}
}
50% {
50% {
transform: scaleY(1);
transform: scaleY(1);
opacity: 1;
opacity: 1;
...
...
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