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
136d1a8c
Commit
136d1a8c
authored
Dec 19, 2025
by
水玉婷
Browse files
feat:添加历史记录页面
parent
c865fc04
Changes
6
Hide whitespace changes
Inline
Side-by-side
src/router/index.js
View file @
136d1a8c
...
@@ -8,6 +8,12 @@ const routes = [
...
@@ -8,6 +8,12 @@ const routes = [
component
:
()
=>
import
(
'
../views/Home.vue
'
),
component
:
()
=>
import
(
'
../views/Home.vue
'
),
meta
:
{
requiresAuth
:
true
}
meta
:
{
requiresAuth
:
true
}
},
},
{
path
:
'
/history
'
,
name
:
'
History
'
,
component
:
()
=>
import
(
'
../views/History.vue
'
),
meta
:
{
requiresAuth
:
true
}
},
{
{
path
:
'
/login
'
,
path
:
'
/login
'
,
name
:
'
Login
'
,
name
:
'
Login
'
,
...
...
src/views/History.vue
0 → 100644
View file @
136d1a8c
<
template
>
<div
class=
"history-container"
>
<!-- 头部栏 -->
<div
class=
"history-header"
>
<div
class=
"header-left"
>
<button
class=
"menu-button"
@
click=
"toggleHistoryPanel"
>
<menu-outlined
/>
</button>
<h2
class=
"header-title"
>
{{
appName
||
'
国械小智
'
}}
</h2>
</div>
<div
class=
"header-right"
>
<button
class=
"new-chat-button"
@
click=
"createNewChat"
>
<plus-outlined
/>
</button>
</div>
</div>
<!-- 历史记录侧边栏 -->
<div
class=
"history-sidebar"
:class=
"
{ 'sidebar-open': isHistoryPanelOpen }">
<div
class=
"sidebar-header"
>
<h3>
历史记录
</h3>
<button
class=
"close-button"
@
click=
"toggleHistoryPanel"
>
<close-outlined
/>
</button>
</div>
<div
class=
"search-box"
>
<div
class=
"search-input"
>
<search-outlined
class=
"search-icon"
/>
<input
v-model=
"searchKeyword"
type=
"text"
placeholder=
"搜索历史记录..."
@
input=
"filterHistory"
/>
</div>
</div>
<div
class=
"history-list"
>
<div
v-for=
"session in recentSessions"
:key=
"session.id"
:class=
"['history-item',
{ active: session.id === currentSessionId }]"
@click="selectSession(session)"
>
<div
class=
"session-info"
>
<div
class=
"session-title"
>
{{
session
.
title
}}
</div>
</div>
<button
class=
"delete-button"
@
click.stop=
"deleteSession(session)"
>
<delete-outlined
/>
</button>
</div>
<div
v-if=
"recentSessions.length === 0"
class=
"empty-state"
>
<file-search-outlined
class=
"empty-icon"
/>
<p>
暂无历史记录
</p>
</div>
</div>
<div
class=
"history-list-header"
v-if=
"totalCount > 0"
>
<span
class=
"total-count"
>
共
{{
totalCount
}}
条记录
</span>
<span
class=
"display-count"
>
显示最近
{{
displayLimit
}}
条
</span>
</div>
</div>
<!-- 遮罩层 -->
<div
class=
"sidebar-overlay"
:class=
"
{ 'overlay-visible': isHistoryPanelOpen }"
@click="toggleHistoryPanel"
>
</div>
<!-- 主内容区域 -->
<div
class=
"main-content"
>
<AiChat
:params=
"chatParams"
:dialogSessionId=
"currentSessionId"
:detailData=
"currentSessionDetail"
:apiBaseUrl=
"apiBaseUrl"
:token=
"userToken"
:appCode=
"appCode"
customClass=
"chat-history"
:key=
"currentSessionId+new Date().getTime()"
/>
</div>
</div>
</
template
>
<
script
setup
lang=
"ts"
>
import
{
ref
,
computed
,
onMounted
}
from
'
vue
'
;
import
AiChat
from
'
./components/AiChat.vue
'
;
import
axios
from
'
../utils/axios
'
;
import
{
message
}
from
'
ant-design-vue
'
;
import
{
MenuOutlined
,
PlusOutlined
,
CloseOutlined
,
SearchOutlined
,
DeleteOutlined
,
FileSearchOutlined
}
from
'
@ant-design/icons-vue
'
;
// 响应式数据
const
isHistoryPanelOpen
=
ref
(
false
);
const
searchKeyword
=
ref
(
''
);
const
currentSessionId
=
ref
(
''
);
const
currentSessionDetail
=
ref
({
title
:
''
});
// API配置
const
getApiBaseUrl
=
()
=>
{
const
basePath
=
import
.
meta
.
env
.
VITE_API_BASE_PATH
;
if
(
basePath
===
'
/
'
)
{
return
''
;
}
return
basePath
;
};
const
apiBaseUrl
=
getApiBaseUrl
();
const
userInfo
=
localStorage
.
getItem
(
'
wechat_user
'
);
const
{
extMap
=
{}
}
=
JSON
.
parse
(
userInfo
||
'
{}
'
);
const
userToken
=
extMap
.
sessionId
;
const
appCode
=
import
.
meta
.
env
.
VITE_APP_CODE
||
'
ped.qywx
'
;
const
chatParams
=
{
appId
:
'
83b2664019a945d0a438abe6339758d8
'
,
stage
:
'
wechat-demo
'
,
};
const
totalCount
=
ref
(
0
);
const
appName
=
ref
(
''
);
interface
Session
{
id
:
string
;
title
:
string
;
version
:
number
;
}
const
historyList
=
ref
<
Session
[]
>
([]);
// 获取对话记录
const
getChatRecordList
=
async
()
=>
{
let
res
=
await
axios
.
post
(
`
${
apiBaseUrl
}
/aiService/ask/history`
,
{
pageNum
:
1
,
pageSize
:
20
,
queryObject
:
{
appId
:
chatParams
.
appId
,
offsetDay
:
365
,
},
});
if
(
res
.
data
.
code
===
0
)
{
let
{
data
=
[],
total
=
0
,
totalInfo
=
{}
}
=
res
.
data
.
data
||
[];
historyList
.
value
=
(
data
||
[]).
map
((
item
)
=>
({
...
item
,
isEdit
:
false
,
}));
totalCount
.
value
=
total
;
appName
.
value
=
totalInfo
?.
app_name
||
''
;
}
};
// 显示限制
const
displayLimit
=
20
;
// 计算属性:过滤后的会话列表
const
filteredSessions
=
computed
(()
=>
{
if
(
!
searchKeyword
.
value
)
{
return
historyList
.
value
;
}
return
historyList
.
value
.
filter
((
session
:
any
)
=>
session
.
title
.
toLowerCase
().
includes
(
searchKeyword
.
value
.
toLowerCase
())
);
});
// 计算属性:最近显示的会话列表
const
recentSessions
=
computed
(()
=>
{
const
sessions
=
filteredSessions
.
value
;
return
sessions
.
slice
(
0
,
displayLimit
);
});
// 方法
const
toggleHistoryPanel
=
()
=>
{
isHistoryPanelOpen
.
value
=
!
isHistoryPanelOpen
.
value
;
};
const
filterHistory
=
()
=>
{
// 搜索功能已通过计算属性实现
};
const
selectSession
=
(
session
:
any
)
=>
{
currentSessionId
.
value
=
session
.
id
;
currentSessionDetail
.
value
=
{
title
:
session
.
title
};
isHistoryPanelOpen
.
value
=
false
;
};
const
createNewChat
=
()
=>
{
// 生成新的会话ID
currentSessionId
.
value
=
''
currentSessionDetail
.
value
=
{
title
:
'
新对话
'
};
isHistoryPanelOpen
.
value
=
false
;
};
const
deleteSession
=
async
(
record
:
Session
)
=>
{
const
res
=
await
axios
.
post
(
`
${
apiBaseUrl
}
/aiService/ask/delete/history`
,
{
id
:
record
.
id
,
version
:
record
.
version
,
});
if
(
res
.
data
.
code
===
0
)
{
message
.
success
(
'
删除成功
'
);
// 检查是否删除的是当前活跃的会话
if
(
currentSessionId
.
value
===
record
.
id
)
{
// 如果删除的是当前会话,切换到新会话或清空状态
if
(
historyList
.
value
.
length
>
1
)
{
// 如果有其他会话,切换到第一个会话
const
remainingSessions
=
historyList
.
value
.
filter
(
session
=>
session
.
id
!==
record
.
id
);
if
(
remainingSessions
.
length
>
0
)
{
selectSession
(
remainingSessions
[
0
]);
}
else
{
// 如果没有其他会话,创建新会话
createNewChat
();
}
}
else
{
// 如果没有其他会话,创建新会话
createNewChat
();
}
}
getChatRecordList
();
// 删除后重新获取对话记录
}
};
// 生命周期
onMounted
(()
=>
{
getChatRecordList
();
});
</
script
>
<
style
lang=
"less"
scoped
>
@import './components/style.less';
.chat-history {
height: calc(100vh - 52px);
}
// 历史记录容器
.history-container {
height: 100vh;
display: flex;
flex-direction: column;
background-color: #f5f5f5;
position: relative;
overflow: hidden;
}
// 头部样式
.history-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 20px;
background: #ffffff;
border-bottom: 1px solid #e8e8e8;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
z-index: 100;
.header-left {
display: flex;
align-items: center;
gap: 12px;
.menu-button {
background: none;
border: none;
padding: 8px;
border-radius: 6px;
cursor: pointer;
color: #666;
transition: all 0.2s ease;
&:hover {
background: #f0f0f0;
color: #1890ff;
}
}
.header-title {
font-size: 18px;
font-weight: 600;
color: #333;
margin: 0;
}
}
.header-right {
.new-chat-button {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
background: #f0f0f0;
color: #666;
border: none;
border-radius: 50%;
cursor: pointer;
transition: all 0.2s ease;
&:hover {
background: #e0e0e0;
color: #1890ff;
transform: scale(1.05);
}
}
}
}
// 侧边栏样式
.history-sidebar {
position: fixed;
top: 0;
left: -300px;
width: 300px;
height: 100vh;
background: #ffffff;
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.15);
transition: left 0.3s ease;
z-index: 1000;
display: flex;
flex-direction: column;
&.sidebar-open {
left: 0;
}
.sidebar-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid #e8e8e8;
h3 {
font-size: 16px;
font-weight: 600;
color: #333;
margin: 0;
}
.close-button {
background: none;
border: none;
padding: 4px;
border-radius: 4px;
cursor: pointer;
color: #666;
&:hover {
background: #f0f0f0;
color: #1890ff;
}
}
}
.search-box {
padding: 16px 20px;
border-bottom: 1px solid #e8e8e8;
.search-input {
position: relative;
.search-icon {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
color: #999;
}
input {
width: 100%;
padding: 8px 12px 8px 36px;
border: 1px solid #d9d9d9;
border-radius: 6px;
font-size: 14px;
&:focus {
outline: none;
border-color: #1890ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
&::placeholder {
color: #999;
}
}
}
}
.history-list-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 20px;
border-bottom: 1px solid #f0f0f0;
background: #fafafa;
font-size: 12px;
.total-count {
color: #666;
font-weight: 500;
}
.display-count {
color: #999;
}
}
.history-list {
flex: 1;
overflow-y: auto;
padding: 8px 0;
.history-item {
display: flex;
align-items: center;
padding: 12px 20px;
cursor: pointer;
transition: background-color 0.2s ease;
&:hover {
background: #f5f5f5;
}
&.active {
background: #e6f7ff;
border-right: 3px solid #1890ff;
}
.session-info {
flex: 1;
min-width: 0;
.session-title {
font-size: 14px;
font-weight: 500;
color: #333;
margin-bottom: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.session-time {
font-size: 12px;
color: #999;
margin-bottom: 2px;
}
.session-preview {
font-size: 12px;
color: #666;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.delete-button {
background: none;
border: none;
padding: 4px;
border-radius: 4px;
cursor: pointer;
color: #ccc;
opacity: 0;
transition: all 0.2s ease;
&:hover {
color: #ff4d4f;
background: #fff2f0;
}
}
&:hover .delete-button {
opacity: 1;
}
}
.empty-state {
text-align: center;
padding: 40px 20px;
color: #999;
.empty-icon {
font-size: 48px;
margin-bottom: 16px;
color: #d9d9d9;
}
p {
font-size: 14px;
margin: 0;
}
}
}
}
// 遮罩层
.sidebar-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.5);
z-index: 999;
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
&.overlay-visible {
opacity: 1;
visibility: visible;
}
}
// 主内容区域
.main-content {
flex: 1;
display: flex;
flex-direction: column;
}
// 响应式设计
@media (max-width: 768px) {
.history-sidebar {
width: 280px;
&.sidebar-open {
left: 0;
}
}
.history-header {
padding: 10px 16px;
.header-title {
font-size: 16px;
}
.new-chat-button {
padding: 6px 12px;
font-size: 13px;
}
}
}
</
style
>
\ No newline at end of file
src/views/components/AiChat.vue
View file @
136d1a8c
...
@@ -53,12 +53,16 @@
...
@@ -53,12 +53,16 @@
<div
v-else
v-html=
"item.content"
class=
"message-inner-box"
></div>
<div
v-else
v-html=
"item.content"
class=
"message-inner-box"
></div>
<!-- 思考过程框 -->
<!-- 思考过程框 -->
<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 ? '▲ ' : '▼ '
<component
}}
<span
v-if=
"item.thinkingTimeText"
class=
"thinking-text"
>
{{ item.thinkingTimeText }}
</span></div>
:is=
"item.thinkBoxExpanded ? UpOutlined : DownOutlined"
class=
"toggle-icon"
/>
<span
v-if=
"item.thinkingTimeText"
class=
"thinking-text"
>
{{ item.thinkingTimeText }}
</span>
</div>
<div
v-if=
"item.thinkBoxExpanded"
class=
"think-box-content"
<div
v-if=
"item.thinkBoxExpanded"
class=
"think-box-content"
v-html=
"templateService.generateThinkingTemplate(item.thinkContent || '')"
></div>
v-html=
"templateService.generateThinkingTemplate(item.thinkContent || '')"
></div>
</div>
</div>
</template>
</template>
</div>
</div>
<!-- 操作按钮 -->
<!-- 操作按钮 -->
...
@@ -99,7 +103,7 @@ import { ref, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
...
@@ -99,7 +103,7 @@ import { ref, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
import
dayjs
from
'
dayjs
'
;
import
dayjs
from
'
dayjs
'
;
import
{
tableTemplate
}
from
'
./utils/tableTemplate
'
;
import
{
tableTemplate
}
from
'
./utils/tableTemplate
'
;
import
{
markdownTemplate
,
isLastBlockMarkdown
,
getLastMarkdownBlockIndex
,
mergeMarkdownContent
}
from
'
./utils/markdownTemplate
'
;
import
{
markdownTemplate
,
isLastBlockMarkdown
,
getLastMarkdownBlockIndex
,
mergeMarkdownContent
}
from
'
./utils/markdownTemplate
'
;
import
{
SendOutlined
,
UserOutlined
}
from
'
@ant-design/icons-vue
'
;
import
{
SendOutlined
,
UserOutlined
,
UpOutlined
,
DownOutlined
}
from
'
@ant-design/icons-vue
'
;
import
defaultAvatar
from
'
@/assets/logo.png
'
;
import
defaultAvatar
from
'
@/assets/logo.png
'
;
import
ChartComponent
from
'
./ChartComponent.vue
'
;
// 导入独立的图表组件
import
ChartComponent
from
'
./ChartComponent.vue
'
;
// 导入独立的图表组件
import
VoiceRecognition
from
'
./VoiceRecognition.vue
'
;
// 导入语音识别组件
import
VoiceRecognition
from
'
./VoiceRecognition.vue
'
;
// 导入语音识别组件
...
...
src/views/components/VoiceRecognition.vue
View file @
136d1a8c
...
@@ -657,6 +657,12 @@ defineExpose({
...
@@ -657,6 +657,12 @@ defineExpose({
z
-
index
:
100
;
z
-
index
:
100
;
background
:
transparent
!
important
;
background
:
transparent
!
important
;
// 禁止用户选择文本
-
webkit
-
user
-
select
:
none
;
-
moz
-
user
-
select
:
none
;
-
ms
-
user
-
select
:
none
;
user
-
select
:
none
;
&
.
recording
{
&
.
recording
{
background
:
@
primary
-
color
;
background
:
@
primary
-
color
;
top
:
0
!
important
;
top
:
0
!
important
;
...
...
src/views/components/style.less
View file @
136d1a8c
...
@@ -35,12 +35,6 @@
...
@@ -35,12 +35,6 @@
padding: 0;
padding: 0;
margin: 0;
margin: 0;
box-sizing: border-box;
box-sizing: border-box;
// 禁止用户选择文本
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
// Webkit浏览器滚动条样式(Chrome, Safari, Edge)
// Webkit浏览器滚动条样式(Chrome, Safari, Edge)
&::-webkit-scrollbar {
&::-webkit-scrollbar {
...
@@ -315,7 +309,7 @@ li {
...
@@ -315,7 +309,7 @@ li {
:deep(.think-box-toggle) {
:deep(.think-box-toggle) {
color: @primary-color;
color: @primary-color;
cursor: pointer;
cursor: pointer;
font-size: 1
2
px;
font-size: 1
4
px;
text-decoration: none;
text-decoration: none;
align-items: center;
align-items: center;
gap: 4px;
gap: 4px;
...
...
src/views/components/utils/markdownTemplate.ts
View file @
136d1a8c
...
@@ -170,13 +170,21 @@ export const parseMarkdown = (text: string): string => {
...
@@ -170,13 +170,21 @@ export const parseMarkdown = (text: string): string => {
return
`<a href="
${
url
}
"
${
target
}
>
${
text
}
</a>`
;
return
`<a href="
${
url
}
"
${
target
}
>
${
text
}
</a>`
;
});
});
// 处理有序列表
// 处理有序列表(支持列表项中包含图片)
text
=
text
.
replace
(
/^
\d
+
\.\s
+
(
.*
)
$/gim
,
'
<li>$1</li>
'
);
text
=
text
.
replace
(
/^
(\d
+
\.\s
+
)(
.*
)
$/gim
,
(
match
,
prefix
,
content
)
=>
{
// 先处理内容中的Markdown格式(包括图片)
const
processedContent
=
parseMarkdown
(
content
);
return
`<li>
${
processedContent
}
</li>`
;
});
text
=
text
.
replace
(
/
(
<li>.*<
\/
li>
)(?=\s
*<li>
)
/gim
,
'
$1
'
);
text
=
text
.
replace
(
/
(
<li>.*<
\/
li>
)(?=\s
*<li>
)
/gim
,
'
$1
'
);
text
=
text
.
replace
(
/
(
<li>.*<
\/
li>
)
/gim
,
'
<ol>$1</ol>
'
);
text
=
text
.
replace
(
/
(
<li>.*<
\/
li>
)
/gim
,
'
<ol>$1</ol>
'
);
// 处理无序列表
// 处理无序列表(支持列表项中包含图片)
text
=
text
.
replace
(
/^
[
-*+
]\s
+
(
.*
)
$/gim
,
'
<li>$1</li>
'
);
text
=
text
.
replace
(
/^
([
-*+
]\s
+
)(
.*
)
$/gim
,
(
match
,
prefix
,
content
)
=>
{
// 先处理内容中的Markdown格式(包括图片)
const
processedContent
=
parseMarkdown
(
content
);
return
`<li>
${
processedContent
}
</li>`
;
});
text
=
text
.
replace
(
/
(
<li>.*<
\/
li>
)
/gim
,
'
<ul>$1</ul>
'
);
text
=
text
.
replace
(
/
(
<li>.*<
\/
li>
)
/gim
,
'
<ul>$1</ul>
'
);
// 处理水平分割线
// 处理水平分割线
...
@@ -589,12 +597,12 @@ class StreamingListProcessor {
...
@@ -589,12 +597,12 @@ class StreamingListProcessor {
if
(
this
.
listType
===
'
ordered
'
)
{
if
(
this
.
listType
===
'
ordered
'
)
{
// 有序列表:移除数字标记,保留内容,但保持正确的序号
// 有序列表:移除数字标记,保留内容,但保持正确的序号
const
content
=
item
.
replace
(
/^
\d
+
\.\s
+/
,
''
).
trim
();
const
content
=
item
.
replace
(
/^
\d
+
\.\s
+/
,
''
).
trim
();
const
processedContent
=
p
rocess
Markdown
Format
(
content
);
const
processedContent
=
p
arse
Markdown
(
content
);
return
`<li value="
${
this
.
orderedListStartNumber
+
index
}
">
${
processedContent
}
</li>`
;
return
`<li value="
${
this
.
orderedListStartNumber
+
index
}
">
${
processedContent
}
</li>`
;
}
else
{
}
else
{
// 无序列表:移除标记,保留内容
// 无序列表:移除标记,保留内容
const
content
=
item
.
replace
(
/^
[
-*+
]\s
+/
,
''
).
trim
();
const
content
=
item
.
replace
(
/^
[
-*+
]\s
+/
,
''
).
trim
();
const
processedContent
=
p
rocess
Markdown
Format
(
content
);
const
processedContent
=
p
arse
Markdown
(
content
);
return
`<li>
${
processedContent
}
</li>`
;
return
`<li>
${
processedContent
}
</li>`
;
}
}
});
});
...
...
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