Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
V
Vue-Dashboard
Project
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
高源
Vue-Dashboard
Commits
70990881
Commit
70990881
authored
Jun 10, 2025
by
User
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
AI聊天集成
parent
083691ba
Show whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
1781 additions
and
1779 deletions
+1781
-1779
.env.prod
.env.prod
+1
-1
.env.test
.env.test
+1
-1
index.html
dist/index.html
+3
-3
index.ts
src/router/routes/index.ts
+112
-112
[url].vue
src/views/_builtin/iframe-page/[url].vue
+23
-5
chat-component.vue
src/views/chat/chat-component.vue
+1605
-0
index.vue
src/views/chat/index.vue
+36
-1657
No files found.
.env.prod
View file @
70990881
# backend service base url, prod environment
# backend service base url, prod environment
VITE_SERVICE_BASE_URL='
http://localhost:80
'
VITE_SERVICE_BASE_URL=''
# other backend service base url, prod environment
# other backend service base url, prod environment
VITE_OTHER_SERVICE_BASE_URL= `{
VITE_OTHER_SERVICE_BASE_URL= `{
...
...
.env.test
View file @
70990881
# backend service base url, test environment
# backend service base url, test environment
VITE_SERVICE_BASE_URL
=
'
http://localhost:80
'
VITE_SERVICE_BASE_URL
=
''
# other backend service base url, test environment
# other backend service base url, test environment
VITE_OTHER_SERVICE_BASE_URL
=
`{
VITE_OTHER_SERVICE_BASE_URL
=
`{
...
...
dist/index.html
View file @
70990881
<!doctype html>
<!doctype html>
<html
lang=
"zh-cmn-Hans"
>
<html
lang=
"zh-cmn-Hans"
>
<head>
<head>
<meta
name=
"buildTime"
content=
"2025-06-10
15:32:11
"
>
<meta
name=
"buildTime"
content=
"2025-06-10
20:28:50
"
>
<meta
charset=
"UTF-8"
/>
<meta
charset=
"UTF-8"
/>
<link
rel=
"icon"
href=
"/favicon.svg"
/>
<link
rel=
"icon"
href=
"/favicon.svg"
/>
<meta
name=
"viewport"
content=
"width=device-width, initial-scale=1.0"
/>
<meta
name=
"viewport"
content=
"width=device-width, initial-scale=1.0"
/>
<meta
name=
"color-scheme"
content=
"light dark"
/>
<meta
name=
"color-scheme"
content=
"light dark"
/>
<title>
VueDashboard
</title>
<title>
VueDashboard
</title>
<script
type=
"module"
crossorigin
src=
"/Content/VueDashboardUi/VueDashboard1/assets/index-B
V5IaHCk
.js"
></script>
<script
type=
"module"
crossorigin
src=
"/Content/VueDashboardUi/VueDashboard1/assets/index-B
_dt3SP8
.js"
></script>
<link
rel=
"stylesheet"
crossorigin
href=
"/Content/VueDashboardUi/VueDashboard1/assets/index-B
FEuYhFr
.css"
>
<link
rel=
"stylesheet"
crossorigin
href=
"/Content/VueDashboardUi/VueDashboard1/assets/index-B
LjiwC98
.css"
>
</head>
</head>
<body>
<body>
<div
id=
"app"
></div>
<div
id=
"app"
></div>
...
...
src/router/routes/index.ts
View file @
70990881
...
@@ -13,49 +13,49 @@ import { transformElegantRoutesToVueRoutes } from '../elegant/transform';
...
@@ -13,49 +13,49 @@ import { transformElegantRoutesToVueRoutes } from '../elegant/transform';
* @link https://github.com/soybeanjs/elegant-router?tab=readme-ov-file#custom-route
* @link https://github.com/soybeanjs/elegant-router?tab=readme-ov-file#custom-route
*/
*/
const
customRoutes
:
CustomRoute
[]
=
[
const
customRoutes
:
CustomRoute
[]
=
[
{
//
{
name
:
'exception'
,
//
name: 'exception',
path
:
'/exception'
,
//
path: '/exception',
component
:
'layout.base'
,
//
component: 'layout.base',
meta
:
{
//
meta: {
title
:
'exception'
,
//
title: 'exception',
i18nKey
:
'route.exception'
,
//
i18nKey: 'route.exception',
icon
:
'ant-design:exception-outlined'
,
//
icon: 'ant-design:exception-outlined',
order
:
7
//
order: 7
},
//
},
children
:
[
//
children: [
{
//
{
name
:
'exception_403'
,
//
name: 'exception_403',
path
:
'/exception/403'
,
//
path: '/exception/403',
component
:
'view.403'
,
//
component: 'view.403',
meta
:
{
//
meta: {
title
:
'exception_403'
,
//
title: 'exception_403',
i18nKey
:
'route.exception_403'
,
//
i18nKey: 'route.exception_403',
icon
:
'ic:baseline-block'
//
icon: 'ic:baseline-block'
}
//
}
},
//
},
{
//
{
name
:
'exception_404'
,
//
name: 'exception_404',
path
:
'/exception/404'
,
//
path: '/exception/404',
component
:
'view.404'
,
//
component: 'view.404',
meta
:
{
//
meta: {
title
:
'exception_404'
,
//
title: 'exception_404',
i18nKey
:
'route.exception_404'
,
//
i18nKey: 'route.exception_404',
icon
:
'ic:baseline-web-asset-off'
//
icon: 'ic:baseline-web-asset-off'
}
//
}
},
//
},
{
//
{
name
:
'exception_500'
,
//
name: 'exception_500',
path
:
'/exception/500'
,
//
path: '/exception/500',
component
:
'view.500'
,
//
component: 'view.500',
meta
:
{
//
meta: {
title
:
'exception_500'
,
//
title: 'exception_500',
i18nKey
:
'route.exception_500'
,
//
i18nKey: 'route.exception_500',
icon
:
'ic:baseline-wifi-off'
//
icon: 'ic:baseline-wifi-off'
}
//
}
}
//
}
]
//
]
},
//
},
// 以下是iframe-page的示例
// 以下是iframe-page的示例
...
@@ -194,82 +194,82 @@ const customRoutes: CustomRoute[] = [
...
@@ -194,82 +194,82 @@ const customRoutes: CustomRoute[] = [
// 以下是获取菜单的示例
// 以下是获取菜单的示例
// ${window.uiGlobalConfig.InternalCode}
// ${window.uiGlobalConfig.InternalCode}
// const { data: menus } = await getRootMenu(`/Restful/Kivii.Basic.Entities.Menu/Show.json?RootInternalCode=dashboard`);
// const { data: menus } = await getRootMenu(`/Restful/Kivii.Basic.Entities.Menu/Show.json?RootInternalCode=dashboard`);
// //
const { data: menus } = await getRootMenu(
const
{
data
:
menus
}
=
await
getRootMenu
(
// //
`/Restful/Kivii.Basic.Entities.Menu/Show.json?RootInternalCode=${window.uiGlobalConfig.InternalCode}`
`/Restful/Kivii.Basic.Entities.Menu/Show.json?RootInternalCode=
${
window
.
uiGlobalConfig
.
InternalCode
}
`
// //
);
);
//
const MenuThree = await getMenuThree(menus?.MenusMain?.Results);
const
MenuThree
=
await
getMenuThree
(
menus
?.
MenusMain
?.
Results
);
//
const MenuRoot = menus?.MenuRoot;
const
MenuRoot
=
menus
?.
MenuRoot
;
// // console.log(MenuRoot);
// // console.log(MenuRoot);
// // 存储 MenuRoot 到 store
// // 存储 MenuRoot 到 store
//
if (MenuRoot) {
if
(
MenuRoot
)
{
//
setTimeout(() => {
setTimeout
(()
=>
{
//
const routeStore = useRouteStore();
const
routeStore
=
useRouteStore
();
//
routeStore.setMenuRoot(MenuRoot);
routeStore
.
setMenuRoot
(
MenuRoot
);
//
}, 1000);
},
1000
);
//
}
}
//
const MenuThree2 = generateRoutes(MenuThree);
const
MenuThree2
=
generateRoutes
(
MenuThree
);
//
if (MenuThree2.length > 0) {
if
(
MenuThree2
.
length
>
0
)
{
//
for (let i = 0; i < MenuThree2.length; i++) {
for
(
let
i
=
0
;
i
<
MenuThree2
.
length
;
i
++
)
{
//
customRoutes.push(MenuThree2[i]);
customRoutes
.
push
(
MenuThree2
[
i
]);
//
}
}
//
}
}
//
function getMenuThree(data: any) {
function
getMenuThree
(
data
:
any
)
{
//
const cloneData = JSON.parse(JSON.stringify(data)); // 对源数据深度克隆
const
cloneData
=
JSON
.
parse
(
JSON
.
stringify
(
data
));
// 对源数据深度克隆
//
return cloneData.filter((father: { Kvid: any; children: any; ParentKvid: undefined }) => {
return
cloneData
.
filter
((
father
:
{
Kvid
:
any
;
children
:
any
;
ParentKvid
:
undefined
})
=>
{
//
const branchArr = cloneData.filter((child: { ParentKvid: any }) => father.Kvid === child.ParentKvid);
const
branchArr
=
cloneData
.
filter
((
child
:
{
ParentKvid
:
any
})
=>
father
.
Kvid
===
child
.
ParentKvid
);
//
// eslint-disable-next-line no-unused-expressions
// eslint-disable-next-line no-unused-expressions
//
branchArr.length > 0 ? (father.children = branchArr) : '';
branchArr
.
length
>
0
?
(
father
.
children
=
branchArr
)
:
''
;
//
// eslint-disable-next-line eqeqeq
// eslint-disable-next-line eqeqeq
//
return father.ParentKvid == undefined;
return
father
.
ParentKvid
==
undefined
;
//
});
});
//
}
}
//
function generateRoutes(data: any[]) {
function
generateRoutes
(
data
:
any
[])
{
//
return data.map(item => {
return
data
.
map
(
item
=>
{
//
const route: any = {
const
route
:
any
=
{
//
name: item.Type === 'System' ? `${item.Type}` : item.Kvid,
name
:
item
.
Type
===
'System'
?
`
${
item
.
Type
}
`
:
item
.
Kvid
,
//
path: item.Type === 'System' ? `/${item.Type}` : `/${item.Kvid}`,
path
:
item
.
Type
===
'System'
?
`/
${
item
.
Type
}
`
:
`/
${
item
.
Kvid
}
`
,
//
component: 'layout.base',
component
:
'layout.base'
,
//
meta: {
meta
:
{
//
title: item.DisplayName,
title
:
item
.
DisplayName
,
//
icon: 'mdi:file-document-multiple-outline',
icon
:
'mdi:file-document-multiple-outline'
,
//
order: item.SortId,
order
:
item
.
SortId
,
//
keepAlive: true
keepAlive
:
true
//
},
},
//
children: []
children
:
[]
//
};
};
//
if (item.children && item.children.length > 0) {
if
(
item
.
children
&&
item
.
children
.
length
>
0
)
{
//
route.children = item.children.map(
route
.
children
=
item
.
children
.
map
(
//
(child: { Remark: string; Type: string; Kvid: any; DisplayName: any; Icon: any; SortId: any }) => {
(
child
:
{
Remark
:
string
;
Type
:
string
;
Kvid
:
any
;
DisplayName
:
any
;
Icon
:
any
;
SortId
:
any
})
=>
{
//
const sanitizedRemark =
const
sanitizedRemark
=
//
child.Remark && child.Remark.startsWith('/') ? child.Remark.replace(/^\//, '') : child.Remark;
child
.
Remark
&&
child
.
Remark
.
startsWith
(
'/'
)
?
child
.
Remark
.
replace
(
/^
\/
/
,
''
)
:
child
.
Remark
;
//
return {
return
{
//
name: child.Type === 'System' ? `${item.Type}_${sanitizedRemark}` : `${item.Kvid}_${child.Kvid}`,
name
:
child
.
Type
===
'System'
?
`
${
item
.
Type
}
_
${
sanitizedRemark
}
`
:
`
${
item
.
Kvid
}
_
${
child
.
Kvid
}
`
,
//
path: child.Type === 'System' ? `/${item.Type}/${sanitizedRemark}` : `/${item.Kvid}/${child.Kvid}`,
path
:
child
.
Type
===
'System'
?
`/
${
item
.
Type
}
/
${
sanitizedRemark
}
`
:
`/
${
item
.
Kvid
}
/
${
child
.
Kvid
}
`
,
//
component: 'view.iframe-page',
component
:
'view.iframe-page'
,
//
props: {
props
:
{
//
url: child.Type === 'System' ? sanitizedRemark : '',
url
:
child
.
Type
===
'System'
?
sanitizedRemark
:
''
,
//
kvid: child.Kvid,
kvid
:
child
.
Kvid
,
//
type: child.Type
type
:
child
.
Type
//
},
},
//
meta: {
meta
:
{
//
title: child.DisplayName,
title
:
child
.
DisplayName
,
//
icon: child.Icon,
icon
:
child
.
Icon
,
//
order: child.SortId,
order
:
child
.
SortId
,
//
keepAlive: true,
keepAlive
:
true
,
//
type: 'iframe'
type
:
'iframe'
//
}
}
//
};
};
//
}
}
//
);
);
//
}
}
//
return route;
return
route
;
//
});
});
//
}
}
export
function
createStaticRoutes
()
{
export
function
createStaticRoutes
()
{
const
constantRoutes
:
ElegantRoute
[]
=
[];
const
constantRoutes
:
ElegantRoute
[]
=
[];
...
...
src/views/_builtin/iframe-page/[url].vue
View file @
70990881
...
@@ -7,6 +7,7 @@ import { loadModule } from 'vue3-sfc-loader';
...
@@ -7,6 +7,7 @@ import { loadModule } from 'vue3-sfc-loader';
import
{
getSelectMenu
}
from
'@/service/api'
;
import
{
getSelectMenu
}
from
'@/service/api'
;
import
NotFound
from
'@/views/_builtin/404/index.vue'
;
// 引入 404 组件
import
NotFound
from
'@/views/_builtin/404/index.vue'
;
// 引入 404 组件
import
{
useTabStore
}
from
'@/store/modules/tab'
;
import
{
useTabStore
}
from
'@/store/modules/tab'
;
import
ChatComponent
from
'@/views/chat/chat-component.vue'
;
// 导入聊天组件
import
ExtJsComponent
from
'./extJs.vue'
;
import
ExtJsComponent
from
'./extJs.vue'
;
import
WebviewComponent
from
'./webview.vue'
;
import
WebviewComponent
from
'./webview.vue'
;
import
VueComponent
from
'./vueComponent.vue'
;
// 添加新组件导入
import
VueComponent
from
'./vueComponent.vue'
;
// 添加新组件导入
...
@@ -62,19 +63,22 @@ const cleanupResources = () => {
...
@@ -62,19 +63,22 @@ const cleanupResources = () => {
hasError
.
value
=
false
;
hasError
.
value
=
false
;
};
};
// 监听
标签关闭事件
// 监听
函数引用
tabCloseEventBus
.
on
(
closedTabId
=>
{
const
tabCloseHandler
=
(
closedTabId
:
string
)
=>
{
// 只有当关闭的是当前标签时才清理资源
// 只有当关闭的是当前标签时才清理资源
if
(
closedTabId
===
tabStore
.
activeTabId
)
{
if
(
closedTabId
===
tabStore
.
activeTabId
)
{
cleanupResources
();
cleanupResources
();
}
}
});
};
// 监听标签关闭事件
tabCloseEventBus
.
on
(
tabCloseHandler
);
// 组件卸载前清理
// 组件卸载前清理
onBeforeUnmount
(()
=>
{
onBeforeUnmount
(()
=>
{
cleanupResources
();
cleanupResources
();
// 移除事件监听
// 移除事件监听
tabCloseEventBus
.
off
();
tabCloseEventBus
.
off
(
tabCloseHandler
);
});
});
// 修复console[type]的类型错误
// 修复console[type]的类型错误
...
@@ -90,8 +94,22 @@ const safeConsoleLog = (type: string, ...args: any[]) => {
...
@@ -90,8 +94,22 @@ const safeConsoleLog = (type: string, ...args: any[]) => {
}
}
};
};
// 定义加载外部组件的函数
// 内部组件映射表
const
internalComponents
:
Record
<
string
,
any
>
=
{
'chat-component.vue'
:
ChatComponent
};
// 定义加载组件的函数(优先加载内部组件)
const
loadExternalComponent
=
async
(
url
:
string
)
=>
{
const
loadExternalComponent
=
async
(
url
:
string
)
=>
{
// 首先检查是否为内部组件
const
componentName
=
url
.
split
(
'/'
).
pop
()
||
url
;
if
(
internalComponents
[
componentName
])
{
asyncComponent
.
value
=
internalComponents
[
componentName
];
console
.
log
(
'Loaded internal component:'
,
componentName
);
return
;
}
// 如果不是内部组件,则从服务端加载
const
options
=
{
const
options
=
{
moduleCache
:
{
moduleCache
:
{
vue
:
await
import
(
'vue'
)
vue
:
await
import
(
'vue'
)
...
...
src/views/chat/chat-component.vue
0 → 100644
View file @
70990881
<
script
setup
lang=
"ts"
>
import
{
computed
,
onMounted
,
onUnmounted
,
ref
}
from
'vue'
;
import
{
type
ModelOption
,
deepSeekService
}
from
'@/service/api/deepseek'
;
import
{
renderMarkdownWithMath
as
renderContentWithMath
}
from
'@/utils/katex-renderer'
;
interface
ChatMessage
{
id
:
string
;
from
:
'user'
|
'ai'
;
content
:
string
;
timestamp
:
number
;
thinkingTime
?:
number
;
contentType
?:
'text'
|
'markdown'
;
thinkingContent
?:
string
;
isShowingThinking
?:
boolean
;
currentThinkingText
?:
string
;
displayContent
?:
string
;
// 用于渐进显示内容
isTyping
?:
boolean
;
// 是否正在打字
phase
?:
'thinking'
|
'answering'
|
'completed'
;
// 当前阶段
}
interface
ChatHistory
{
id
:
string
;
title
:
string
;
lastMessage
:
string
;
timestamp
:
number
;
messages
:
ChatMessage
[];
}
// 响应式数据
const
searchText
=
ref
(
''
);
const
inputValue
=
ref
(
''
);
const
currentChatId
=
ref
(
'1'
);
const
showStartPage
=
ref
(
true
);
const
messages
=
ref
<
ChatMessage
[]
>
([]);
const
isAiThinking
=
ref
(
false
);
const
thinkingStartTime
=
ref
(
0
);
const
thinkingTime
=
ref
(
0
);
const
thinkingInterval
=
ref
<
NodeJS
.
Timeout
|
null
>
(
null
);
const
lastThinkingTime
=
ref
(
0
);
// 配置相关
const
showConfig
=
ref
(
false
);
const
selectedModelId
=
ref
(
'deepseek'
);
const
testing
=
ref
(
false
);
const
modelOptions
=
ref
<
ModelOption
[]
>
([]);
// 聊天历史数据
const
chatHistory
=
ref
<
ChatHistory
[]
>
([
{
id
:
'1'
,
title
:
'新对话'
,
lastMessage
:
''
,
timestamp
:
Date
.
now
(),
messages
:
[]
}
]);
// 计算属性
const
filteredChatHistory
=
computed
(()
=>
{
if
(
!
searchText
.
value
)
return
chatHistory
.
value
;
return
chatHistory
.
value
.
filter
(
chat
=>
chat
.
title
.
toLowerCase
().
includes
(
searchText
.
value
.
toLowerCase
()));
});
const
currentChat
=
computed
(()
=>
{
return
chatHistory
.
value
.
find
(
chat
=>
chat
.
id
===
currentChatId
.
value
);
});
const
isApiConfigured
=
computed
(()
=>
{
return
selectedModelId
.
value
!==
''
;
});
const
currentModelName
=
computed
(()
=>
{
if
(
!
selectedModelId
.
value
)
return
'AI助手'
;
const
currentModel
=
modelOptions
.
value
.
find
(
model
=>
model
.
id
===
selectedModelId
.
value
);
return
currentModel
?.
name
||
'AI助手'
;
});
// 聊天容器引用
const
chatContentRef
=
ref
<
HTMLElement
|
null
>
(
null
);
// 滚动到底部
const
scrollToBottom
=
()
=>
{
if
(
chatContentRef
.
value
)
{
setTimeout
(()
=>
{
if
(
chatContentRef
.
value
)
{
chatContentRef
.
value
.
scrollTop
=
chatContentRef
.
value
.
scrollHeight
;
}
},
50
);
}
};
const
newChat
=
()
=>
{
const
newChatId
=
`chat_
${
Date
.
now
()}
`
;
const
newChatItem
:
ChatHistory
=
{
id
:
newChatId
,
title
:
'新对话'
,
lastMessage
:
''
,
timestamp
:
Date
.
now
(),
messages
:
[]
};
chatHistory
.
value
.
unshift
(
newChatItem
);
currentChatId
.
value
=
newChatId
;
showStartPage
.
value
=
true
;
messages
.
value
=
[];
inputValue
.
value
=
''
;
};
const
switchChat
=
(
chatId
:
string
)
=>
{
const
chat
=
chatHistory
.
value
.
find
(
c
=>
c
.
id
===
chatId
);
if
(
chat
)
{
currentChatId
.
value
=
chatId
;
messages
.
value
=
[...
chat
.
messages
];
showStartPage
.
value
=
chat
.
messages
.
length
===
0
;
}
};
// 配置相关方法
const
onModelChange
=
()
=>
{
if
(
selectedModelId
.
value
)
{
deepSeekService
.
switchModel
(
selectedModelId
.
value
);
}
};
const
saveConfig
=
()
=>
{
localStorage
.
setItem
(
'deepseek-selected-model'
,
selectedModelId
.
value
);
deepSeekService
.
switchModel
(
selectedModelId
.
value
);
showConfig
.
value
=
false
;
// alert('配置已保存!');
};
const
testConnection
=
async
()
=>
{
testing
.
value
=
true
;
try
{
deepSeekService
.
switchModel
(
selectedModelId
.
value
);
await
deepSeekService
.
chat
([{
role
:
'user'
,
content
:
'Hello'
}],
{
maxTokens
:
10
});
// alert('连接测试成功!');
}
catch
(
error
)
{
// alert(`连接测试失败: ${error instanceof Error ? error.message : '未知错误'}`);
}
finally
{
testing
.
value
=
false
;
}
};
// 思考相关方法
const
startThinking
=
()
=>
{
isAiThinking
.
value
=
true
;
thinkingStartTime
.
value
=
Date
.
now
();
thinkingTime
.
value
=
0
;
thinkingInterval
.
value
=
setInterval
(()
=>
{
thinkingTime
.
value
=
Math
.
floor
((
Date
.
now
()
-
thinkingStartTime
.
value
)
/
1000
);
},
1000
);
};
const
stopThinking
=
()
=>
{
isAiThinking
.
value
=
false
;
if
(
thinkingInterval
.
value
)
{
clearInterval
(
thinkingInterval
.
value
);
thinkingInterval
.
value
=
null
;
}
};
// 模拟思考过程显示
const
showThinkingProcess
=
(
message
:
ChatMessage
,
content
:
string
)
=>
{
let
currentIndex
=
0
;
message
.
currentThinkingText
=
''
;
message
.
isShowingThinking
=
true
;
const
typeNextChar
=
()
=>
{
if
(
currentIndex
<
content
.
length
)
{
message
.
currentThinkingText
+=
content
[
currentIndex
];
currentIndex
+=
1
;
setTimeout
(
typeNextChar
,
15
);
// 快速显示思考过程
}
else
{
message
.
isShowingThinking
=
false
;
}
};
typeNextChar
();
};
// 更新聊天历史
const
updateChatHistory
=
()
=>
{
const
currentChatItem
=
chatHistory
.
value
.
find
(
chat
=>
chat
.
id
===
currentChatId
.
value
);
if
(
currentChatItem
)
{
currentChatItem
.
messages
=
[...
messages
.
value
];
currentChatItem
.
lastMessage
=
messages
.
value
[
messages
.
value
.
length
-
1
]?.
content
||
''
;
currentChatItem
.
timestamp
=
Date
.
now
();
// 如果是第一条消息,更新聊天标题
if
(
currentChatItem
.
title
===
'新对话'
&&
messages
.
value
.
length
>
0
)
{
const
firstUserMessage
=
messages
.
value
.
find
(
msg
=>
msg
.
from
===
'user'
);
if
(
firstUserMessage
)
{
currentChatItem
.
title
=
firstUserMessage
.
content
.
slice
(
0
,
20
)
+
(
firstUserMessage
.
content
.
length
>
20
?
'...'
:
''
);
}
}
}
};
// 混合方案:思考过程 + 真正流式输出
const
onSubmit
=
async
()
=>
{
if
(
!
inputValue
.
value
.
trim
()
||
!
isApiConfigured
.
value
)
return
;
showStartPage
.
value
=
false
;
// 添加用户消息
const
userMessage
:
ChatMessage
=
{
id
:
`msg_
${
Date
.
now
()}
`
,
from
:
'user'
,
content
:
inputValue
.
value
,
timestamp
:
Date
.
now
(),
contentType
:
'text'
};
messages
.
value
.
push
(
userMessage
);
scrollToBottom
();
const
question
=
inputValue
.
value
;
inputValue
.
value
=
''
;
// 开始思考状态
startThinking
();
// 创建AI消息,初始为思考阶段
const
aiMessage
:
ChatMessage
=
{
id
:
`msg_ai_
${
Date
.
now
()}
`
,
from
:
'ai'
,
content
:
''
,
timestamp
:
Date
.
now
(),
contentType
:
'markdown'
,
phase
:
'thinking'
,
isShowingThinking
:
true
,
currentThinkingText
:
''
,
displayContent
:
''
,
isTyping
:
false
};
messages
.
value
.
push
(
aiMessage
);
scrollToBottom
();
// 显示思考过程
const
thinkingContent
=
`分析用户问题:
${
question
}
让我仔细思考这个问题的关键要素和可能的解答方向...
首先需要理解问题的核心内容和用户的真实需求。
接下来我会从多个角度来分析这个问题:
1. 基础概念和定义
2. 相关背景信息
3. 可能的解决方案
4. 具体的建议和步骤
正在整理相关信息和最佳回答方案...`
;
showThinkingProcess
(
aiMessage
,
thinkingContent
);
try
{
// 等待思考过程显示一段时间后开始流式输出
setTimeout
(
async
()
=>
{
stopThinking
();
aiMessage
.
thinkingTime
=
Math
.
floor
((
Date
.
now
()
-
thinkingStartTime
.
value
)
/
1000
);
// 切换到答案阶段
aiMessage
.
phase
=
'answering'
;
aiMessage
.
isShowingThinking
=
false
;
aiMessage
.
isTyping
=
true
;
// console.log('🚀 开始流式输出答案...');
// 更详细的流式日志和强制刷新
await
deepSeekService
.
streamChat
(
[{
role
:
'user'
,
content
:
question
}],
// onChunk: 每收到一个数据块立即更新
async
(
chunk
:
string
)
=>
{
// console.log(`📦 [${chunkCount}] 收到数据块:`, JSON.stringify(chunk));
// 累加内容
aiMessage
.
content
+=
chunk
;
// 找到消息在数组中的位置并完全替换
const
messageIndex
=
messages
.
value
.
findIndex
(
m
=>
m
.
id
===
aiMessage
.
id
);
if
(
messageIndex
!==
-
1
)
{
// 创建新的消息对象强制触发响应式更新
messages
.
value
[
messageIndex
]
=
{
...
aiMessage
,
content
:
aiMessage
.
content
,
displayContent
:
aiMessage
.
content
};
scrollToBottom
();
}
// console.log(
// `📝 更新后总长度: ${aiMessage.content.length}, 显示长度: ${aiMessage.displayContent || aiMessage.content.length}`
// );
},
// onComplete: 流式传输完成
()
=>
{
aiMessage
.
isTyping
=
false
;
aiMessage
.
phase
=
'completed'
;
lastThinkingTime
.
value
=
aiMessage
.
thinkingTime
||
0
;
// console.log(`✅ 流式传输完成,共收到 ${chunkCount} 个数据块,最终内容长度: ${aiMessage.content.length}`);
updateChatHistory
();
},
// onError: 处理错误
(
error
:
Error
)
=>
{
// console.error('❌ 流式传输错误:', error);
aiMessage
.
content
=
`抱歉,AI回复失败:
${
error
.
message
}
`
;
aiMessage
.
displayContent
=
aiMessage
.
content
;
aiMessage
.
isTyping
=
false
;
aiMessage
.
phase
=
'completed'
;
}
);
},
3000
);
// 3秒思考时间
}
catch
(
error
)
{
stopThinking
();
// console.error('AI回复失败:', error);
aiMessage
.
content
=
`抱歉,AI回复失败:
${
error
instanceof
Error
?
error
.
message
:
'未知错误'
}
`
;
aiMessage
.
isTyping
=
false
;
aiMessage
.
phase
=
'completed'
;
}
};
// 其他功能方法
const
handleKeyDown
=
(
event
:
KeyboardEvent
)
=>
{
if
(
event
.
key
===
'Enter'
&&
!
event
.
shiftKey
)
{
event
.
preventDefault
();
onSubmit
();
}
};
const
copyMessage
=
async
(
content
:
string
)
=>
{
try
{
await
navigator
.
clipboard
.
writeText
(
content
);
// alert('已复制到剪贴板');
}
catch
(
error
)
{
// console.error('复制失败:', error);
}
};
// 格式化内容,将思考过程和回答内容组合
const
getFormattedContent
=
(
message
:
ChatMessage
):
string
=>
{
let
content
=
''
;
// 如果有思考过程就直接显示
if
(
message
.
currentThinkingText
)
{
content
+=
`<think>
${
message
.
currentThinkingText
}
</think>
`
;
}
// 添加最终答案内容
if
(
message
.
content
&&
(
message
.
phase
===
'completed'
||
message
.
phase
===
'answering'
))
{
content
+=
message
.
displayContent
||
message
.
content
;
}
return
content
;
};
// 使用自定义 markdown 渲染器渲染内容
const
renderMarkdownWithMath
=
(
content
:
string
):
string
=>
{
try
{
const
result
=
renderContentWithMath
(
content
);
console
.
log
(
'Markdown 渲染结果:'
,
result
);
return
result
;
}
catch
(
error
)
{
console
.
error
(
'Markdown 渲染错误:'
,
error
);
return
content
;
// 返回原内容作为fallback
}
};
const
regenerateMessage
=
async
(
message
:
ChatMessage
)
=>
{
// 找到对应的用户消息
const
messageIndex
=
messages
.
value
.
findIndex
(
msg
=>
msg
.
id
===
message
.
id
);
if
(
messageIndex
>
0
)
{
const
userMessage
=
messages
.
value
[
messageIndex
-
1
];
if
(
userMessage
.
from
===
'user'
)
{
// 删除AI消息,重新生成
messages
.
value
.
splice
(
messageIndex
,
1
);
inputValue
.
value
=
userMessage
.
content
;
onSubmit
();
}
}
};
const
showChatMenu
=
(
_chatId
:
string
)
=>
{
// 实现聊天菜单功能(删除、重命名等)
// console.log('显示聊天菜单', _chatId);
};
// 生命周期
onMounted
(()
=>
{
// 加载可用模型选项
modelOptions
.
value
=
deepSeekService
.
getModelOptions
();
// 加载保存的模型选择
const
savedModel
=
localStorage
.
getItem
(
'deepseek-selected-model'
);
if
(
savedModel
)
{
selectedModelId
.
value
=
savedModel
;
deepSeekService
.
switchModel
(
savedModel
);
}
});
onUnmounted
(()
=>
{
// 清理定时器
if
(
thinkingInterval
.
value
)
{
clearInterval
(
thinkingInterval
.
value
);
}
});
</
script
>
<
template
>
<div
class=
"chat-container"
>
<!-- 左侧侧边栏 -->
<div
class=
"sidebar"
>
<!-- 顶部工具栏 -->
<div
class=
"sidebar-header"
>
<div
class=
"logo-section"
>
<div
class=
"logo"
>
<i
class=
"icon-user"
></i>
</div>
<span
class=
"title"
>
{{
currentModelName
}}
</span>
</div>
<div
class=
"header-actions"
>
<i
class=
"icon-add action-btn"
@
click=
"newChat"
></i>
<i
class=
"icon-settings action-btn"
title=
"API配置"
@
click=
"showConfig = !showConfig"
></i>
</div>
</div>
<!-- 模型配置面板 -->
<div
v-if=
"showConfig"
class=
"config-panel"
>
<div
class=
"config-item"
>
<label>
选择模型:
</label>
<select
v-model=
"selectedModelId"
@
change=
"onModelChange"
>
<option
v-for=
"model in modelOptions"
:key=
"model.id"
:value=
"model.id"
>
{{
model
.
name
}}
</option>
</select>
<div
v-if=
"selectedModelId"
class=
"model-description"
>
{{
modelOptions
.
find
(
m
=>
m
.
id
===
selectedModelId
)?.
description
}}
</div>
</div>
<div
class=
"config-actions"
>
<button
:disabled=
"!selectedModelId"
class=
"save-btn"
@
click=
"saveConfig"
>
保存
</button>
<button
:disabled=
"testing || !selectedModelId"
class=
"test-btn"
@
click=
"testConnection"
>
{{
testing
?
'测试中...'
:
'测试连接'
}}
</button>
</div>
</div>
<!-- 搜索框 -->
<div
class=
"search-section"
>
<div
class=
"search-input"
>
<i
class=
"icon-search"
></i>
<input
v-model=
"searchText"
placeholder=
"搜索对话"
/>
</div>
</div>
<!-- 历史对话列表 -->
<div
class=
"chat-history"
>
<div
class=
"history-section"
>
<div
class=
"section-title"
>
<span>
对话历史
</span>
</div>
<div
v-for=
"chat in filteredChatHistory"
:key=
"chat.id"
class=
"chat-item"
:class=
"
{ active: currentChatId === chat.id }"
@click="switchChat(chat.id)"
>
<i
class=
"icon-message"
></i>
<span
class=
"chat-title"
>
{{
chat
.
title
}}
</span>
<i
class=
"icon-more"
@
click
.
stop=
"showChatMenu(chat.id)"
></i>
</div>
</div>
</div>
<!-- 状态指示 -->
<div
class=
"sidebar-footer"
>
<div
class=
"status-indicator"
:class=
"
{ connected: isApiConfigured }">
<span>
{{
isApiConfigured
?
'✅ 模型已选择'
:
'❌ 未选择模型'
}}
</span>
</div>
</div>
</div>
<!-- 右侧聊天区域 -->
<div
class=
"chat-main"
>
<!-- 聊天区域头部 -->
<div
class=
"chat-header"
>
<div
class=
"chat-title-section"
>
<span
class=
"chat-title"
>
{{
currentChat
?.
title
||
'DeepSeek AI助手'
}}
</span>
<span
class=
"chat-model"
>
DeepSeek Chat
</span>
</div>
<div
class=
"chat-actions"
>
<span
v-if=
"lastThinkingTime"
class=
"token-count"
>
上次思考用时
{{
lastThinkingTime
}}
秒
</span>
<i
class=
"icon-more"
></i>
</div>
</div>
<!-- 聊天内容区域 -->
<div
v-if=
"!showStartPage"
ref=
"chatContentRef"
class=
"chat-content"
>
<template
v-for=
"msg in messages"
:key=
"`$
{msg.id}-${msg.phase}-${Date.now()}`">
<!-- 用户消息 -->
<div
v-if=
"msg.from === 'user'"
class=
"message user-message"
>
<div
class=
"message-content"
>
{{
msg
.
content
}}
</div>
<div
class=
"message-avatar"
>
👤
</div>
</div>
<!-- AI消息 - 统一样式:思考过程 + 最终答案 -->
<div
v-else
class=
"message ai-message unified-response"
>
<div
class=
"message-avatar"
>
🤖
</div>
<div
class=
"message-content"
>
<!-- 顶部:深度思考状态 -->
<div
v-if=
"msg.thinkingTime"
class=
"thinking-status-header"
>
🧠 已深度思考(用时
{{
msg
.
thinkingTime
}}
秒)
</div>
<div
v-else-if=
"msg.phase === 'thinking'"
class=
"thinking-status-header thinking-active"
>
🧠 正在深度思考...
</div>
<!-- 统一使用 MarkdownCard 展示思考过程和答案 -->
<div
class=
"unified-markdown-section"
>
<!-- 使用 McMarkdownCard 统一渲染 -->
<div
class=
"unified-content markdown-content"
v-html=
"renderMarkdownWithMath(getFormattedContent(msg))"
></div>
</div>
<!-- 底部:操作按钮 -->
<div
v-if=
"(msg.phase === 'completed' || msg.phase === 'answering') && msg.content"
class=
"message-actions"
>
<i
class=
"icon-copy"
title=
"复制"
@
click=
"copyMessage(msg.content)"
></i>
<i
class=
"icon-refresh"
title=
"重新生成"
@
click=
"regenerateMessage(msg)"
></i>
<i
class=
"icon-like"
title=
"点赞"
></i>
<i
class=
"icon-dislike"
title=
"点踩"
></i>
</div>
</div>
</div>
</
template
>
<!-- AI思考状态 -->
<div
v-if=
"isAiThinking"
class=
"message ai-message thinking-message"
>
<div
class=
"message-avatar"
>
🤖
</div>
<div
class=
"message-content"
>
<div
class=
"thinking-animation"
>
<div
class=
"thinking-dots"
>
<span></span>
<span></span>
<span></span>
</div>
<div
class=
"thinking-text"
>
正在思考中...({{ thinkingTime }}秒)
</div>
</div>
</div>
</div>
</div>
<!-- 启动页 -->
<div
v-else
class=
"start-page"
>
<div
class=
"welcome-content"
>
<h2>
🤖 DeepSeek AI助手
</h2>
<p>
基于DeepSeek强大AI模型的智能对话助手
</p>
<div
class=
"features"
>
<div
class=
"feature-item"
>
✨ 深度思考过程展示
</div>
<div
class=
"feature-item"
>
💻 专业代码生成
</div>
<div
class=
"feature-item"
>
📝 Markdown格式化回复
</div>
<div
class=
"feature-item"
>
🧮 数学公式渲染支持
</div>
</div>
<div
v-if=
"!isApiConfigured"
class=
"config-tip"
>
<p>
⚠️ 请先在左侧选择AI模型
</p>
</div>
</div>
</div>
<!-- 输入区域 -->
<div
class=
"input-section"
>
<div
class=
"input-container"
>
<input
v-model=
"inputValue"
placeholder=
"输入您的问题... (Enter发送, Shift+Enter换行)"
:disabled=
"!isApiConfigured || isAiThinking"
class=
"chat-input"
@
keydown=
"handleKeyDown"
/>
<button
:disabled=
"!inputValue.trim() || !isApiConfigured || isAiThinking"
class=
"send-btn"
@
click=
"onSubmit"
>
发送
</button>
</div>
<div
class=
"input-footer"
>
<span
class=
"input-counter"
>
{{ inputValue.length }}/2000
</span>
<div
class=
"input-actions"
>
<i
class=
"icon-paperclip"
title=
"附件"
></i>
<i
class=
"icon-mic"
title=
"语音"
></i>
</div>
</div>
</div>
</div>
</div>
</template>
<
style
scoped
lang=
"scss"
>
.n-config-provider
{
height
:
100%
!
important
;
}
.chat-container
{
display
:
flex
;
height
:
90vh
!
important
;
background
:
#f5f5f5
;
.sidebar
{
width
:
300px
;
background
:
#ffffff
;
border-right
:
1px
solid
#ddd
;
display
:
flex
;
flex-direction
:
column
;
.sidebar-header
{
padding
:
16px
;
border-bottom
:
1px
solid
#eee
;
display
:
flex
;
justify-content
:
space-between
;
align-items
:
center
;
.logo-section
{
display
:
flex
;
align-items
:
center
;
gap
:
8px
;
.logo
{
width
:
32px
;
height
:
32px
;
border-radius
:
50%
;
background
:
#5e7ce0
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
color
:
white
;
}
.title
{
font-weight
:
500
;
color
:
#252b3a
;
}
}
.header-actions
{
display
:
flex
;
gap
:
8px
;
.action-btn
{
cursor
:
pointer
;
padding
:
4px
;
border-radius
:
4px
;
&
:hover
{
background
:
#f0f0f0
;
}
}
}
}
.config-panel
{
padding
:
16px
;
background
:
#f8f9fa
;
border-bottom
:
1px
solid
#eee
;
.config-item
{
margin-bottom
:
12px
;
label
{
display
:
block
;
margin-bottom
:
4px
;
font-size
:
12px
;
color
:
#666
;
}
input
,
select
{
width
:
100%
;
padding
:
8px
;
border
:
1px
solid
#ddd
;
border-radius
:
4px
;
font-size
:
12px
;
background
:
#ffffff
!
important
;
color
:
#252b3a
!
important
;
&
:focus
{
border-color
:
#5e7ce0
;
outline
:
none
;
}
&
:
:
placeholder
{
color
:
#999
;
}
option
{
background
:
#ffffff
;
color
:
#252b3a
;
}
}
.model-description
{
margin-top
:
6px
;
padding
:
6px
8px
;
background
:
#f0f0f0
;
border-radius
:
4px
;
font-size
:
11px
;
color
:
#666
;
line-height
:
1
.4
;
}
}
.config-actions
{
display
:
flex
;
gap
:
8px
;
button
{
flex
:
1
;
padding
:
6px
12px
;
border
:
none
;
border-radius
:
4px
;
font-size
:
12px
;
cursor
:
pointer
;
&
.save-btn
{
background
:
#5e7ce0
;
color
:
white
;
}
&
.test-btn
{
background
:
#f0f0f0
;
color
:
#333
;
}
&
:disabled
{
opacity
:
0
.5
;
cursor
:
not
-
allowed
;
}
}
}
}
.search-section
{
padding
:
16px
;
.search-input
{
position
:
relative
;
display
:
flex
;
align-items
:
center
;
background
:
#f5f5f5
;
border-radius
:
8px
;
padding
:
8px
12px
;
.icon-search
{
color
:
#999
;
margin-right
:
8px
;
}
input
{
flex
:
1
;
border
:
none
;
background
:
transparent
;
outline
:
none
;
color
:
#252b3a
;
&
:
:
placeholder
{
color
:
#999
;
}
}
}
}
.chat-history
{
flex
:
1
;
overflow-y
:
auto
;
.history-section
{
padding
:
0
16px
;
.section-title
{
padding
:
8px
0
;
font-size
:
12px
;
color
:
#999
;
border-bottom
:
1px
solid
#eee
;
margin-bottom
:
8px
;
}
.chat-item
{
display
:
flex
;
align-items
:
center
;
padding
:
12px
8px
;
border-radius
:
8px
;
cursor
:
pointer
;
margin-bottom
:
4px
;
color
:
#252b3a
;
// 默认黑色字体
&
:hover
{
background
:
#f5f5f5
;
}
&
.active
{
background
:
#e6f3ff
;
color
:
#5e7ce0
;
}
.icon-message
{
margin-right
:
8px
;
color
:
#999
;
// 选中状态下的图标颜色
.chat-item.active
&
{
color
:
#5e7ce0
;
}
}
.chat-title
{
flex
:
1
;
font-size
:
14px
;
white-space
:
nowrap
;
overflow
:
hidden
;
text-overflow
:
ellipsis
;
color
:
inherit
;
// 继承父元素颜色
}
.icon-more
{
opacity
:
0
;
transition
:
opacity
0
.2s
;
color
:
#999
;
// 选中状态下的图标颜色
.chat-item.active
&
{
color
:
#5e7ce0
;
}
}
&
:hover
.icon-more
{
opacity
:
1
;
}
}
}
}
.sidebar-footer
{
padding
:
16px
;
border-top
:
1px
solid
#eee
;
.status-indicator
{
padding
:
8px
;
border-radius
:
6px
;
font-size
:
12px
;
text-align
:
center
;
background
:
#ffd6d6
;
color
:
#d32f2f
;
&
.connected
{
background
:
#d4edda
;
color
:
#155724
;
}
}
}
}
.chat-main
{
flex
:
1
;
display
:
flex
;
flex-direction
:
column
;
background
:
#ffffff
;
.chat-header
{
padding
:
16px
24px
;
border-bottom
:
1px
solid
#eee
;
display
:
flex
;
justify-content
:
space-between
;
align-items
:
center
;
.chat-title-section
{
display
:
flex
;
flex-direction
:
column
;
.chat-title
{
font-size
:
16px
;
font-weight
:
500
;
color
:
#252b3a
;
margin-bottom
:
4px
;
}
.chat-model
{
font-size
:
12px
;
color
:
#999
;
}
}
.chat-actions
{
display
:
flex
;
align-items
:
center
;
gap
:
12px
;
.token-count
{
font-size
:
12px
;
color
:
#999
;
}
.icon-more
{
cursor
:
pointer
;
padding
:
4px
;
border-radius
:
4px
;
&
:hover
{
background
:
#f0f0f0
;
}
}
}
}
.chat-content
{
flex
:
1
;
padding
:
24px
;
overflow-y
:
auto
;
display
:
flex
;
flex-direction
:
column
;
gap
:
16px
;
.message
{
display
:
flex
;
gap
:
12px
;
max-width
:
85%
;
&
.user-message
{
align-self
:
flex-end
;
flex-direction
:
row-reverse
;
.message-content
{
background
:
#5e7ce0
;
color
:
white
;
border-radius
:
18px
4px
18px
18px
;
}
}
&
.ai-message
{
align-self
:
flex-start
;
.message-content
{
background
:
#f5f5f5
;
color
:
#252b3a
;
border-radius
:
4px
18px
18px
18px
;
}
&
.thinking-message
.message-content
{
background
:
#e8f4fd
;
border
:
1px
solid
#b3d7f0
;
}
}
.message-avatar
{
width
:
32px
;
height
:
32px
;
border-radius
:
50%
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
font-size
:
16px
;
flex-shrink
:
0
;
margin-top
:
4px
;
}
.message-content
{
padding
:
12px
16px
;
word-wrap
:
break-word
;
position
:
relative
;
}
}
}
.start-page
{
flex
:
1
;
display
:
flex
;
flex-direction
:
column
;
align-items
:
center
;
justify-content
:
center
;
padding
:
40px
;
text-align
:
center
;
.welcome-content
{
h2
{
color
:
#252b3a
;
margin-bottom
:
16px
;
}
p
{
color
:
#666
;
margin-bottom
:
24px
;
font-size
:
16px
;
}
.features
{
display
:
flex
;
flex-direction
:
column
;
gap
:
8px
;
margin-bottom
:
24px
;
.feature-item
{
color
:
#5e7ce0
;
font-size
:
14px
;
}
}
.config-tip
{
p
{
color
:
#d32f2f
;
font-weight
:
500
;
}
}
.math-demo
{
margin
:
24px
0
;
padding
:
20px
;
background
:
#f8f9fa
;
border-radius
:
12px
;
border
:
1px
solid
#e9ecef
;
h3
{
color
:
#252b3a
;
margin-bottom
:
16px
;
font-size
:
16px
;
font-weight
:
600
;
}
.math-demo-card
{
background
:
white
;
border-radius
:
8px
;
box-shadow
:
0
1px
3px
rgba
(
0
,
0
,
0
,
0
.1
);
padding
:
16px
;
}
}
}
}
.input-section
{
padding
:
16px
24px
;
border-top
:
1px
solid
#eee
;
background
:
#ffffff
!
important
;
.input-container
{
display
:
flex
;
gap
:
12px
;
margin-bottom
:
8px
;
background
:
#ffffff
!
important
;
.chat-input
{
flex
:
1
;
padding
:
12px
16px
;
border
:
1px
solid
#ddd
;
border-radius
:
24px
;
outline
:
none
;
font-size
:
14px
;
background
:
#ffffff
!
important
;
color
:
#252b3a
!
important
;
&
:focus
{
border-color
:
#5e7ce0
;
background
:
#ffffff
!
important
;
color
:
#252b3a
!
important
;
}
&
:disabled
{
background
:
#f5f5f5
!
important
;
color
:
#666
!
important
;
cursor
:
not
-
allowed
;
}
&
:
:
placeholder
{
color
:
#999
!
important
;
}
}
.send-btn
{
padding
:
12px
24px
;
background
:
#5e7ce0
;
color
:
white
;
border
:
none
;
border-radius
:
24px
;
cursor
:
pointer
;
font-size
:
14px
;
&
:hover
{
background
:
#4c6ed7
;
}
&
:disabled
{
background
:
#ccc
;
cursor
:
not
-
allowed
;
}
}
}
.input-footer
{
display
:
flex
;
justify-content
:
space-between
;
align-items
:
center
;
padding
:
0
8px
;
.input-counter
{
font-size
:
12px
;
color
:
#999
;
}
.input-actions
{
display
:
flex
;
gap
:
12px
;
i
{
cursor
:
pointer
;
color
:
#666
;
padding
:
4px
;
border-radius
:
4px
;
&
:hover
{
background
:
#f0f0f0
;
}
}
}
}
}
}
}
// 图标样式
.
icon-user
:
:
before
{
content
:
'👤'
;
}
.
icon-add
:
:
before
{
content
:
'➕'
;
}
.
icon-settings
:
:
before
{
content
:
'⚙️'
;
}
.
icon-search
:
:
before
{
content
:
'🔍'
;
}
.
icon-message
:
:
before
{
content
:
'💬'
;
}
.
icon-more
:
:
before
{
content
:
'⋯'
;
}
.
icon-copy
:
:
before
{
content
:
'📋'
;
}
.
icon-refresh
:
:
before
{
content
:
'🔄'
;
}
.
icon-like
:
:
before
{
content
:
'👍'
;
}
.
icon-dislike
:
:
before
{
content
:
'👎'
;
}
.
icon-paperclip
:
:
before
{
content
:
'📎'
;
}
.
icon-mic
:
:
before
{
content
:
'🎤'
;
}
.
icon-brain
:
:
before
{
content
:
'🧠'
;
}
// 思考状态样式
.thinking-header
{
display
:
flex
;
align-items
:
center
;
gap
:
8px
;
margin-bottom
:
8px
;
padding
:
6px
12px
;
background
:
linear-gradient
(
135deg
,
#667eea
0%
,
#764ba2
100%
);
color
:
white
;
border-radius
:
6px
;
font-size
:
11px
;
font-weight
:
500
;
&
.completed
{
background
:
linear-gradient
(
135deg
,
#4caf50
0%
,
#45a049
100%
);
}
}
.thinking-process-content
{
background
:
#f8f9fa
;
border
:
1px
solid
#e9ecef
;
border-radius
:
8px
;
padding
:
12px
;
font-family
:
'Monaco'
,
'Menlo'
,
'Ubuntu Mono'
,
monospace
;
font-size
:
13px
;
line-height
:
1
.4
;
color
:
#333
;
white-space
:
pre-wrap
;
min-height
:
40px
;
.thinking-cursor
{
color
:
#5e7ce0
;
animation
:
blink
1s
infinite
;
}
}
@keyframes
blink
{
0
%
,
50
%
{
opacity
:
1
;
}
51
%
,
100
%
{
opacity
:
0
;
}
}
.thinking-animation
{
display
:
flex
;
flex-direction
:
column
;
align-items
:
center
;
gap
:
12px
;
padding
:
12px
;
.thinking-dots
{
display
:
flex
;
gap
:
6px
;
span
{
width
:
8px
;
height
:
8px
;
background
:
#5e7ce0
;
border-radius
:
50%
;
animation
:
thinking-dot
1
.4s
ease-in-out
infinite
both
;
&
:nth-child
(
1
)
{
animation-delay
:
-0
.32s
;
}
&
:nth-child
(
2
)
{
animation-delay
:
-0
.16s
;
}
&
:nth-child
(
3
)
{
animation-delay
:
0s
;
}
}
}
.thinking-text
{
color
:
#666
;
font-size
:
13px
;
text-align
:
center
;
}
}
@keyframes
thinking-dot
{
0
%
,
80
%
,
100
%
{
transform
:
scale
(
0
.6
);
opacity
:
0
.4
;
}
40
%
{
transform
:
scale
(
1
);
opacity
:
1
;
}
}
// AI消息统一样式 - 匹配截图效果
.ai-message.unified-response
{
.message-content
{
background
:
#f8f9fa
;
border-radius
:
12px
;
padding
:
16px
;
margin
:
12px
0
;
border
:
1px
solid
#e3e3e3
;
}
// 统一内容区域
.unified-content
{
margin
:
0
;
}
// 顶部思考状态头
.thinking-status-header
{
color
:
#6b7280
;
font-size
:
13px
;
margin-bottom
:
12px
;
padding
:
6px
0
;
border-bottom
:
1px
solid
#e5e7eb
;
&
.thinking-active
{
color
:
#3b82f6
;
animation
:
pulse
2s
infinite
;
}
}
// 中间思考过程区域
.thinking-process-section
{
margin
:
12px
0
;
.thinking-content
{
background
:
transparent
;
border
:
none
;
padding
:
0
;
font-family
:
-
apple-system
,
BlinkMacSystemFont
,
'Segoe UI'
,
Roboto
,
sans-serif
;
font-size
:
14px
;
line-height
:
1
.6
;
color
:
#4b5563
;
white-space
:
pre-wrap
;
margin-bottom
:
16px
;
.thinking-cursor
{
color
:
#3b82f6
;
animation
:
blink
1s
infinite
;
}
}
}
// 底部最终答案区域(加粗)
.final-answer-section
{
margin-top
:
16px
;
padding-top
:
12px
;
.final-answer
{
font-weight
:
600
;
font-size
:
14px
;
line-height
:
1
.6
;
color
:
#111827
;
// 加粗markdown内容
p
{
font-weight
:
600
;
margin
:
8px
0
;
}
h1
,
h2
,
h3
,
h4
,
h5
,
h6
{
font-weight
:
700
;
}
strong
,
b
{
font-weight
:
700
;
}
}
}
// 操作按钮
.message-actions
{
display
:
flex
;
gap
:
12px
;
margin-top
:
16px
;
padding-top
:
12px
;
border-top
:
1px
solid
#e5e7eb
;
opacity
:
0
;
transition
:
opacity
0
.2s
ease
;
i
{
cursor
:
pointer
;
color
:
#6b7280
;
padding
:
6px
;
border-radius
:
6px
;
font-size
:
16px
;
transition
:
all
0
.2s
ease
;
&
:hover
{
background
:
#f3f4f6
;
color
:
#374151
;
transform
:
scale
(
1
.1
);
}
}
}
&
:hover
.message-actions
{
opacity
:
1
;
}
}
// 脉搏动画
@keyframes
pulse
{
0
%
,
100
%
{
opacity
:
1
;
}
50
%
{
opacity
:
0
.6
;
}
}
// Markdown 内容样式
.markdown-content
{
line-height
:
1
.6
;
color
:
#252b3a
;
h1
,
h2
,
h3
,
h4
,
h5
,
h6
{
margin
:
16px
0
8px
0
;
color
:
#252b3a
;
font-weight
:
600
;
}
h1
{
font-size
:
20px
;
}
h2
{
font-size
:
18px
;
}
h3
{
font-size
:
16px
;
}
h4
{
font-size
:
14px
;
}
p
{
margin
:
8px
0
;
}
ul
,
ol
{
margin
:
8px
0
;
padding-left
:
20px
;
}
li
{
margin
:
4px
0
;
}
blockquote
{
margin
:
12px
0
;
padding
:
8px
12px
;
background
:
#f8f9fa
;
border-left
:
3px
solid
#5e7ce0
;
border-radius
:
4px
;
color
:
#666
;
font-style
:
italic
;
}
code
{
background
:
#f1f3f4
;
padding
:
2px
6px
;
border-radius
:
4px
;
font-family
:
'Monaco'
,
'Menlo'
,
'Ubuntu Mono'
,
monospace
;
font-size
:
12px
;
color
:
#e83e8c
;
}
pre
{
background
:
#1e1e1e
;
color
:
#d4d4d4
;
padding
:
12px
;
border-radius
:
6px
;
overflow-x
:
auto
;
margin
:
12px
0
;
code
{
background
:
none
;
padding
:
0
;
color
:
#d4d4d4
;
font-size
:
13px
;
}
}
strong
{
font-weight
:
600
;
color
:
#252b3a
;
}
em
{
font-style
:
italic
;
}
a
{
color
:
#5e7ce0
;
text-decoration
:
none
;
&
:hover
{
text-decoration
:
underline
;
}
}
// KaTeX 数学公式样式
.katex
{
font-size
:
1
.1em
;
}
.katex-display
{
margin
:
16px
0
;
text-align
:
center
;
}
.math-display
{
margin
:
16px
0
;
text-align
:
center
;
overflow-x
:
auto
;
}
// 思考过程样式
think
{
display
:
block
;
background
:
#f0f8ff
;
border-left
:
4px
solid
#5e7ce0
;
padding
:
12px
16px
;
margin
:
16px
0
;
border-radius
:
0
8px
8px
0
;
font-style
:
italic
;
color
:
#4a6cf7
;
white-space
:
pre-wrap
;
}
// 数学公式错误样式
.math-error
{
color
:
#cc0000
;
background
:
#ffe6e6
;
padding
:
4px
8px
;
border-radius
:
4px
;
font-family
:
monospace
;
border
:
1px
solid
#ffcccc
;
}
}
.text-content
{
line-height
:
1
.6
;
color
:
#252b3a
;
}
.message-actions
{
display
:
flex
;
gap
:
8px
;
margin-top
:
8px
;
opacity
:
0
;
transition
:
opacity
0
.2s
;
i
{
cursor
:
pointer
;
color
:
#666
;
padding
:
4px
;
border-radius
:
4px
;
font-size
:
12px
;
&
:hover
{
background
:
#e0e0e0
;
}
}
}
.message
:hover
.message-actions
{
opacity
:
1
;
}
// 全局输入框样式重写,确保所有输入元素都有正确的颜色
.chat-container
{
input
{
background
:
#ffffff
!
important
;
color
:
#252b3a
!
important
;
&
:
:
placeholder
{
color
:
#999
!
important
;
}
&
:focus
{
background
:
#ffffff
!
important
;
color
:
#252b3a
!
important
;
}
&
:disabled
{
background
:
#f5f5f5
!
important
;
color
:
#666
!
important
;
}
}
select
{
background
:
#ffffff
!
important
;
color
:
#252b3a
!
important
;
option
{
background
:
#ffffff
!
important
;
color
:
#252b3a
!
important
;
}
}
.input-section
{
background
:
#ffffff
!
important
;
.input-container
{
background
:
#ffffff
!
important
;
}
}
}
</
style
>
src/views/chat/index.vue
View file @
70990881
<
script
setup
lang=
"ts"
>
<
script
setup
lang=
"ts"
>
import
{
computed
,
nextTick
,
onActivated
,
onBeforeUnmount
,
onDeactivated
,
onMounted
,
onUnmounted
,
ref
}
from
'vue'
;
import
{
computed
,
onActivated
,
onDeactivated
,
onMounted
,
ref
}
from
'vue'
;
import
{
NConfigProvider
,
darkTheme
}
from
'naive-ui'
;
import
{
useAppStore
}
from
'@/store/modules/app'
;
import
{
useThemeStore
}
from
'@/store/modules/theme'
;
import
IframePage
from
'@/views/_builtin/iframe-page/[url].vue'
;
import
{
type
ModelOption
,
deepSeekService
}
from
'@/service/api/deepseek'
;
import
{
renderMarkdownWithMath
as
renderContentWithMath
}
from
'@/utils/katex-renderer'
;
defineOptions
({
name
:
'ChatPage'
});
const
appStore
=
useAppStore
();
// 控制组件显示状态的变量
interface
ChatMessage
{
id
:
string
;
from
:
'user'
|
'ai'
;
content
:
string
;
timestamp
:
number
;
thinkingTime
?:
number
;
contentType
?:
'text'
|
'markdown'
;
thinkingContent
?:
string
;
isShowingThinking
?:
boolean
;
currentThinkingText
?:
string
;
displayContent
?:
string
;
// 用于渐进显示内容
isTyping
?:
boolean
;
// 是否正在打字
phase
?:
'thinking'
|
'answering'
|
'completed'
;
// 当前阶段
}
interface
ChatHistory
{
id
:
string
;
title
:
string
;
lastMessage
:
string
;
timestamp
:
number
;
messages
:
ChatMessage
[];
}
// 页面激活状态管理
const
isActive
=
ref
(
true
);
const
isActive
=
ref
(
true
);
const
containerId
=
`chat-
${
Math
.
random
().
toString
(
36
).
substr
(
2
,
9
)}
`
;
const
extjsContainerId
=
`menuRoot-
${
Math
.
random
().
toString
(
36
).
substr
(
2
,
9
)}
`
;
// 主题相关
const
themeStore
=
useThemeStore
();
const
naiveDarkTheme
=
computed
(()
=>
(
themeStore
.
darkMode
?
darkTheme
:
undefined
));
// 响应式数据
const
searchText
=
ref
(
''
);
const
inputValue
=
ref
(
''
);
const
currentChatId
=
ref
(
'1'
);
const
showStartPage
=
ref
(
true
);
const
messages
=
ref
<
ChatMessage
[]
>
([]);
const
isAiThinking
=
ref
(
false
);
const
thinkingStartTime
=
ref
(
0
);
const
thinkingTime
=
ref
(
0
);
const
thinkingInterval
=
ref
<
NodeJS
.
Timeout
|
null
>
(
null
);
const
lastThinkingTime
=
ref
(
0
);
// 配置相关
const
showConfig
=
ref
(
false
);
const
selectedModelId
=
ref
(
'deepseek'
);
const
testing
=
ref
(
false
);
const
modelOptions
=
ref
<
ModelOption
[]
>
([]);
// 聊天历史数据
const
chatHistory
=
ref
<
ChatHistory
[]
>
([
{
id
:
'1'
,
title
:
'新对话'
,
lastMessage
:
''
,
timestamp
:
Date
.
now
(),
messages
:
[]
}
]);
// 计算属性
// 设置默认的自动启动项,直接加载chat-component.vue
const
filteredChatHistory
=
computed
(()
=>
{
const
autoStartProps
=
ref
({
if
(
!
searchText
.
value
)
return
chatHistory
.
value
;
type
:
'System'
,
return
chatHistory
.
value
.
filter
(
chat
=>
chat
.
title
.
toLowerCase
().
includes
(
searchText
.
value
.
toLowerCase
()));
kvid
:
'chat-default'
,
url
:
'chat-component.vue'
});
});
const
currentChat
=
computed
(()
=>
{
// 添加 extjs-root 就绪状态
return
chatHistory
.
value
.
find
(
chat
=>
chat
.
id
===
currentChatId
.
value
);
const
isExtjsRootReady
=
ref
(
false
);
});
const
isApiConfigured
=
computed
(()
=>
{
return
selectedModelId
.
value
!==
''
;
});
const
currentModelName
=
computed
(()
=>
{
// 检查 extjs-root 是否存在
if
(
!
selectedModelId
.
value
)
return
'AI助手'
;
const
checkExtjsRoot
=
()
=>
{
const
currentModel
=
modelOptions
.
value
.
find
(
model
=>
model
.
id
===
selectedModelId
.
value
);
const
extjsRoot
=
document
.
getElementById
(
'extjs-root'
);
return
currentModel
?.
name
||
'AI助手'
;
isExtjsRootReady
.
value
=
Boolean
(
extjsRoot
);
});
if
(
!
extjsRoot
)
{
setTimeout
(
checkExtjsRoot
,
100
);
// 继续检查
// 聊天容器引用
const
chatContentRef
=
ref
<
HTMLElement
|
null
>
(
null
);
// 清理函数
function
cleanup
()
{
try
{
// 清理定时器
if
(
thinkingInterval
.
value
)
{
clearInterval
(
thinkingInterval
.
value
);
thinkingInterval
.
value
=
null
;
}
// 清空状态
isAiThinking
.
value
=
false
;
thinkingTime
.
value
=
0
;
// 安全地清空容器,避免DOM操作冲突
nextTick
(()
=>
{
try
{
const
container
=
document
.
getElementById
(
containerId
);
if
(
container
&&
container
.
parentNode
)
{
// 检查容器是否仍然在DOM中
container
.
innerHTML
=
''
;
}
}
catch
(
domError
)
{
// 忽略DOM操作错误,因为容器可能已被移除
console
.
warn
(
'清理容器时出现DOM错误,可能容器已被移除:'
,
domError
);
}
});
}
catch
(
e
)
{
console
.
error
(
'清理聊天组件时出错:'
,
e
);
}
}
// 暴露清理方法给父组件
defineExpose
({
cleanup
});
// 滚动到底部
const
scrollToBottom
=
()
=>
{
if
(
chatContentRef
.
value
)
{
setTimeout
(()
=>
{
if
(
chatContentRef
.
value
)
{
chatContentRef
.
value
.
scrollTop
=
chatContentRef
.
value
.
scrollHeight
;
}
},
50
);
}
}
};
};
const
newChat
=
()
=>
{
const
newChatId
=
`chat_
${
Date
.
now
()}
`
;
const
newChatItem
:
ChatHistory
=
{
id
:
newChatId
,
title
:
'新对话'
,
lastMessage
:
''
,
timestamp
:
Date
.
now
(),
messages
:
[]
};
chatHistory
.
value
.
unshift
(
newChatItem
);
currentChatId
.
value
=
newChatId
;
showStartPage
.
value
=
true
;
messages
.
value
=
[];
inputValue
.
value
=
''
;
};
const
switchChat
=
(
chatId
:
string
)
=>
{
const
chat
=
chatHistory
.
value
.
find
(
c
=>
c
.
id
===
chatId
);
if
(
chat
)
{
currentChatId
.
value
=
chatId
;
messages
.
value
=
[...
chat
.
messages
];
showStartPage
.
value
=
chat
.
messages
.
length
===
0
;
}
};
// 配置相关方法
const
onModelChange
=
()
=>
{
if
(
selectedModelId
.
value
)
{
deepSeekService
.
switchModel
(
selectedModelId
.
value
);
}
};
const
saveConfig
=
()
=>
{
localStorage
.
setItem
(
'deepseek-selected-model'
,
selectedModelId
.
value
);
deepSeekService
.
switchModel
(
selectedModelId
.
value
);
showConfig
.
value
=
false
;
// alert('配置已保存!');
};
const
testConnection
=
async
()
=>
{
testing
.
value
=
true
;
try
{
deepSeekService
.
switchModel
(
selectedModelId
.
value
);
await
deepSeekService
.
chat
([{
role
:
'user'
,
content
:
'Hello'
}],
{
maxTokens
:
10
});
// alert('连接测试成功!');
}
catch
(
error
)
{
// alert(`连接测试失败: ${error instanceof Error ? error.message : '未知错误'}`);
}
finally
{
testing
.
value
=
false
;
}
};
// 思考相关方法
const
startThinking
=
()
=>
{
isAiThinking
.
value
=
true
;
thinkingStartTime
.
value
=
Date
.
now
();
thinkingTime
.
value
=
0
;
thinkingInterval
.
value
=
setInterval
(()
=>
{
thinkingTime
.
value
=
Math
.
floor
((
Date
.
now
()
-
thinkingStartTime
.
value
)
/
1000
);
},
1000
);
};
const
stopThinking
=
()
=>
{
isAiThinking
.
value
=
false
;
if
(
thinkingInterval
.
value
)
{
clearInterval
(
thinkingInterval
.
value
);
thinkingInterval
.
value
=
null
;
}
};
// 模拟思考过程显示
const
showThinkingProcess
=
(
message
:
ChatMessage
,
content
:
string
)
=>
{
let
currentIndex
=
0
;
message
.
currentThinkingText
=
''
;
message
.
isShowingThinking
=
true
;
const
typeNextChar
=
()
=>
{
if
(
currentIndex
<
content
.
length
)
{
message
.
currentThinkingText
+=
content
[
currentIndex
];
currentIndex
+=
1
;
setTimeout
(
typeNextChar
,
15
);
// 快速显示思考过程
}
else
{
message
.
isShowingThinking
=
false
;
}
};
typeNextChar
();
};
// 更新聊天历史
const
updateChatHistory
=
()
=>
{
const
currentChatItem
=
chatHistory
.
value
.
find
(
chat
=>
chat
.
id
===
currentChatId
.
value
);
if
(
currentChatItem
)
{
currentChatItem
.
messages
=
[...
messages
.
value
];
currentChatItem
.
lastMessage
=
messages
.
value
[
messages
.
value
.
length
-
1
]?.
content
||
''
;
currentChatItem
.
timestamp
=
Date
.
now
();
// 如果是第一条消息,更新聊天标题
if
(
currentChatItem
.
title
===
'新对话'
&&
messages
.
value
.
length
>
0
)
{
const
firstUserMessage
=
messages
.
value
.
find
(
msg
=>
msg
.
from
===
'user'
);
if
(
firstUserMessage
)
{
currentChatItem
.
title
=
firstUserMessage
.
content
.
slice
(
0
,
20
)
+
(
firstUserMessage
.
content
.
length
>
20
?
'...'
:
''
);
}
}
}
};
// 混合方案:思考过程 + 真正流式输出
const
onSubmit
=
async
()
=>
{
if
(
!
inputValue
.
value
.
trim
()
||
!
isApiConfigured
.
value
)
return
;
showStartPage
.
value
=
false
;
// 添加用户消息
const
userMessage
:
ChatMessage
=
{
id
:
`msg_
${
Date
.
now
()}
`
,
from
:
'user'
,
content
:
inputValue
.
value
,
timestamp
:
Date
.
now
(),
contentType
:
'text'
};
messages
.
value
.
push
(
userMessage
);
scrollToBottom
();
const
question
=
inputValue
.
value
;
inputValue
.
value
=
''
;
// 开始思考状态
startThinking
();
// 创建AI消息,初始为思考阶段
const
aiMessage
:
ChatMessage
=
{
id
:
`msg_ai_
${
Date
.
now
()}
`
,
from
:
'ai'
,
content
:
''
,
timestamp
:
Date
.
now
(),
contentType
:
'markdown'
,
phase
:
'thinking'
,
isShowingThinking
:
true
,
currentThinkingText
:
''
,
displayContent
:
''
,
isTyping
:
false
};
messages
.
value
.
push
(
aiMessage
);
scrollToBottom
();
// 显示思考过程
const
thinkingContent
=
`分析用户问题:
${
question
}
让我仔细思考这个问题的关键要素和可能的解答方向...
首先需要理解问题的核心内容和用户的真实需求。
接下来我会从多个角度来分析这个问题:
1. 基础概念和定义
2. 相关背景信息
3. 可能的解决方案
4. 具体的建议和步骤
正在整理相关信息和最佳回答方案...`
;
showThinkingProcess
(
aiMessage
,
thinkingContent
);
try
{
// 等待思考过程显示一段时间后开始流式输出
setTimeout
(
async
()
=>
{
stopThinking
();
aiMessage
.
thinkingTime
=
Math
.
floor
((
Date
.
now
()
-
thinkingStartTime
.
value
)
/
1000
);
// 切换到答案阶段
aiMessage
.
phase
=
'answering'
;
aiMessage
.
isShowingThinking
=
false
;
aiMessage
.
isTyping
=
true
;
// console.log('🚀 开始流式输出答案...');
// 更详细的流式日志和强制刷新
await
deepSeekService
.
streamChat
(
[{
role
:
'user'
,
content
:
question
}],
// onChunk: 每收到一个数据块立即更新
async
(
chunk
:
string
)
=>
{
// console.log(`📦 [${chunkCount}] 收到数据块:`, JSON.stringify(chunk));
// 累加内容
aiMessage
.
content
+=
chunk
;
// 找到消息在数组中的位置并完全替换
const
messageIndex
=
messages
.
value
.
findIndex
(
m
=>
m
.
id
===
aiMessage
.
id
);
if
(
messageIndex
!==
-
1
)
{
// 创建新的消息对象强制触发响应式更新
messages
.
value
[
messageIndex
]
=
{
...
aiMessage
,
content
:
aiMessage
.
content
,
displayContent
:
aiMessage
.
content
};
scrollToBottom
();
}
// console.log(
// `📝 更新后总长度: ${aiMessage.content.length}, 显示长度: ${aiMessage.displayContent || aiMessage.content.length}`
// );
},
// onComplete: 流式传输完成
()
=>
{
aiMessage
.
isTyping
=
false
;
aiMessage
.
phase
=
'completed'
;
lastThinkingTime
.
value
=
aiMessage
.
thinkingTime
||
0
;
// console.log(`✅ 流式传输完成,共收到 ${chunkCount} 个数据块,最终内容长度: ${aiMessage.content.length}`);
updateChatHistory
();
},
// onError: 处理错误
(
error
:
Error
)
=>
{
// console.error('❌ 流式传输错误:', error);
aiMessage
.
content
=
`抱歉,AI回复失败:
${
error
.
message
}
`
;
aiMessage
.
displayContent
=
aiMessage
.
content
;
aiMessage
.
isTyping
=
false
;
aiMessage
.
phase
=
'completed'
;
}
);
},
3000
);
// 3秒思考时间
}
catch
(
error
)
{
stopThinking
();
// console.error('AI回复失败:', error);
aiMessage
.
content
=
`抱歉,AI回复失败:
${
error
instanceof
Error
?
error
.
message
:
'未知错误'
}
`
;
aiMessage
.
isTyping
=
false
;
aiMessage
.
phase
=
'completed'
;
}
};
// 其他功能方法
const
handleKeyDown
=
(
event
:
KeyboardEvent
)
=>
{
if
(
event
.
key
===
'Enter'
&&
!
event
.
shiftKey
)
{
event
.
preventDefault
();
onSubmit
();
}
};
const
copyMessage
=
async
(
content
:
string
)
=>
{
try
{
await
navigator
.
clipboard
.
writeText
(
content
);
// alert('已复制到剪贴板');
}
catch
(
error
)
{
// console.error('复制失败:', error);
}
};
// 格式化内容,将思考过程和回答内容组合
const
getFormattedContent
=
(
message
:
ChatMessage
):
string
=>
{
let
content
=
''
;
// 如果有思考过程就直接显示
if
(
message
.
currentThinkingText
)
{
content
+=
`<think>
${
message
.
currentThinkingText
}
</think>
`
;
}
// 添加最终答案内容
if
(
message
.
content
&&
(
message
.
phase
===
'completed'
||
message
.
phase
===
'answering'
))
{
content
+=
message
.
displayContent
||
message
.
content
;
}
return
content
;
};
// 使用自定义 markdown 渲染器渲染内容
const
renderMarkdownWithMath
=
(
content
:
string
):
string
=>
{
try
{
const
result
=
renderContentWithMath
(
content
);
console
.
log
(
'Markdown 渲染结果:'
,
result
);
return
result
;
}
catch
(
error
)
{
console
.
error
(
'Markdown 渲染错误:'
,
error
);
return
content
;
// 返回原内容作为fallback
}
};
const
regenerateMessage
=
async
(
message
:
ChatMessage
)
=>
{
// 找到对应的用户消息
const
messageIndex
=
messages
.
value
.
findIndex
(
msg
=>
msg
.
id
===
message
.
id
);
if
(
messageIndex
>
0
)
{
const
userMessage
=
messages
.
value
[
messageIndex
-
1
];
if
(
userMessage
.
from
===
'user'
)
{
// 删除AI消息,重新生成
messages
.
value
.
splice
(
messageIndex
,
1
);
inputValue
.
value
=
userMessage
.
content
;
onSubmit
();
}
}
};
const
showChatMenu
=
(
_chatId
:
string
)
=>
{
// 实现聊天菜单功能(删除、重命名等)
// console.log('显示聊天菜单', _chatId);
};
// 检查Teleport目标容器是否存在
const
checkTeleportTarget
=
()
=>
{
const
target
=
document
.
getElementById
(
'extjs-root'
);
if
(
!
target
)
{
console
.
warn
(
'Teleport目标容器 #extjs-root 不存在,正在创建...'
);
const
container
=
document
.
createElement
(
'div'
);
container
.
id
=
'extjs-root'
;
document
.
body
.
appendChild
(
container
);
}
};
// 生命周期
onMounted
(()
=>
{
onMounted
(()
=>
{
// 确保Teleport目标容器存在
checkExtjsRoot
();
checkTeleportTarget
();
isActive
.
value
=
true
;
isActive
.
value
=
true
;
// 加载可用模型选项
modelOptions
.
value
=
deepSeekService
.
getModelOptions
();
// 加载保存的模型选择
const
savedModel
=
localStorage
.
getItem
(
'deepseek-selected-model'
);
if
(
savedModel
)
{
selectedModelId
.
value
=
savedModel
;
deepSeekService
.
switchModel
(
savedModel
);
}
});
});
onActivated
(()
=>
{
onActivated
(()
=>
{
...
@@ -476,1211 +38,28 @@ onActivated(() => {
...
@@ -476,1211 +38,28 @@ onActivated(() => {
onDeactivated
(()
=>
{
onDeactivated
(()
=>
{
isActive
.
value
=
false
;
isActive
.
value
=
false
;
// 延迟清理,避免与路由切换冲突
setTimeout
(()
=>
{
if
(
!
isActive
.
value
)
{
cleanup
();
}
},
100
);
});
});
onBeforeUnmount
(()
=>
{
const
gap
=
computed
(()
=>
(
appStore
.
isMobile
?
0
:
16
));
// 立即设置为非激活状态
isActive
.
value
=
false
;
// 清理定时器(同步操作)
if
(
thinkingInterval
.
value
)
{
clearInterval
(
thinkingInterval
.
value
);
thinkingInterval
.
value
=
null
;
}
// 清空状态
isAiThinking
.
value
=
false
;
thinkingTime
.
value
=
0
;
});
onUnmounted
(()
=>
{
// 最后的清理检查
if
(
thinkingInterval
.
value
)
{
clearInterval
(
thinkingInterval
.
value
);
thinkingInterval
.
value
=
null
;
}
});
</
script
>
</
script
>
<
template
>
<
template
>
<Teleport
to=
"#extjs-root"
:disabled=
"!isActive"
>
<NSpace
vertical
:size=
"16"
>
<div
<NGrid
:x-gap=
"gap"
:y-gap=
"16"
responsive=
"screen"
item-responsive
>
v-if=
"isActive"
<NGi
span=
"24 s:24 m:14"
>
:id=
"containerId"
<!--
<ProjectNews
/>
-->
class=
"chat-container-wrapper"
</NGi>
:data-theme=
"themeStore.darkMode ? 'dark' : 'light'"
<NGi
span=
"24 s:24 m:10"
>
>
<!--
<CreativityBanner
/>
-->
<NConfigProvider
:theme=
"naiveDarkTheme"
:theme-overrides=
"themeStore.naiveTheme"
>
</NGi>
<div
class=
"chat-container"
>
</NGrid>
<!-- 左侧侧边栏 -->
<!-- 默认加载 chat-component.vue,通过 IframePage 加载 -->
<div
class=
"sidebar"
>
<template
v-if=
"isExtjsRootReady"
>
<!-- 顶部工具栏 -->
<div
v-show=
"isActive"
:id=
"extjsContainerId"
style=
"width: 100%; height: 100%"
>
<div
class=
"sidebar-header"
>
<IframePage
:type=
"autoStartProps.type"
:kvid=
"autoStartProps.kvid"
:url=
"autoStartProps.url"
/>
<div
class=
"logo-section"
>
<div
class=
"logo"
>
<i
class=
"icon-user"
></i>
</div>
<span
class=
"title"
>
{{
currentModelName
}}
</span>
</div>
<div
class=
"header-actions"
>
<i
class=
"icon-add action-btn"
@
click=
"newChat"
></i>
<i
class=
"icon-settings action-btn"
title=
"API配置"
@
click=
"showConfig = !showConfig"
></i>
</div>
</div>
<!-- 模型配置面板 -->
<div
v-if=
"showConfig"
class=
"config-panel"
>
<div
class=
"config-item"
>
<label>
选择模型:
</label>
<select
v-model=
"selectedModelId"
@
change=
"onModelChange"
>
<option
v-for=
"model in modelOptions"
:key=
"model.id"
:value=
"model.id"
>
{{
model
.
name
}}
</option>
</select>
<div
v-if=
"selectedModelId"
class=
"model-description"
>
{{
modelOptions
.
find
(
m
=>
m
.
id
===
selectedModelId
)?.
description
}}
</div>
</div>
<div
class=
"config-actions"
>
<button
:disabled=
"!selectedModelId"
class=
"save-btn"
@
click=
"saveConfig"
>
保存
</button>
<button
:disabled=
"testing || !selectedModelId"
class=
"test-btn"
@
click=
"testConnection"
>
{{
testing
?
'测试中...'
:
'测试连接'
}}
</button>
</div>
</div>
<!-- 搜索框 -->
<div
class=
"search-section"
>
<div
class=
"search-input"
>
<i
class=
"icon-search"
></i>
<input
v-model=
"searchText"
placeholder=
"搜索对话"
/>
</div>
</div>
<!-- 历史对话列表 -->
<div
class=
"chat-history"
>
<div
class=
"history-section"
>
<div
class=
"section-title"
>
<span>
对话历史
</span>
</div>
<div
v-for=
"chat in filteredChatHistory"
:key=
"chat.id"
class=
"chat-item"
:class=
"
{ active: currentChatId === chat.id }"
@click="switchChat(chat.id)"
>
<i
class=
"icon-message"
></i>
<span
class=
"chat-title"
>
{{
chat
.
title
}}
</span>
<i
class=
"icon-more"
@
click
.
stop=
"showChatMenu(chat.id)"
></i>
</div>
</div>
</div>
<!-- 状态指示 -->
<div
class=
"sidebar-footer"
>
<div
class=
"status-indicator"
:class=
"
{ connected: isApiConfigured }">
<span>
{{
isApiConfigured
?
'✅ 模型已选择'
:
'❌ 未选择模型'
}}
</span>
</div>
</div>
</div>
<!-- 右侧聊天区域 -->
<div
class=
"chat-main"
>
<!-- 聊天区域头部 -->
<div
class=
"chat-header"
>
<div
class=
"chat-title-section"
>
<span
class=
"chat-title"
>
{{
currentChat
?.
title
||
'DeepSeek AI助手'
}}
</span>
<span
class=
"chat-model"
>
DeepSeek Chat
</span>
</div>
<div
class=
"chat-actions"
>
<span
v-if=
"lastThinkingTime"
class=
"token-count"
>
上次思考用时
{{
lastThinkingTime
}}
秒
</span>
<i
class=
"icon-more"
></i>
</div>
</div>
<!-- 聊天内容区域 -->
<div
v-if=
"!showStartPage"
ref=
"chatContentRef"
class=
"chat-content"
>
<template
v-for=
"msg in messages"
:key=
"`$
{msg.id}-${msg.phase}-${Date.now()}`">
<!-- 用户消息 -->
<div
v-if=
"msg.from === 'user'"
class=
"message user-message"
>
<div
class=
"message-content"
>
{{
msg
.
content
}}
</div>
<div
class=
"message-avatar"
>
👤
</div>
</div>
<!-- AI消息 - 统一样式:思考过程 + 最终答案 -->
<div
v-else
class=
"message ai-message unified-response"
>
<div
class=
"message-avatar"
>
🤖
</div>
<div
class=
"message-content"
>
<!-- 顶部:深度思考状态 -->
<div
v-if=
"msg.thinkingTime"
class=
"thinking-status-header"
>
🧠 已深度思考(用时
{{
msg
.
thinkingTime
}}
秒)
</div>
<div
v-else-if=
"msg.phase === 'thinking'"
class=
"thinking-status-header thinking-active"
>
🧠 正在深度思考...
</div>
<!-- 统一使用 MarkdownCard 展示思考过程和答案 -->
<div
class=
"unified-markdown-section"
>
<!-- 使用 McMarkdownCard 统一渲染 -->
<div
class=
"unified-content markdown-content"
v-html=
"renderMarkdownWithMath(getFormattedContent(msg))"
></div>
</div>
<!-- 底部:操作按钮 -->
<div
v-if=
"(msg.phase === 'completed' || msg.phase === 'answering') && msg.content"
class=
"message-actions"
>
<i
class=
"icon-copy"
title=
"复制"
@
click=
"copyMessage(msg.content)"
></i>
<i
class=
"icon-refresh"
title=
"重新生成"
@
click=
"regenerateMessage(msg)"
></i>
<i
class=
"icon-like"
title=
"点赞"
></i>
<i
class=
"icon-dislike"
title=
"点踩"
></i>
</div>
</div>
</div>
</div>
</
template
>
</
template
>
</NSpace>
<!-- AI思考状态 -->
<div
v-if=
"isAiThinking"
class=
"message ai-message thinking-message"
>
<div
class=
"message-avatar"
>
🤖
</div>
<div
class=
"message-content"
>
<div
class=
"thinking-animation"
>
<div
class=
"thinking-dots"
>
<span></span>
<span></span>
<span></span>
</div>
<div
class=
"thinking-text"
>
正在思考中...({{ thinkingTime }}秒)
</div>
</div>
</div>
</div>
</div>
<!-- 启动页 -->
<div
v-else
class=
"start-page"
>
<div
class=
"welcome-content"
>
<h2>
🤖 DeepSeek AI助手
</h2>
<p>
基于DeepSeek强大AI模型的智能对话助手
</p>
<div
class=
"features"
>
<div
class=
"feature-item"
>
✨ 深度思考过程展示
</div>
<div
class=
"feature-item"
>
💻 专业代码生成
</div>
<div
class=
"feature-item"
>
📝 Markdown格式化回复
</div>
<div
class=
"feature-item"
>
🧮 数学公式渲染支持
</div>
</div>
<div
v-if=
"!isApiConfigured"
class=
"config-tip"
>
<p>
⚠️ 请先在左侧选择AI模型
</p>
</div>
</div>
</div>
<!-- 输入区域 -->
<div
class=
"input-section"
>
<div
class=
"input-container"
>
<input
v-model=
"inputValue"
placeholder=
"输入您的问题... (Enter发送, Shift+Enter换行)"
:disabled=
"!isApiConfigured || isAiThinking"
class=
"chat-input"
@
keydown=
"handleKeyDown"
/>
<button
:disabled=
"!inputValue.trim() || !isApiConfigured || isAiThinking"
class=
"send-btn"
@
click=
"onSubmit"
>
发送
</button>
</div>
<div
class=
"input-footer"
>
<span
class=
"input-counter"
>
{{ inputValue.length }}/2000
</span>
<div
class=
"input-actions"
>
<i
class=
"icon-paperclip"
title=
"附件"
></i>
<i
class=
"icon-mic"
title=
"语音"
></i>
</div>
</div>
</div>
</div>
</div>
</NConfigProvider>
</div>
</Teleport>
</template>
</template>
<
style
scoped
lang=
"scss"
>
<
style
scoped
></
style
>
.chat-container-wrapper
{
width
:
100%
;
height
:
100%
;
// 浅色主题变量
--text-color
:
#252b3a
;
--input-bg
:
#fff
;
--placeholder-color
:
#999
;
--disabled-text-color
:
#999
;
--border-color
:
#ddd
;
--sidebar-bg
:
#ffffff
;
--main-bg
:
#f5f5f5
;
// 深色主题适配
&
[
data-theme
=
'dark'
]
{
--text-color
:
#e1e5e9
;
--input-bg
:
#2d3748
;
--placeholder-color
:
#718096
;
--disabled-text-color
:
#718096
;
--border-color
:
#4a5568
;
--sidebar-bg
:
#1a202c
;
--main-bg
:
#2d3748
;
}
}
.chat-container
{
display
:
flex
;
height
:
85vh
;
background
:
var
(
--
main-bg
);
.sidebar
{
width
:
300px
;
background
:
var
(
--
sidebar-bg
);
border-right
:
1px
solid
var
(
--
border-color
);
display
:
flex
;
flex-direction
:
column
;
.sidebar-header
{
padding
:
16px
;
border-bottom
:
1px
solid
#eee
;
display
:
flex
;
justify-content
:
space-between
;
align-items
:
center
;
.logo-section
{
display
:
flex
;
align-items
:
center
;
gap
:
8px
;
.logo
{
width
:
32px
;
height
:
32px
;
border-radius
:
50%
;
background
:
#5e7ce0
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
color
:
white
;
}
.title
{
font-weight
:
500
;
color
:
#252b3a
;
}
}
.header-actions
{
display
:
flex
;
gap
:
8px
;
.action-btn
{
cursor
:
pointer
;
padding
:
4px
;
border-radius
:
4px
;
&
:hover
{
background
:
#f0f0f0
;
}
}
}
}
.config-panel
{
padding
:
16px
;
background
:
#f8f9fa
;
border-bottom
:
1px
solid
#eee
;
.config-item
{
margin-bottom
:
12px
;
label
{
display
:
block
;
margin-bottom
:
4px
;
font-size
:
12px
;
color
:
#666
;
}
input
,
select
{
width
:
100%
;
padding
:
8px
;
border
:
1px
solid
var
(
--
border-color
);
border-radius
:
4px
;
font-size
:
12px
;
color
:
var
(
--
text-color
,
#252b3a
);
background
:
var
(
--
input-bg
,
#fff
);
&
:focus
{
border-color
:
#5e7ce0
;
outline
:
none
;
}
&
:
:
placeholder
{
color
:
var
(
--
placeholder-color
,
#999
);
}
}
.model-description
{
margin-top
:
6px
;
padding
:
6px
8px
;
background
:
#f0f0f0
;
border-radius
:
4px
;
font-size
:
11px
;
color
:
#666
;
line-height
:
1
.4
;
}
}
.config-actions
{
display
:
flex
;
gap
:
8px
;
button
{
flex
:
1
;
padding
:
6px
12px
;
border
:
none
;
border-radius
:
4px
;
font-size
:
12px
;
cursor
:
pointer
;
&
.save-btn
{
background
:
#5e7ce0
;
color
:
white
;
}
&
.test-btn
{
background
:
#f0f0f0
;
color
:
#333
;
}
&
:disabled
{
opacity
:
0
.5
;
cursor
:
not
-
allowed
;
}
}
}
}
.search-section
{
padding
:
16px
;
.search-input
{
position
:
relative
;
display
:
flex
;
align-items
:
center
;
background
:
#f5f5f5
;
border-radius
:
8px
;
padding
:
8px
12px
;
.icon-search
{
color
:
#999
;
margin-right
:
8px
;
}
input
{
flex
:
1
;
border
:
none
;
background
:
transparent
;
outline
:
none
;
color
:
#252b3a
;
&
:
:
placeholder
{
color
:
#999
;
}
}
}
}
.chat-history
{
flex
:
1
;
overflow-y
:
auto
;
.history-section
{
padding
:
0
16px
;
.section-title
{
padding
:
8px
0
;
font-size
:
12px
;
color
:
#999
;
border-bottom
:
1px
solid
#eee
;
margin-bottom
:
8px
;
}
.chat-item
{
display
:
flex
;
align-items
:
center
;
padding
:
12px
8px
;
border-radius
:
8px
;
cursor
:
pointer
;
margin-bottom
:
4px
;
color
:
#252b3a
;
// 默认黑色字体
&
:hover
{
background
:
#f5f5f5
;
}
&
.active
{
background
:
#e6f3ff
;
color
:
#5e7ce0
;
}
.icon-message
{
margin-right
:
8px
;
color
:
#999
;
// 选中状态下的图标颜色
.chat-item.active
&
{
color
:
#5e7ce0
;
}
}
.chat-title
{
flex
:
1
;
font-size
:
14px
;
white-space
:
nowrap
;
overflow
:
hidden
;
text-overflow
:
ellipsis
;
color
:
inherit
;
// 继承父元素颜色
}
.icon-more
{
opacity
:
0
;
transition
:
opacity
0
.2s
;
color
:
#999
;
// 选中状态下的图标颜色
.chat-item.active
&
{
color
:
#5e7ce0
;
}
}
&
:hover
.icon-more
{
opacity
:
1
;
}
}
}
}
.sidebar-footer
{
padding
:
16px
;
border-top
:
1px
solid
#eee
;
.status-indicator
{
padding
:
8px
;
border-radius
:
6px
;
font-size
:
12px
;
text-align
:
center
;
background
:
#ffd6d6
;
color
:
#d32f2f
;
&
.connected
{
background
:
#d4edda
;
color
:
#155724
;
}
}
}
}
.chat-main
{
flex
:
1
;
display
:
flex
;
flex-direction
:
column
;
background
:
#ffffff
;
.chat-header
{
padding
:
16px
24px
;
border-bottom
:
1px
solid
#eee
;
display
:
flex
;
justify-content
:
space-between
;
align-items
:
center
;
.chat-title-section
{
display
:
flex
;
flex-direction
:
column
;
.chat-title
{
font-size
:
16px
;
font-weight
:
500
;
color
:
#252b3a
;
margin-bottom
:
4px
;
}
.chat-model
{
font-size
:
12px
;
color
:
#999
;
}
}
.chat-actions
{
display
:
flex
;
align-items
:
center
;
gap
:
12px
;
.token-count
{
font-size
:
12px
;
color
:
#999
;
}
.icon-more
{
cursor
:
pointer
;
padding
:
4px
;
border-radius
:
4px
;
&
:hover
{
background
:
#f0f0f0
;
}
}
}
}
.chat-content
{
flex
:
1
;
padding
:
24px
;
overflow-y
:
auto
;
display
:
flex
;
flex-direction
:
column
;
gap
:
16px
;
.message
{
display
:
flex
;
gap
:
12px
;
max-width
:
85%
;
&
.user-message
{
align-self
:
flex-end
;
flex-direction
:
row-reverse
;
.message-content
{
background
:
#5e7ce0
;
color
:
white
;
border-radius
:
18px
4px
18px
18px
;
}
}
&
.ai-message
{
align-self
:
flex-start
;
.message-content
{
background
:
#f5f5f5
;
color
:
#252b3a
;
border-radius
:
4px
18px
18px
18px
;
}
&
.thinking-message
.message-content
{
background
:
#e8f4fd
;
border
:
1px
solid
#b3d7f0
;
}
}
.message-avatar
{
width
:
32px
;
height
:
32px
;
border-radius
:
50%
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
font-size
:
16px
;
flex-shrink
:
0
;
margin-top
:
4px
;
}
.message-content
{
padding
:
12px
16px
;
word-wrap
:
break-word
;
position
:
relative
;
}
}
}
.start-page
{
flex
:
1
;
display
:
flex
;
flex-direction
:
column
;
align-items
:
center
;
justify-content
:
center
;
padding
:
40px
;
text-align
:
center
;
.welcome-content
{
h2
{
color
:
#252b3a
;
margin-bottom
:
16px
;
}
p
{
color
:
#666
;
margin-bottom
:
24px
;
font-size
:
16px
;
}
.features
{
display
:
flex
;
flex-direction
:
column
;
gap
:
8px
;
margin-bottom
:
24px
;
.feature-item
{
color
:
#5e7ce0
;
font-size
:
14px
;
}
}
.config-tip
{
p
{
color
:
#d32f2f
;
font-weight
:
500
;
}
}
.math-demo
{
margin
:
24px
0
;
padding
:
20px
;
background
:
#f8f9fa
;
border-radius
:
12px
;
border
:
1px
solid
#e9ecef
;
h3
{
color
:
#252b3a
;
margin-bottom
:
16px
;
font-size
:
16px
;
font-weight
:
600
;
}
.math-demo-card
{
background
:
white
;
border-radius
:
8px
;
box-shadow
:
0
1px
3px
rgba
(
0
,
0
,
0
,
0
.1
);
padding
:
16px
;
}
}
}
}
.input-section
{
padding
:
16px
24px
;
border-top
:
1px
solid
#eee
;
.input-container
{
display
:
flex
;
gap
:
12px
;
margin-bottom
:
8px
;
.chat-input
{
flex
:
1
;
padding
:
12px
16px
;
border
:
1px
solid
var
(
--
border-color
);
border-radius
:
24px
;
outline
:
none
;
font-size
:
14px
;
color
:
var
(
--
text-color
,
#252b3a
);
background
:
var
(
--
input-bg
,
#fff
);
&
:focus
{
border-color
:
#5e7ce0
;
}
&
:disabled
{
background
:
#f5f5f5
;
cursor
:
not
-
allowed
;
color
:
var
(
--
disabled-text-color
,
#999
);
}
&
:
:
placeholder
{
color
:
var
(
--
placeholder-color
,
#999
);
}
}
.send-btn
{
padding
:
12px
24px
;
background
:
#5e7ce0
;
color
:
white
;
border
:
none
;
border-radius
:
24px
;
cursor
:
pointer
;
font-size
:
14px
;
&
:hover
{
background
:
#4c6ed7
;
}
&
:disabled
{
background
:
#ccc
;
cursor
:
not
-
allowed
;
}
}
}
.input-footer
{
display
:
flex
;
justify-content
:
space-between
;
align-items
:
center
;
padding
:
0
8px
;
.input-counter
{
font-size
:
12px
;
color
:
#999
;
}
.input-actions
{
display
:
flex
;
gap
:
12px
;
i
{
cursor
:
pointer
;
color
:
#666
;
padding
:
4px
;
border-radius
:
4px
;
&
:hover
{
background
:
#f0f0f0
;
}
}
}
}
}
}
}
// 图标样式
.
icon-user
:
:
before
{
content
:
'👤'
;
}
.
icon-add
:
:
before
{
content
:
'➕'
;
}
.
icon-settings
:
:
before
{
content
:
'⚙️'
;
}
.
icon-search
:
:
before
{
content
:
'🔍'
;
}
.
icon-message
:
:
before
{
content
:
'💬'
;
}
.
icon-more
:
:
before
{
content
:
'⋯'
;
}
.
icon-copy
:
:
before
{
content
:
'📋'
;
}
.
icon-refresh
:
:
before
{
content
:
'🔄'
;
}
.
icon-like
:
:
before
{
content
:
'👍'
;
}
.
icon-dislike
:
:
before
{
content
:
'👎'
;
}
.
icon-paperclip
:
:
before
{
content
:
'📎'
;
}
.
icon-mic
:
:
before
{
content
:
'🎤'
;
}
.
icon-brain
:
:
before
{
content
:
'🧠'
;
}
// 思考状态样式
.thinking-header
{
display
:
flex
;
align-items
:
center
;
gap
:
8px
;
margin-bottom
:
8px
;
padding
:
6px
12px
;
background
:
linear-gradient
(
135deg
,
#667eea
0%
,
#764ba2
100%
);
color
:
white
;
border-radius
:
6px
;
font-size
:
11px
;
font-weight
:
500
;
&
.completed
{
background
:
linear-gradient
(
135deg
,
#4caf50
0%
,
#45a049
100%
);
}
}
.thinking-process-content
{
background
:
#f8f9fa
;
border
:
1px
solid
#e9ecef
;
border-radius
:
8px
;
padding
:
12px
;
font-family
:
'Monaco'
,
'Menlo'
,
'Ubuntu Mono'
,
monospace
;
font-size
:
13px
;
line-height
:
1
.4
;
color
:
#333
;
white-space
:
pre-wrap
;
min-height
:
40px
;
.thinking-cursor
{
color
:
#5e7ce0
;
animation
:
blink
1s
infinite
;
}
}
@keyframes
blink
{
0
%
,
50
%
{
opacity
:
1
;
}
51
%
,
100
%
{
opacity
:
0
;
}
}
.thinking-animation
{
display
:
flex
;
flex-direction
:
column
;
align-items
:
center
;
gap
:
12px
;
padding
:
12px
;
.thinking-dots
{
display
:
flex
;
gap
:
6px
;
span
{
width
:
8px
;
height
:
8px
;
background
:
#5e7ce0
;
border-radius
:
50%
;
animation
:
thinking-dot
1
.4s
ease-in-out
infinite
both
;
&
:nth-child
(
1
)
{
animation-delay
:
-0
.32s
;
}
&
:nth-child
(
2
)
{
animation-delay
:
-0
.16s
;
}
&
:nth-child
(
3
)
{
animation-delay
:
0s
;
}
}
}
.thinking-text
{
color
:
#666
;
font-size
:
13px
;
text-align
:
center
;
}
}
@keyframes
thinking-dot
{
0
%
,
80
%
,
100
%
{
transform
:
scale
(
0
.6
);
opacity
:
0
.4
;
}
40
%
{
transform
:
scale
(
1
);
opacity
:
1
;
}
}
// AI消息统一样式 - 匹配截图效果
.ai-message.unified-response
{
.message-content
{
background
:
#f8f9fa
;
border-radius
:
12px
;
padding
:
16px
;
margin
:
12px
0
;
border
:
1px
solid
#e3e3e3
;
}
// 统一内容区域
.unified-content
{
margin
:
0
;
}
// 顶部思考状态头
.thinking-status-header
{
color
:
#6b7280
;
font-size
:
13px
;
margin-bottom
:
12px
;
padding
:
6px
0
;
border-bottom
:
1px
solid
#e5e7eb
;
&
.thinking-active
{
color
:
#3b82f6
;
animation
:
pulse
2s
infinite
;
}
}
// 中间思考过程区域
.thinking-process-section
{
margin
:
12px
0
;
.thinking-content
{
background
:
transparent
;
border
:
none
;
padding
:
0
;
font-family
:
-
apple-system
,
BlinkMacSystemFont
,
'Segoe UI'
,
Roboto
,
sans-serif
;
font-size
:
14px
;
line-height
:
1
.6
;
color
:
#4b5563
;
white-space
:
pre-wrap
;
margin-bottom
:
16px
;
.thinking-cursor
{
color
:
#3b82f6
;
animation
:
blink
1s
infinite
;
}
}
}
// 底部最终答案区域(加粗)
.final-answer-section
{
margin-top
:
16px
;
padding-top
:
12px
;
.final-answer
{
font-weight
:
600
;
font-size
:
14px
;
line-height
:
1
.6
;
color
:
#111827
;
// 加粗markdown内容
p
{
font-weight
:
600
;
margin
:
8px
0
;
}
h1
,
h2
,
h3
,
h4
,
h5
,
h6
{
font-weight
:
700
;
}
strong
,
b
{
font-weight
:
700
;
}
}
}
// 操作按钮
.message-actions
{
display
:
flex
;
gap
:
12px
;
margin-top
:
16px
;
padding-top
:
12px
;
border-top
:
1px
solid
#e5e7eb
;
opacity
:
0
;
transition
:
opacity
0
.2s
ease
;
i
{
cursor
:
pointer
;
color
:
#6b7280
;
padding
:
6px
;
border-radius
:
6px
;
font-size
:
16px
;
transition
:
all
0
.2s
ease
;
&
:hover
{
background
:
#f3f4f6
;
color
:
#374151
;
transform
:
scale
(
1
.1
);
}
}
}
&
:hover
.message-actions
{
opacity
:
1
;
}
}
// 脉搏动画
@keyframes
pulse
{
0
%
,
100
%
{
opacity
:
1
;
}
50
%
{
opacity
:
0
.6
;
}
}
// Markdown 内容样式
.markdown-content
{
line-height
:
1
.6
;
color
:
#252b3a
;
h1
,
h2
,
h3
,
h4
,
h5
,
h6
{
margin
:
16px
0
8px
0
;
color
:
#252b3a
;
font-weight
:
600
;
}
h1
{
font-size
:
20px
;
}
h2
{
font-size
:
18px
;
}
h3
{
font-size
:
16px
;
}
h4
{
font-size
:
14px
;
}
p
{
margin
:
8px
0
;
}
ul
,
ol
{
margin
:
8px
0
;
padding-left
:
20px
;
}
li
{
margin
:
4px
0
;
}
blockquote
{
margin
:
12px
0
;
padding
:
8px
12px
;
background
:
#f8f9fa
;
border-left
:
3px
solid
#5e7ce0
;
border-radius
:
4px
;
color
:
#666
;
font-style
:
italic
;
}
code
{
background
:
#f1f3f4
;
padding
:
2px
6px
;
border-radius
:
4px
;
font-family
:
'Monaco'
,
'Menlo'
,
'Ubuntu Mono'
,
monospace
;
font-size
:
12px
;
color
:
#e83e8c
;
}
pre
{
background
:
#1e1e1e
;
color
:
#d4d4d4
;
padding
:
12px
;
border-radius
:
6px
;
overflow-x
:
auto
;
margin
:
12px
0
;
code
{
background
:
none
;
padding
:
0
;
color
:
#d4d4d4
;
font-size
:
13px
;
}
}
strong
{
font-weight
:
600
;
color
:
#252b3a
;
}
em
{
font-style
:
italic
;
}
a
{
color
:
#5e7ce0
;
text-decoration
:
none
;
&
:hover
{
text-decoration
:
underline
;
}
}
// KaTeX 数学公式样式
.katex
{
font-size
:
1
.1em
;
}
.katex-display
{
margin
:
16px
0
;
text-align
:
center
;
}
.math-display
{
margin
:
16px
0
;
text-align
:
center
;
overflow-x
:
auto
;
}
// 思考过程样式
think
{
display
:
block
;
background
:
#f0f8ff
;
border-left
:
4px
solid
#5e7ce0
;
padding
:
12px
16px
;
margin
:
16px
0
;
border-radius
:
0
8px
8px
0
;
font-style
:
italic
;
color
:
#4a6cf7
;
white-space
:
pre-wrap
;
}
// 数学公式错误样式
.math-error
{
color
:
#cc0000
;
background
:
#ffe6e6
;
padding
:
4px
8px
;
border-radius
:
4px
;
font-family
:
monospace
;
border
:
1px
solid
#ffcccc
;
}
}
.text-content
{
line-height
:
1
.6
;
color
:
#252b3a
;
}
.message-actions
{
display
:
flex
;
gap
:
8px
;
margin-top
:
8px
;
opacity
:
0
;
transition
:
opacity
0
.2s
;
i
{
cursor
:
pointer
;
color
:
#666
;
padding
:
4px
;
border-radius
:
4px
;
font-size
:
12px
;
&
:hover
{
background
:
#e0e0e0
;
}
}
}
.message
:hover
.message-actions
{
opacity
:
1
;
}
</
style
>
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a 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