Commit fe24be5c by User

修复聊天页面深色主题下输入框字体颜色问题

parent 90ddeed0
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, onUnmounted, ref } from 'vue'; import { computed, onActivated, onBeforeUnmount, onDeactivated, onMounted, onUnmounted, ref } from 'vue';
import { NConfigProvider, darkTheme } from 'naive-ui';
import { useThemeStore } from '@/store/modules/theme';
import { type ModelOption, deepSeekService } from '@/service/api/deepseek'; import { type ModelOption, deepSeekService } from '@/service/api/deepseek';
import { renderMarkdownWithMath as renderContentWithMath } from '@/utils/katex-renderer'; import { renderMarkdownWithMath as renderContentWithMath } from '@/utils/katex-renderer';
defineOptions({ name: 'ChatPage' });
interface ChatMessage { interface ChatMessage {
id: string; id: string;
from: 'user' | 'ai'; from: 'user' | 'ai';
...@@ -26,6 +30,14 @@ interface ChatHistory { ...@@ -26,6 +30,14 @@ interface ChatHistory {
messages: ChatMessage[]; messages: ChatMessage[];
} }
// 页面激活状态管理
const isActive = ref(true);
const containerId = `chat-${Math.random().toString(36).substr(2, 9)}`;
// 主题相关
const themeStore = useThemeStore();
const naiveDarkTheme = computed(() => (themeStore.darkMode ? darkTheme : undefined));
// 响应式数据 // 响应式数据
const searchText = ref(''); const searchText = ref('');
const inputValue = ref(''); const inputValue = ref('');
...@@ -78,6 +90,34 @@ const currentModelName = computed(() => { ...@@ -78,6 +90,34 @@ const currentModelName = computed(() => {
// 聊天容器引用 // 聊天容器引用
const chatContentRef = ref<HTMLElement | null>(null); const chatContentRef = ref<HTMLElement | null>(null);
// 清理函数
function cleanup() {
try {
// 清理定时器
if (thinkingInterval.value) {
clearInterval(thinkingInterval.value);
thinkingInterval.value = null;
}
// 清空状态
isAiThinking.value = false;
thinkingTime.value = 0;
// 清空容器
const container = document.getElementById(containerId);
if (container) {
container.innerHTML = '';
}
} catch (e) {
console.error('清理聊天组件时出错:', e);
}
}
// 暴露清理方法给父组件
defineExpose({
cleanup
});
// 滚动到底部 // 滚动到底部
const scrollToBottom = () => { const scrollToBottom = () => {
if (chatContentRef.value) { if (chatContentRef.value) {
...@@ -396,6 +436,7 @@ const showChatMenu = (_chatId: string) => { ...@@ -396,6 +436,7 @@ const showChatMenu = (_chatId: string) => {
// 生命周期 // 生命周期
onMounted(() => { onMounted(() => {
isActive.value = true;
// 加载可用模型选项 // 加载可用模型选项
modelOptions.value = deepSeekService.getModelOptions(); modelOptions.value = deepSeekService.getModelOptions();
...@@ -407,6 +448,19 @@ onMounted(() => { ...@@ -407,6 +448,19 @@ onMounted(() => {
} }
}); });
onActivated(() => {
isActive.value = true;
});
onDeactivated(() => {
isActive.value = false;
});
onBeforeUnmount(() => {
// 组件卸载前清理
cleanup();
});
onUnmounted(() => { onUnmounted(() => {
// 清理定时器 // 清理定时器
if (thinkingInterval.value) { if (thinkingInterval.value) {
...@@ -416,208 +470,248 @@ onUnmounted(() => { ...@@ -416,208 +470,248 @@ onUnmounted(() => {
</script> </script>
<template> <template>
<div class="chat-container"> <Teleport to="#extjs-root">
<!-- 左侧侧边栏 --> <div
<div class="sidebar"> v-show="isActive"
<!-- 顶部工具栏 --> :id="containerId"
<div class="sidebar-header"> class="chat-container-wrapper"
<div class="logo-section"> :data-theme="themeStore.darkMode ? 'dark' : 'light'"
<div class="logo"> >
<i class="icon-user"></i> <NConfigProvider :theme="naiveDarkTheme" :theme-overrides="themeStore.naiveTheme">
</div> <div class="chat-container">
<span class="title">{{ currentModelName }}</span> <!-- 左侧侧边栏 -->
</div> <div class="sidebar">
<div class="header-actions"> <!-- 顶部工具栏 -->
<i class="icon-add action-btn" @click="newChat"></i> <div class="sidebar-header">
<i class="icon-settings action-btn" title="API配置" @click="showConfig = !showConfig"></i> <div class="logo-section">
</div> <div class="logo">
</div> <i class="icon-user"></i>
</div>
<!-- 模型配置面板 --> <span class="title">{{ currentModelName }}</span>
<div v-if="showConfig" class="config-panel"> </div>
<div class="config-item"> <div class="header-actions">
<label>选择模型:</label> <i class="icon-add action-btn" @click="newChat"></i>
<select v-model="selectedModelId" @change="onModelChange"> <i class="icon-settings action-btn" title="API配置" @click="showConfig = !showConfig"></i>
<option v-for="model in modelOptions" :key="model.id" :value="model.id"> </div>
{{ model.name }} </div>
</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 v-if="showConfig" class="config-panel">
<div class="search-input"> <div class="config-item">
<i class="icon-search"></i> <label>选择模型:</label>
<input v-model="searchText" placeholder="搜索对话" /> <select v-model="selectedModelId" @change="onModelChange">
</div> <option v-for="model in modelOptions" :key="model.id" :value="model.id">
</div> {{ 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="chat-history"> <div class="search-section">
<div class="history-section"> <div class="search-input">
<div class="section-title"> <i class="icon-search"></i>
<span>对话历史</span> <input v-model="searchText" placeholder="搜索对话" />
</div> </div>
<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="chat-history">
<div class="status-indicator" :class="{ connected: isApiConfigured }"> <div class="history-section">
<span>{{ isApiConfigured ? '✅ 模型已选择' : '❌ 未选择模型' }}</span> <div class="section-title">
</div> <span>对话历史</span>
</div> </div>
</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="chat-main"> <div class="sidebar-footer">
<!-- 聊天区域头部 --> <div class="status-indicator" :class="{ connected: isApiConfigured }">
<div class="chat-header"> <span>{{ isApiConfigured ? '✅ 模型已选择' : '❌ 未选择模型' }}</span>
<div class="chat-title-section"> </div>
<span class="chat-title">{{ currentChat?.title || 'DeepSeek AI助手' }}</span> </div>
<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> </div>
<!-- AI消息 - 统一样式:思考过程 + 最终答案 --> <!-- 右侧聊天区域 -->
<div v-else class="message ai-message unified-response"> <div class="chat-main">
<div class="message-avatar">🤖</div> <!-- 聊天区域头部 -->
<div class="message-content"> <div class="chat-header">
<!-- 顶部:深度思考状态 --> <div class="chat-title-section">
<div v-if="msg.thinkingTime" class="thinking-status-header"> <span class="chat-title">{{ currentChat?.title || 'DeepSeek AI助手' }}</span>
🧠 已深度思考(用时{{ msg.thinkingTime }}秒) <span class="chat-model">DeepSeek Chat</span>
</div> </div>
<div v-else-if="msg.phase === 'thinking'" class="thinking-status-header thinking-active"> <div class="chat-actions">
🧠 正在深度思考... <span v-if="lastThinkingTime" class="token-count">上次思考用时 {{ lastThinkingTime }}</span>
<i class="icon-more"></i>
</div> </div>
</div>
<!-- 统一使用 MarkdownCard 展示思考过程和答案 --> <!-- 聊天内容区域 -->
<div class="unified-markdown-section"> <div v-if="!showStartPage" ref="chatContentRef" class="chat-content">
<!-- 使用 McMarkdownCard 统一渲染 --> <template v-for="msg in messages" :key="`${msg.id}-${msg.phase}-${Date.now()}`">
<div <!-- 用户消息 -->
class="unified-content markdown-content" <div v-if="msg.from === 'user'" class="message user-message">
v-html="renderMarkdownWithMath(getFormattedContent(msg))" <div class="message-content">{{ msg.content }}</div>
></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>
<!-- 底部:操作按钮 --> <!-- 启动页 -->
<div <div v-else class="start-page">
v-if="(msg.phase === 'completed' || msg.phase === 'answering') && msg.content" <div class="welcome-content">
class="message-actions" <h2>🤖 DeepSeek AI助手</h2>
> <p>基于DeepSeek强大AI模型的智能对话助手</p>
<i class="icon-copy" title="复制" @click="copyMessage(msg.content)"></i> <div class="features">
<i class="icon-refresh" title="重新生成" @click="regenerateMessage(msg)"></i> <div class="feature-item">✨ 深度思考过程展示</div>
<i class="icon-like" title="点赞"></i> <div class="feature-item">💻 专业代码生成</div>
<i class="icon-dislike" title="点踩"></i> <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> </div>
</div>
</template> <!-- 输入区域 -->
<div class="input-section">
<!-- AI思考状态 --> <div class="input-container">
<div v-if="isAiThinking" class="message ai-message thinking-message"> <input
<div class="message-avatar">🤖</div> v-model="inputValue"
<div class="message-content"> placeholder="输入您的问题... (Enter发送, Shift+Enter换行)"
<div class="thinking-animation"> :disabled="!isApiConfigured || isAiThinking"
<div class="thinking-dots"> class="chat-input"
<span></span> @keydown="handleKeyDown"
<span></span> />
<span></span> <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 class="thinking-text">正在思考中...({{ thinkingTime }}秒)</div>
</div> </div>
</div> </div>
</div> </div>
</div> </NConfigProvider>
<!-- 启动页 -->
<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>
</div> </Teleport>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
.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 { .chat-container {
display: flex; display: flex;
height: 85vh; height: 85vh;
background: #f5f5f5; background: var(--main-bg);
.sidebar { .sidebar {
width: 300px; width: 300px;
background: #ffffff; background: var(--sidebar-bg);
border-right: 1px solid #ddd; border-right: 1px solid var(--border-color);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
...@@ -685,9 +779,20 @@ onUnmounted(() => { ...@@ -685,9 +779,20 @@ onUnmounted(() => {
select { select {
width: 100%; width: 100%;
padding: 8px; padding: 8px;
border: 1px solid #ddd; border: 1px solid var(--border-color);
border-radius: 4px; border-radius: 4px;
font-size: 12px; 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 { .model-description {
...@@ -1038,10 +1143,12 @@ onUnmounted(() => { ...@@ -1038,10 +1143,12 @@ onUnmounted(() => {
.chat-input { .chat-input {
flex: 1; flex: 1;
padding: 12px 16px; padding: 12px 16px;
border: 1px solid #ddd; border: 1px solid var(--border-color);
border-radius: 24px; border-radius: 24px;
outline: none; outline: none;
font-size: 14px; font-size: 14px;
color: var(--text-color, #252b3a);
background: var(--input-bg, #fff);
&:focus { &:focus {
border-color: #5e7ce0; border-color: #5e7ce0;
...@@ -1050,6 +1157,11 @@ onUnmounted(() => { ...@@ -1050,6 +1157,11 @@ onUnmounted(() => {
&:disabled { &:disabled {
background: #f5f5f5; background: #f5f5f5;
cursor: not-allowed; cursor: not-allowed;
color: var(--disabled-text-color, #999);
}
&::placeholder {
color: var(--placeholder-color, #999);
} }
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment