282 lines
6.4 KiB
Vue
282 lines
6.4 KiB
Vue
<template>
|
|
<div class="chat-sidebar">
|
|
<div class="sidebar-header">
|
|
<el-button type="primary" class="new-chat-btn" :icon="ChatDotRound" @click="handleNewChat">
|
|
新建对话
|
|
</el-button>
|
|
<el-button :icon="Cpu" class="mcp-btn" @click="handleMcpConfig" title="MCP 工具中心" />
|
|
<el-button :icon="Setting" class="settings-btn" @click="settingsVisible = true" title="AI接入设置" />
|
|
</div>
|
|
|
|
<div class="session-list" v-loading="loading">
|
|
<div
|
|
v-for="item in list"
|
|
:key="item.id"
|
|
class="session-item"
|
|
:class="{ active: currentId === item.id }"
|
|
@click="handleSelect(item)"
|
|
>
|
|
<el-icon class="session-icon"><ChatDotRound /></el-icon>
|
|
<template v-if="editingId === item.id">
|
|
<el-input
|
|
v-model="editTitle"
|
|
size="small"
|
|
class="rename-input"
|
|
@click.stop
|
|
@keyup.enter="saveRename(item)"
|
|
@keyup.esc="cancelRename"
|
|
@blur="saveRename(item)"
|
|
ref="renameInputRef"
|
|
/>
|
|
</template>
|
|
<span v-else class="session-title" :title="item.title">{{ item.title }}</span>
|
|
<div class="session-actions">
|
|
<el-button
|
|
link
|
|
type="primary"
|
|
class="action-btn"
|
|
:icon="Edit"
|
|
@click.stop="startRename(item)"
|
|
title="重命名"
|
|
/>
|
|
<el-button
|
|
link
|
|
type="danger"
|
|
class="action-btn"
|
|
:icon="Delete"
|
|
@click.stop="handleDelete(item)"
|
|
title="删除"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<el-empty v-if="!loading && list.length === 0" description="暂无对话" :image-size="60" />
|
|
</div>
|
|
|
|
<ProviderDialog v-model="settingsVisible" @change="handleProviderChange" />
|
|
<McpConfig v-model="mcpConfigVisible" />
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted, nextTick, watch } from 'vue'
|
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
import { ChatDotRound, Setting, Delete, Edit, Cpu } from '@element-plus/icons-vue'
|
|
import { getChatSessionList, createChatSession, deleteChatSession, updateChatSession } from '@/api/ai'
|
|
import ProviderDialog from './providerDialog.vue'
|
|
import McpConfig from './mcpConfig.vue'
|
|
|
|
const emit = defineEmits(['select', 'new', 'rename'])
|
|
|
|
const loading = ref(false)
|
|
const list = ref([])
|
|
const currentId = ref(null)
|
|
const settingsVisible = ref(false)
|
|
const mcpConfigVisible = ref(false)
|
|
|
|
// 重命名
|
|
const editingId = ref(null)
|
|
const editTitle = ref('')
|
|
const renameInputRef = ref()
|
|
|
|
onMounted(() => {
|
|
fetchList()
|
|
})
|
|
|
|
async function fetchList() {
|
|
loading.value = true
|
|
try {
|
|
const res = await getChatSessionList()
|
|
if (res.code === 200) {
|
|
list.value = res.data.list || []
|
|
}
|
|
} catch (e) {
|
|
ElMessage.error(e.message || '查询失败')
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
function handleSelect(item) {
|
|
currentId.value = item.id
|
|
emit('select', item)
|
|
}
|
|
|
|
async function handleNewChat() {
|
|
try {
|
|
const res = await createChatSession({ title: '新对话' })
|
|
if (res.code === 200) {
|
|
const newSession = { id: res.data.id, title: res.data.title }
|
|
list.value.unshift(newSession)
|
|
currentId.value = newSession.id
|
|
emit('new', newSession)
|
|
}
|
|
} catch (e) {
|
|
ElMessage.error(e.message || '创建失败')
|
|
}
|
|
}
|
|
|
|
function startRename(item) {
|
|
editingId.value = item.id
|
|
editTitle.value = item.title
|
|
nextTick(() => {
|
|
if (renameInputRef.value) {
|
|
const input = renameInputRef.value[0] || renameInputRef.value
|
|
if (input && input.focus) input.focus()
|
|
if (input && input.select) input.select()
|
|
}
|
|
})
|
|
}
|
|
|
|
function cancelRename() {
|
|
editingId.value = null
|
|
editTitle.value = ''
|
|
}
|
|
|
|
async function saveRename(item) {
|
|
if (editingId.value !== item.id) return
|
|
const newTitle = editTitle.value.trim()
|
|
if (!newTitle) {
|
|
ElMessage.warning('名称不能为空')
|
|
return
|
|
}
|
|
if (newTitle === item.title) {
|
|
cancelRename()
|
|
return
|
|
}
|
|
|
|
try {
|
|
await updateChatSession(item.id, { title: newTitle })
|
|
item.title = newTitle
|
|
ElMessage.success('重命名成功')
|
|
// 如果是当前会话,通知父组件更新标题
|
|
if (currentId.value === item.id) {
|
|
emit('rename', { id: item.id, title: newTitle })
|
|
}
|
|
} catch (e) {
|
|
ElMessage.error(e.message || '重命名失败')
|
|
} finally {
|
|
cancelRename()
|
|
}
|
|
}
|
|
|
|
async function handleDelete(item) {
|
|
try {
|
|
await ElMessageBox.confirm(`确定要删除对话「${item.title}」吗?`, '提示', { type: 'warning' })
|
|
await deleteChatSession(item.id)
|
|
ElMessage.success('删除成功')
|
|
list.value = list.value.filter(s => s.id !== item.id)
|
|
if (currentId.value === item.id) {
|
|
currentId.value = null
|
|
emit('select', null)
|
|
}
|
|
} catch (e) {
|
|
if (e !== 'cancel') {
|
|
ElMessage.error(e.message || '删除失败')
|
|
}
|
|
}
|
|
}
|
|
|
|
function handleProviderChange() {
|
|
// 配置变更后不需要特殊处理
|
|
}
|
|
|
|
function handleMcpConfig() {
|
|
mcpConfigVisible.value = true
|
|
}
|
|
|
|
defineExpose({
|
|
fetchList,
|
|
setCurrent: (id) => { currentId.value = id }
|
|
})
|
|
</script>
|
|
|
|
<style lang="less" scoped>
|
|
.chat-sidebar {
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100%;
|
|
background: var(--el-fill-color-light);
|
|
border-right: 1px solid var(--el-border-color-lighter);
|
|
}
|
|
|
|
.sidebar-header {
|
|
display: flex;
|
|
gap: 8px;
|
|
padding: 16px;
|
|
border-bottom: 1px solid var(--el-border-color-lighter);
|
|
|
|
.new-chat-btn {
|
|
flex: 1;
|
|
}
|
|
|
|
.settings-btn {
|
|
width: 36px;
|
|
padding: 0;
|
|
}
|
|
|
|
.mcp-btn {
|
|
width: 36px;
|
|
padding: 0;
|
|
}
|
|
}
|
|
|
|
.session-list {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 8px;
|
|
}
|
|
|
|
.session-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 10px 12px;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
margin-bottom: 4px;
|
|
transition: background 0.2s;
|
|
|
|
&:hover {
|
|
background: var(--el-fill-color);
|
|
}
|
|
|
|
&.active {
|
|
background: var(--el-color-primary-light-9);
|
|
color: var(--el-color-primary);
|
|
}
|
|
|
|
.session-icon {
|
|
flex-shrink: 0;
|
|
font-size: 16px;
|
|
}
|
|
|
|
.session-title {
|
|
flex: 1;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.rename-input {
|
|
flex: 1;
|
|
}
|
|
|
|
.session-actions {
|
|
display: flex;
|
|
gap: 2px;
|
|
flex-shrink: 0;
|
|
|
|
.action-btn {
|
|
padding: 0;
|
|
opacity: 0.6;
|
|
transition: opacity 0.2s;
|
|
|
|
&:hover {
|
|
opacity: 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</style>
|