1
This commit is contained in:
@@ -0,0 +1,59 @@
|
|||||||
|
# Frontend 上下文窗口 popover 主题适配 — Design Spec
|
||||||
|
|
||||||
|
**日期:** 2026-08-07
|
||||||
|
**状态:** Approved
|
||||||
|
**范围:** Bug 修复(CSS 变量 / Element Plus Teleport)
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
|
||||||
|
`src/views/frontend/components/InputArea.vue` 中的"上下文窗口"指示器(`.context-indicator`)点击后弹出 `el-popover`,其中显示 token 使用量、阈值百分比等。当前问题是:该 popover 没有适配 frontend 的深 / 浅色主题。
|
||||||
|
|
||||||
|
## 根因
|
||||||
|
|
||||||
|
Element Plus 的 `el-popover` 默认 `teleported: true`(已确认,源码 `element-plus/es/components/tooltip/src/content.mjs`),弹出层被 Teleport 到 body 之外。
|
||||||
|
|
||||||
|
`body[data-fe-theme="light"]` 只覆盖了 `--fe-*` 系列变量,未覆盖 Element Plus 内部用到的 `--el-bg-color-overlay`、`--el-border-color-lighter`、`--el-text-color-regular` 等。结果是:
|
||||||
|
|
||||||
|
- popover 容器(`.el-popper`)回退到 Element Plus 默认值(浅色背景 + 浅色文本)
|
||||||
|
- 在深色主题下,与 frontend 整体深色风格不一致
|
||||||
|
- 而 `.context-popover` 内部的 `--fe-*` 变量虽然继承自 `:root`,但 popover 容器本身的背景 / 边框 / 文字色来自 `--el-*`,这些没被覆盖
|
||||||
|
|
||||||
|
## 方案
|
||||||
|
|
||||||
|
**给 `el-popover` 设置 `:teleported="false"`**,让 popover 容器保留在 `.frontend-container` DOM 树内,从而继承 `.frontend-container` 下的所有 `--el-*` / `--fe-*` 变量映射。
|
||||||
|
|
||||||
|
### 改动点
|
||||||
|
|
||||||
|
`src/views/frontend/components/InputArea.vue` 第 81-85 行的 `el-popover` 标签,新增 `:teleported="false"`:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<el-popover
|
||||||
|
placement="top"
|
||||||
|
:width="280"
|
||||||
|
trigger="click"
|
||||||
|
:teleported="false"
|
||||||
|
>
|
||||||
|
```
|
||||||
|
|
||||||
|
`.context-popover` 现有的 scoped 样式(`.popover-title` / `.popover-body` / `.popover-footer` / `.token-count`)保持不变,因为它们使用 `--fe-*` 变量,本来就能工作。
|
||||||
|
|
||||||
|
### 不需要改动
|
||||||
|
|
||||||
|
- `theme.scss`:`.frontend-container` 已经把 `--el-bg-color-overlay`、`--el-text-color-regular`、`--el-color-success`、`--el-color-danger` 等都映射到 `--fe-*` 变量,只要 popover 在 `.frontend-container` 内就能自动生效
|
||||||
|
- 浅色覆盖:`.frontend-container[data-theme="light"]` 同样完整覆盖了 `--el-*` 系列
|
||||||
|
- `.context-popover` 的 scoped 样式:因为 `.context-popover` 本身在 InputArea 模板中,scoped 的 `data-v-xxx` 仍会附加到元素上,popover 留在组件树内后所有选择器自然匹配
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
1. 切到深色主题,点击输入框右下角的"XX%"指示器 → popover 应为深色背景(`#252526` / `--fe-bg-elevated`)、浅色文字
|
||||||
|
2. 切到浅色主题,点击同一指示器 → popover 应为白色(`#ffffff` / `--el-bg-color`)、深色文字
|
||||||
|
3. 百分比 `el-tag`:深色主题下 `success` 是青色(`#4ec9b0`),`danger` 是珊瑚红(`#f48771`);浅色主题下 `success` 是蓝色(`#1890ff`),`danger` 是红色(`#f56c6c`)
|
||||||
|
4. `token-count` 文字:深色下青色,浅色下蓝色
|
||||||
|
5. 重复点击 / 多次切换主题,popover 位置不漂移、不被聊天内容遮挡
|
||||||
|
|
||||||
|
## 非目标
|
||||||
|
|
||||||
|
- 不改 `el-popover` 的其它属性(placement、width、trigger)
|
||||||
|
- 不改 `theme.scss`
|
||||||
|
- 不改 `.context-popover` 现有样式(变量已经够用)
|
||||||
|
- 不动 `el-tooltip`(左下角 `@` 按钮的 tooltip,独立组件,独立处理)
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 509 KiB After Width: | Height: | Size: 525 KiB |
@@ -1,40 +1,49 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="chat-header">
|
<div class="chat-header">
|
||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
|
<div class="agent-avatar">{{ agentNameInitial }}</div>
|
||||||
<span class="conversation-name">{{ agentName }}</span>
|
<span class="conversation-name">{{ agentName }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<!-- 清空会话 -->
|
<el-tooltip content="清空会话" placement="bottom" :show-after="300">
|
||||||
<el-button @click="emit('clear')">
|
<el-button class="icon-btn icon-btn--danger" link @click="emit('clear')">
|
||||||
清空会话
|
<el-icon><Brush /></el-icon>
|
||||||
</el-button>
|
</el-button>
|
||||||
<!-- 模型选择 -->
|
</el-tooltip>
|
||||||
<el-button @click="showModelModal = true">
|
|
||||||
{{ currentModel?.name || currentModel?.modelName || '选择模型' }}
|
|
||||||
</el-button>
|
|
||||||
<el-button
|
|
||||||
:type="activeTab === 'agent' ? 'primary' : 'default'"
|
|
||||||
@click="handleTabClick('agent')"
|
|
||||||
>
|
|
||||||
智能体
|
|
||||||
</el-button>
|
|
||||||
<el-button
|
|
||||||
:type="activeTab === 'file' ? 'primary' : 'default'"
|
|
||||||
@click="handleTabClick('file')"
|
|
||||||
>
|
|
||||||
文件
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 模型选择弹窗 -->
|
<el-select
|
||||||
<LlmModelModal v-model="showModelModal" @select="handleModelSelect" />
|
class="model-select"
|
||||||
|
:model-value="modelValue"
|
||||||
|
@update:model-value="handleSelectChange"
|
||||||
|
popper-class="frontend-el-select-dropdown"
|
||||||
|
placeholder="选择模型"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="opt in modelOptions"
|
||||||
|
:key="opt.value"
|
||||||
|
:label="opt.label"
|
||||||
|
:value="opt.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
|
||||||
|
<el-tooltip
|
||||||
|
:content="filePanelVisible ? '收起文件面板' : '展开文件面板'"
|
||||||
|
placement="bottom"
|
||||||
|
:show-after="300"
|
||||||
|
>
|
||||||
|
<el-button class="icon-btn" link @click="handleToggleClick">
|
||||||
|
<el-icon>
|
||||||
|
<component :is="filePanelVisible ? Fold : Expand" />
|
||||||
|
</el-icon>
|
||||||
|
</el-button>
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { Cpu } from '@element-plus/icons-vue'
|
import { Brush, Fold, Expand } from '@element-plus/icons-vue'
|
||||||
import LlmModelModal from './LlmModelModal.vue'
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
agentName: {
|
agentName: {
|
||||||
@@ -44,26 +53,36 @@ const props = defineProps({
|
|||||||
currentModel: {
|
currentModel: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: null
|
default: null
|
||||||
|
},
|
||||||
|
modelOptions: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
filePanelVisible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const activeTab = ref('agent')
|
const emit = defineEmits(['clear', 'update:modelValue', 'toggle-file-panel'])
|
||||||
const showModelModal = ref(false)
|
|
||||||
|
|
||||||
const emit = defineEmits(['tab-change', 'model-change', 'clear'])
|
const agentNameInitial = computed(() => {
|
||||||
|
const name = (props.agentName || '').trim()
|
||||||
function handleTabClick(tab) {
|
if (!name) return '?'
|
||||||
activeTab.value = tab
|
// 中文取首字,英文取首字母
|
||||||
emit('tab-change', tab)
|
return [...name][0]
|
||||||
}
|
|
||||||
|
|
||||||
defineExpose({
|
|
||||||
setActiveTab: (tab) => {
|
|
||||||
activeTab.value = tab
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
function handleModelSelect(model) {
|
|
||||||
emit('model-change', model)
|
function handleSelectChange(value) {
|
||||||
|
emit('update:modelValue', value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleToggleClick() {
|
||||||
|
emit('toggle-file-panel')
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -78,8 +97,25 @@ function handleModelSelect(model) {
|
|||||||
color: var(--fe-text-primary);
|
color: var(--fe-text-primary);
|
||||||
|
|
||||||
.header-left {
|
.header-left {
|
||||||
max-width: 400px;
|
display: flex;
|
||||||
overflow: hidden;
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
max-width: 480px;
|
||||||
|
|
||||||
|
.agent-avatar {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--fe-accent);
|
||||||
|
color: var(--fe-text-inverse);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.conversation-name {
|
.conversation-name {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@@ -93,6 +129,43 @@ function handleModelSelect(model) {
|
|||||||
.header-right {
|
.header-right {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.icon-btn {
|
||||||
|
padding: 4px 8px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--fe-text-secondary);
|
||||||
|
height: 32px;
|
||||||
|
width: 32px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: var(--fe-radius-md);
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--fe-bg-hover);
|
||||||
|
color: var(--fe-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--danger:hover {
|
||||||
|
color: var(--fe-danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-icon {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-select {
|
||||||
|
width: 180px;
|
||||||
|
:deep(.el-select__wrapper) {
|
||||||
|
background: var(--fe-bg-elevated);
|
||||||
|
box-shadow: 0 0 0 1px var(--fe-border) inset;
|
||||||
|
border-radius: var(--fe-radius-md);
|
||||||
|
}
|
||||||
|
:deep(.el-select__placeholder) { color: var(--fe-text-muted); }
|
||||||
|
:deep(.el-select__selected-item) { color: var(--fe-text-primary); }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -445,7 +445,7 @@ defineExpose({ scrollToBottom, forceScrollToBottom })
|
|||||||
.chat-window {
|
.chat-window {
|
||||||
position: relative;
|
position: relative;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 16px 20px;
|
padding: 12px 16px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -491,7 +491,7 @@ defineExpose({ scrollToBottom, forceScrollToBottom })
|
|||||||
}
|
}
|
||||||
|
|
||||||
.message-group {
|
.message-group {
|
||||||
margin-bottom: 12px;
|
margin-bottom: 10px;
|
||||||
|
|
||||||
&.user-group {
|
&.user-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -501,7 +501,7 @@ defineExpose({ scrollToBottom, forceScrollToBottom })
|
|||||||
.message-bubble {
|
.message-bubble {
|
||||||
background: var(--fe-bg-elevated);
|
background: var(--fe-bg-elevated);
|
||||||
color: var(--fe-text-primary);
|
color: var(--fe-text-primary);
|
||||||
border-radius: 16px 16px 4px 16px;
|
border-radius: 10px 10px 4px 10px;
|
||||||
box-shadow: var(--fe-shadow-sm);
|
box-shadow: var(--fe-shadow-sm);
|
||||||
max-width: 70%;
|
max-width: 70%;
|
||||||
|
|
||||||
@@ -530,15 +530,16 @@ defineExpose({ scrollToBottom, forceScrollToBottom })
|
|||||||
.message-bubble {
|
.message-bubble {
|
||||||
background: var(--fe-bg-elevated);
|
background: var(--fe-bg-elevated);
|
||||||
color: var(--fe-text-primary);
|
color: var(--fe-text-primary);
|
||||||
border-radius: 16px 16px 16px 4px;
|
border-radius: 10px 10px 10px 4px;
|
||||||
box-shadow: var(--fe-shadow-sm);
|
box-shadow: var(--fe-shadow-sm);
|
||||||
max-width: 70%;
|
max-width: 70%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-bubble {
|
.message-bubble {
|
||||||
padding: 10px 14px;
|
padding: 9px 13px;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
|
border-radius: 10px;
|
||||||
|
|
||||||
.message-text {
|
.message-text {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|||||||
@@ -24,12 +24,30 @@
|
|||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="sidebar-search">
|
||||||
|
<el-input
|
||||||
|
v-model="searchKeyword"
|
||||||
|
placeholder="搜索会话"
|
||||||
|
:prefix-icon="Search"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<el-scrollbar class="sidebar-scrollbar">
|
<el-scrollbar class="sidebar-scrollbar">
|
||||||
<div v-if="loading && sessionList.length === 0" class="loading-tip">
|
<div v-if="loading && sessionList.length === 0" class="loading-tip">
|
||||||
加载中...
|
加载中...
|
||||||
</div>
|
</div>
|
||||||
|
<template v-for="group in sessionGroups" :key="group.key">
|
||||||
|
<template v-if="group.items.length > 0">
|
||||||
|
<div class="group-header" @click="toggleGroup(group.key)">
|
||||||
|
<span class="group-title">{{ group.label }}</span>
|
||||||
|
<el-icon class="group-toggle">
|
||||||
|
<ArrowDown v-if="expandedGroups.has(group.key)" />
|
||||||
|
<ArrowRight v-else />
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
<div v-show="expandedGroups.has(group.key)" class="group-items">
|
||||||
<div
|
<div
|
||||||
v-for="session in sessionList"
|
v-for="session in group.items"
|
||||||
:key="session.sessionId"
|
:key="session.sessionId"
|
||||||
class="conversation-item"
|
class="conversation-item"
|
||||||
:class="{ active: currentSession?.sessionId === session.sessionId, 'has-new-message': session.hasNewMessage }"
|
:class="{ active: currentSession?.sessionId === session.sessionId, 'has-new-message': session.hasNewMessage }"
|
||||||
@@ -43,6 +61,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-if="session.hasNewMessage" class="new-message-dot"></div>
|
<div v-if="session.hasNewMessage" class="new-message-dot"></div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- 右键菜单 -->
|
<!-- 右键菜单 -->
|
||||||
<teleport to="body">
|
<teleport to="body">
|
||||||
@@ -67,9 +88,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onUnmounted } from 'vue'
|
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { DArrowLeft, DArrowRight, Refresh, Plus, Delete, Loading } from '@element-plus/icons-vue'
|
import { DArrowLeft, DArrowRight, Refresh, Plus, Delete, Loading, Search, ArrowDown, ArrowRight } from '@element-plus/icons-vue'
|
||||||
import { listChatSessions, deleteChatSession, renameChatSession } from '@/api/frontend'
|
import { listChatSessions, deleteChatSession, renameChatSession } from '@/api/frontend'
|
||||||
import { formatTimeAgo } from '@/utils/time'
|
import { formatTimeAgo } from '@/utils/time'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
@@ -82,6 +103,70 @@ const currentSession = ref(null)
|
|||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const emit = defineEmits(['select', 'new-session', 'ready'])
|
const emit = defineEmits(['select', 'new-session', 'ready'])
|
||||||
|
|
||||||
|
// —— 搜索关键字(仅样式占位,本次不绑定行为) ——
|
||||||
|
const searchKeyword = ref('')
|
||||||
|
|
||||||
|
// —— 分组折叠状态:'today' | 'yesterday' | 'earlier' ——
|
||||||
|
const expandedGroups = ref(new Set())
|
||||||
|
|
||||||
|
function parseTimestamp(t) {
|
||||||
|
if (!t) return null
|
||||||
|
if (typeof t === 'number' && String(t).length === 13) return t
|
||||||
|
if (typeof t === 'number' || /^\d{14}$/.test(t)) {
|
||||||
|
const s = String(t)
|
||||||
|
return new Date(`${s.slice(0,4)}-${s.slice(4,6)}-${s.slice(6,8)}T${s.slice(8,10)}:${s.slice(10,12)}:${s.slice(12,14)}`).getTime()
|
||||||
|
}
|
||||||
|
if (typeof t === 'string' && t.includes(' ')) {
|
||||||
|
const iso = t.replace(' ', 'T').replace(/\.(\d+)?$/, '')
|
||||||
|
const ms = new Date(iso).getTime()
|
||||||
|
return Number.isNaN(ms) ? null : ms
|
||||||
|
}
|
||||||
|
const ms = new Date(t).getTime()
|
||||||
|
return Number.isNaN(ms) ? null : ms
|
||||||
|
}
|
||||||
|
|
||||||
|
function groupOf(session) {
|
||||||
|
const ts = parseTimestamp(session?.updateTime)
|
||||||
|
if (ts == null) {
|
||||||
|
console.warn('[ConversationSidebar] 无法解析会话时间', session)
|
||||||
|
return 'earlier'
|
||||||
|
}
|
||||||
|
const now = new Date()
|
||||||
|
const d = new Date(ts)
|
||||||
|
const sameDay = (a, b) =>
|
||||||
|
a.getFullYear() === b.getFullYear() &&
|
||||||
|
a.getMonth() === b.getMonth() &&
|
||||||
|
a.getDate() === b.getDate()
|
||||||
|
const yesterday = new Date(now); yesterday.setDate(now.getDate() - 1)
|
||||||
|
if (sameDay(d, now)) return 'today'
|
||||||
|
if (sameDay(d, yesterday)) return 'yesterday'
|
||||||
|
return 'earlier'
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionGroups = computed(() => {
|
||||||
|
const groups = { today: [], yesterday: [], earlier: [] }
|
||||||
|
for (const s of sessionList.value) {
|
||||||
|
groups[groupOf(s)].push(s)
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{ key: 'today', label: '今天', items: groups.today },
|
||||||
|
{ key: 'yesterday', label: '昨天', items: groups.yesterday },
|
||||||
|
{ key: 'earlier', label: '更早', items: groups.earlier }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
function toggleGroup(key) {
|
||||||
|
const next = new Set(expandedGroups.value)
|
||||||
|
if (next.has(key)) next.delete(key)
|
||||||
|
else next.add(key)
|
||||||
|
expandedGroups.value = next
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncExpandedWithCurrent() {
|
||||||
|
// 用户要求默认全部展开;用户可点击分组标题手动收/展
|
||||||
|
expandedGroups.value = new Set(['today', 'yesterday', 'earlier'])
|
||||||
|
}
|
||||||
|
|
||||||
// 响应式:viewport ≤ 900px 收缩,> 900px 展开
|
// 响应式:viewport ≤ 900px 收缩,> 900px 展开
|
||||||
let sidebarMediaQuery = null
|
let sidebarMediaQuery = null
|
||||||
const sidebarBreakpointHandler = (e) => {
|
const sidebarBreakpointHandler = (e) => {
|
||||||
@@ -148,6 +233,7 @@ function toggleSidebar() {
|
|||||||
function selectSession(session) {
|
function selectSession(session) {
|
||||||
currentSession.value = session
|
currentSession.value = session
|
||||||
emit('select', session)
|
emit('select', session)
|
||||||
|
syncExpandedWithCurrent()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchSessions() {
|
async function fetchSessions() {
|
||||||
@@ -169,6 +255,7 @@ async function fetchSessions() {
|
|||||||
const remain = 300 - elapsed
|
const remain = 300 - elapsed
|
||||||
if (remain > 0) await new Promise(resolve => setTimeout(resolve, remain))
|
if (remain > 0) await new Promise(resolve => setTimeout(resolve, remain))
|
||||||
loading.value = false
|
loading.value = false
|
||||||
|
syncExpandedWithCurrent()
|
||||||
emit('ready')
|
emit('ready')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -201,6 +288,7 @@ async function handleDeleteSession(session) {
|
|||||||
currentSession.value = null
|
currentSession.value = null
|
||||||
emit('select', null)
|
emit('select', null)
|
||||||
}
|
}
|
||||||
|
syncExpandedWithCurrent()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error !== 'cancel') {
|
if (error !== 'cancel') {
|
||||||
console.error('删除会话失败:', error)
|
console.error('删除会话失败:', error)
|
||||||
@@ -352,6 +440,50 @@ defineExpose({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sidebar-search {
|
||||||
|
padding: 8px 12px 0;
|
||||||
|
:deep(.el-input__wrapper) {
|
||||||
|
background: var(--fe-bg-elevated);
|
||||||
|
box-shadow: 0 0 0 1px var(--fe-border) inset;
|
||||||
|
border-radius: var(--fe-radius-md);
|
||||||
|
transition: box-shadow 0.15s, background 0.15s;
|
||||||
|
}
|
||||||
|
:deep(.el-input__wrapper.is-focus) {
|
||||||
|
box-shadow: 0 0 0 1px var(--fe-accent) inset;
|
||||||
|
background: var(--fe-bg-elevated);
|
||||||
|
}
|
||||||
|
:deep(.el-input__inner) {
|
||||||
|
color: var(--fe-text-primary);
|
||||||
|
&::placeholder { color: var(--fe-text-muted); }
|
||||||
|
}
|
||||||
|
:deep(.el-input__wrapper.is-focus .el-input__inner::placeholder) {
|
||||||
|
color: var(--fe-text-secondary);
|
||||||
|
}
|
||||||
|
:deep(.el-input__prefix .el-icon) { color: var(--fe-text-muted); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 16px 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
.group-title {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--fe-accent);
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
.group-toggle {
|
||||||
|
color: var(--fe-text-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
&:hover .group-toggle { color: var(--fe-text-primary); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-items { padding: 0 8px; }
|
||||||
|
|
||||||
@keyframes sidebar-rotate {
|
@keyframes sidebar-rotate {
|
||||||
from { transform: rotate(0deg); }
|
from { transform: rotate(0deg); }
|
||||||
to { transform: rotate(360deg); }
|
to { transform: rotate(360deg); }
|
||||||
@@ -378,7 +510,7 @@ defineExpose({
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
border-radius: 8px;
|
border-radius: var(--fe-radius-md);
|
||||||
color: var(--fe-text-primary);
|
color: var(--fe-text-primary);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
|
||||||
|
|||||||
@@ -790,6 +790,13 @@ function handleNodeClick(data) {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
// 浅色主题下 el-scrollbar 滚动区背景设白
|
||||||
|
:deep(.el-scrollbar__wrap) {
|
||||||
|
body[data-fe-theme="light"] & {
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.loading-wrapper,
|
.loading-wrapper,
|
||||||
.empty-wrapper {
|
.empty-wrapper {
|
||||||
padding:20px;
|
padding:20px;
|
||||||
|
|||||||
@@ -11,11 +11,13 @@
|
|||||||
<div class="main-content">
|
<div class="main-content">
|
||||||
<!-- 顶部栏 -->
|
<!-- 顶部栏 -->
|
||||||
<ChatHeader
|
<ChatHeader
|
||||||
ref="chatHeaderRef"
|
|
||||||
:agent-name="currentConversation?.agentName || '请选择会话'"
|
:agent-name="currentConversation?.agentName || '请选择会话'"
|
||||||
:current-model="currentModel"
|
:current-model="currentModel"
|
||||||
@tab-change="handleTabChange"
|
:model-options="modelSelectOptions"
|
||||||
@model-change="handleModelChange"
|
:model-value="currentModel?.modelName || currentModel?.name || ''"
|
||||||
|
:file-panel-visible="filePanelVisible"
|
||||||
|
@update:model-value="handleModelSelectChange"
|
||||||
|
@toggle-file-panel="handleToggleFilePanel"
|
||||||
@clear="handleClear"
|
@clear="handleClear"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -183,14 +185,13 @@ const chatWindowRef = ref();
|
|||||||
const inputAreaRef = ref();
|
const inputAreaRef = ref();
|
||||||
const conversationSidebarRef = ref();
|
const conversationSidebarRef = ref();
|
||||||
const editorTextareaRef = ref();
|
const editorTextareaRef = ref();
|
||||||
const chatHeaderRef = ref();
|
|
||||||
|
|
||||||
const currentConversation = ref(null);
|
const currentConversation = ref(null);
|
||||||
const messages = ref([]);
|
const messages = ref([]);
|
||||||
const quotedFiles = ref([]);
|
const quotedFiles = ref([]);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const switchingConversation = ref(false);
|
const switchingConversation = ref(false);
|
||||||
const filePanelVisible = ref(false);
|
const filePanelVisible = ref(true);
|
||||||
|
|
||||||
// 智能体市场跳转带来的 agentId
|
// 智能体市场跳转带来的 agentId
|
||||||
const agentId = ref(null);
|
const agentId = ref(null);
|
||||||
@@ -206,7 +207,6 @@ const activeWorkspacePath = computed(() => {
|
|||||||
// 模型相关
|
// 模型相关
|
||||||
const currentModel = ref(null);
|
const currentModel = ref(null);
|
||||||
const modelList = ref([]);
|
const modelList = ref([]);
|
||||||
const showModelModal = ref(false);
|
|
||||||
|
|
||||||
// 文件预览相关
|
// 文件预览相关
|
||||||
const previewVisible = ref(false);
|
const previewVisible = ref(false);
|
||||||
@@ -307,6 +307,19 @@ async function loadAgentIfNeeded() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const modelSelectOptions = computed(() => modelList.value.map(m => ({
|
||||||
|
label: m.name || m.modelName || '',
|
||||||
|
value: m.modelName || m.name || ''
|
||||||
|
})))
|
||||||
|
|
||||||
|
function handleModelSelectChange(value) {
|
||||||
|
const found = modelList.value.find(m => (m.modelName || m.name) === value)
|
||||||
|
if (found) {
|
||||||
|
currentModel.value = found
|
||||||
|
ElMessage.success(`已选择模型: ${found.name || found.modelName}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 初始化模型列表
|
// 初始化模型列表
|
||||||
async function initModels() {
|
async function initModels() {
|
||||||
try {
|
try {
|
||||||
@@ -806,24 +819,13 @@ function transformMessages(rawMessages) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleTabChange(tab) {
|
|
||||||
if (tab === "agent") {
|
|
||||||
filePanelVisible.value = false;
|
|
||||||
previewVisible.value = false;
|
|
||||||
} else if (tab === "file") {
|
|
||||||
filePanelVisible.value = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleFilePanelClose() {
|
function handleFilePanelClose() {
|
||||||
filePanelVisible.value = false;
|
filePanelVisible.value = false;
|
||||||
previewVisible.value = false;
|
previewVisible.value = false;
|
||||||
chatHeaderRef.value?.setActiveTab('agent');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleModelChange(model) {
|
function handleToggleFilePanel() {
|
||||||
currentModel.value = model;
|
filePanelVisible.value = !filePanelVisible.value
|
||||||
ElMessage.success(`已选择模型: ${model.name || model.modelName || model.llmName}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleClear() {
|
function handleClear() {
|
||||||
|
|||||||
@@ -3,34 +3,34 @@
|
|||||||
// --fe- 前缀保证不会与 admin 主题变量冲突(admin 模块未使用 --fe-*)。
|
// --fe- 前缀保证不会与 admin 主题变量冲突(admin 模块未使用 --fe-*)。
|
||||||
:root {
|
:root {
|
||||||
// —— 基础色 ——
|
// —— 基础色 ——
|
||||||
--fe-bg-base: #1e1e1e;
|
--fe-bg-base: #14171c;
|
||||||
--fe-bg-elevated: #252526;
|
--fe-bg-elevated: #1c1f24;
|
||||||
--fe-bg-overlay: #2d2d30;
|
--fe-bg-overlay: #23272e;
|
||||||
--fe-bg-input: #3c3c3c;
|
--fe-bg-input: #2a2e35;
|
||||||
--fe-bg-hover: #2a2d2e;
|
--fe-bg-hover: #1f2329;
|
||||||
|
|
||||||
// —— 边框 ——
|
// —— 边框 ——
|
||||||
--fe-border: #3c3c3c;
|
--fe-border: #2c3138;
|
||||||
--fe-border-strong: #555555;
|
--fe-border-strong: #4a505a;
|
||||||
|
|
||||||
// —— 文本 ——
|
// —— 文本 ——
|
||||||
--fe-text-primary: #e6e6e6;
|
--fe-text-primary: #e4e6eb;
|
||||||
--fe-text-secondary: #a0a0a0;
|
--fe-text-secondary: #9aa0a6;
|
||||||
--fe-text-muted: #6e6e6e;
|
--fe-text-muted: #6b7280;
|
||||||
--fe-text-inverse: #1e1e1e;
|
--fe-text-inverse: #14171c;
|
||||||
|
|
||||||
// —— 品牌色 ——
|
// —— 品牌色 ——
|
||||||
--fe-accent: #4ec9b0;
|
--fe-accent: #6c8cff;
|
||||||
--fe-accent-hover: #5fd9c0;
|
--fe-accent-hover: #8aa3ff;
|
||||||
--fe-accent-soft: rgba(78, 201, 176, 0.15);
|
--fe-accent-soft: rgba(108, 140, 255, 0.15);
|
||||||
--fe-danger: #f48771;
|
--fe-danger: #f07178;
|
||||||
--fe-danger-soft: rgba(244, 135, 113, 0.1);
|
--fe-danger-soft: rgba(240, 113, 120, 0.1);
|
||||||
--fe-warning: #dcdcaa;
|
--fe-warning: #e0a050;
|
||||||
--fe-info: #569cd6;
|
--fe-info: #6cc4d6;
|
||||||
--fe-info-soft: rgba(86, 156, 214, 0.1);
|
--fe-info-soft: rgba(108, 196, 214, 0.1);
|
||||||
--fe-info-border: rgba(86, 156, 214, 0.4);
|
--fe-info-border: rgba(108, 196, 214, 0.4);
|
||||||
--fe-accent-border: rgba(78, 201, 176, 0.4);
|
--fe-accent-border: rgba(108, 140, 255, 0.4);
|
||||||
--fe-danger-border: rgba(244, 135, 113, 0.4);
|
--fe-danger-border: rgba(240, 113, 120, 0.4);
|
||||||
|
|
||||||
// —— 布局尺寸 ——
|
// —— 布局尺寸 ——
|
||||||
--fe-sidebar-width: 260px;
|
--fe-sidebar-width: 260px;
|
||||||
@@ -41,9 +41,9 @@
|
|||||||
--fe-radius-sm: 4px;
|
--fe-radius-sm: 4px;
|
||||||
--fe-radius-md: 6px;
|
--fe-radius-md: 6px;
|
||||||
--fe-radius-lg: 12px;
|
--fe-radius-lg: 12px;
|
||||||
--fe-shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3);
|
--fe-shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.5);
|
||||||
--fe-shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4);
|
--fe-shadow-md: 0 4px 12px rgba(0, 0, 0, 0.55);
|
||||||
--fe-shadow-lg: 0 4px 12px rgba(0, 0, 0, 0.5);
|
--fe-shadow-lg: 0 4px 12px rgba(0, 0, 0, 0.65);
|
||||||
}
|
}
|
||||||
|
|
||||||
// —— 浅色主题覆盖(通过 body[data-fe-theme="light"] 切换,由 settings.js 同步)——
|
// —— 浅色主题覆盖(通过 body[data-fe-theme="light"] 切换,由 settings.js 同步)——
|
||||||
@@ -215,6 +215,34 @@ body[data-fe-theme="light"] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// —— ElSelect 下拉面板(Teleport 到 body,需打 popper-class 标记)——
|
||||||
|
// 在 ChatHeader 模型下拉里使用 popper-class="frontend-el-select-dropdown",
|
||||||
|
// 让弹出的 .el-select-dropdown 继承 frontend 主题。
|
||||||
|
.frontend-el-select-dropdown {
|
||||||
|
--el-bg-color: var(--fe-bg-elevated);
|
||||||
|
--el-bg-color-overlay: var(--fe-bg-overlay);
|
||||||
|
--el-text-color-primary: var(--fe-text-primary);
|
||||||
|
--el-text-color-regular: var(--fe-text-secondary);
|
||||||
|
--el-text-color-secondary: var(--fe-text-muted);
|
||||||
|
--el-text-color-placeholder: var(--fe-text-muted);
|
||||||
|
--el-border-color: var(--fe-border);
|
||||||
|
--el-border-color-light: var(--fe-border);
|
||||||
|
--el-border-color-lighter: var(--fe-border);
|
||||||
|
--el-border-color-extra-light: var(--fe-border);
|
||||||
|
--el-fill-color-blank: var(--fe-bg-elevated);
|
||||||
|
--el-fill-color-light: var(--fe-bg-hover);
|
||||||
|
--el-fill-color-lighter: var(--fe-bg-hover);
|
||||||
|
--el-color-primary: var(--fe-accent);
|
||||||
|
--el-color-primary-light-3: var(--fe-accent-hover);
|
||||||
|
--el-color-primary-light-5: var(--fe-accent-soft);
|
||||||
|
--el-color-primary-light-7: var(--fe-accent-soft);
|
||||||
|
--el-color-primary-light-9: var(--fe-accent-soft);
|
||||||
|
--el-color-danger: var(--fe-danger);
|
||||||
|
|
||||||
|
border: 1px solid var(--fe-border);
|
||||||
|
box-shadow: var(--fe-shadow-md);
|
||||||
|
}
|
||||||
|
|
||||||
// —— 全局右键菜单(用于会话列表 / 文件面板等,通过 Teleport 渲染到 body)——
|
// —— 全局右键菜单(用于会话列表 / 文件面板等,通过 Teleport 渲染到 body)——
|
||||||
// --fe-* 变量在 :root 全局可继承,菜单元素样式不受所在 DOM 位置影响。
|
// --fe-* 变量在 :root 全局可继承,菜单元素样式不受所在 DOM 位置影响。
|
||||||
.context-menu {
|
.context-menu {
|
||||||
|
|||||||
Reference in New Issue
Block a user