feat: 重构布局
This commit is contained in:
parent
c6b4744840
commit
dd1fdd16ec
|
|
@ -0,0 +1,590 @@
|
|||
<template>
|
||||
<div class="main-container" :class="{ 'fullscreen-mode': isFullscreen }">
|
||||
<!-- 全屏按钮 -->
|
||||
<div class="fullscreen-button" @click="toggleFullscreen">
|
||||
<el-tooltip :content="isFullscreen ? '退出全屏' : '全屏显示'" placement="left">
|
||||
<el-icon :size="20">
|
||||
<component :is="isFullscreen ? 'Close' : 'FullScreen'" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
|
||||
<!-- 知识库选择按钮 -->
|
||||
<div class="knowledge-button" @click="openKnowledgeDialog">
|
||||
<el-tooltip content="选择知识库" placement="left">
|
||||
<el-badge :value="boundKnowledgeBases.length" :hidden="boundKnowledgeBases.length === 0" type="primary">
|
||||
<el-icon :size="20">
|
||||
<Collection />
|
||||
</el-icon>
|
||||
</el-badge>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
|
||||
<ChatBox
|
||||
v-if="isDataLoaded"
|
||||
:chat-type="chatType"
|
||||
:user-id="userId"
|
||||
:placeholder="placeholder"
|
||||
:recommend-questions="recommendQuestions"
|
||||
@stream-complete="handleStreamComplete"
|
||||
@source-click="handleSourceClick"
|
||||
@message-received="handleMessageReceived"
|
||||
@chat-created="handleChatCreated"
|
||||
@chat-switched="handleChatSwitched"
|
||||
/>
|
||||
|
||||
<!-- 知识库选择对话框 -->
|
||||
<el-dialog
|
||||
v-model="knowledgeDialogVisible"
|
||||
title="选择知识库"
|
||||
width="600px"
|
||||
append-to-body
|
||||
:z-index="isFullscreen ? 20000 : 2000"
|
||||
@close="closeKnowledgeDialog"
|
||||
>
|
||||
<div class="knowledge-dialog-content">
|
||||
<div class="bound-knowledge" v-if="boundKnowledgeBases.length > 0">
|
||||
<div class="section-title">已绑定知识库</div>
|
||||
<div class="knowledge-list">
|
||||
<el-tag
|
||||
v-for="kb in boundKnowledgeBases"
|
||||
:key="kb.id"
|
||||
closable
|
||||
@close="removeKnowledgeBase(kb.id)"
|
||||
style="margin-right: 8px; margin-bottom: 8px;"
|
||||
type="success"
|
||||
>
|
||||
{{ kb.name }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="no-binding-hint">
|
||||
<el-empty description="暂未绑定知识库" :image-size="80" />
|
||||
</div>
|
||||
|
||||
<div class="add-knowledge">
|
||||
<div class="section-title">添加知识库</div>
|
||||
<el-select
|
||||
v-model="selectedKnowledgeBaseIds"
|
||||
placeholder="请选择要绑定的知识库"
|
||||
style="width: 100%"
|
||||
filterable
|
||||
multiple
|
||||
:loading="isLoadingKnowledgeBases"
|
||||
:popper-class="isFullscreen ? 'fullscreen-select-dropdown' : ''"
|
||||
:teleported="true"
|
||||
>
|
||||
<el-option
|
||||
v-for="kb in availableKnowledgeBases"
|
||||
:key="kb.id"
|
||||
:label="kb.name"
|
||||
:value="kb.id"
|
||||
>
|
||||
<div style="display: flex; flex-direction: column;">
|
||||
<span>{{ kb.name }}</span>
|
||||
<span v-if="kb.description" style="font-size: 12px; color: #909399;">
|
||||
{{ kb.description }}
|
||||
</span>
|
||||
</div>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="closeKnowledgeDialog">取消</el-button>
|
||||
<el-button type="primary" @click="confirmBindKnowledgeBases" :loading="isBinding">
|
||||
确定
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import ChatBox from '@/views/chatweb/components/ChatBox.vue'
|
||||
import { useAclStore } from '@/store/modules/acl'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { getRecommendations } from '@/api/prologue'
|
||||
import { FullScreen, Close, Collection } from '@element-plus/icons-vue'
|
||||
import {
|
||||
getWorkflowAndDatasetTableData,
|
||||
getAllUserDatasets,
|
||||
addDatasetsToWorkflow,
|
||||
removeDatasetsFromWorkflow
|
||||
} from '@/api/functionKnowledgeConfig'
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
const aclStore = useAclStore()
|
||||
|
||||
// 状态管理
|
||||
const placeholder = ref('给AI助手发送消息')
|
||||
const chatType = ref('7')
|
||||
const userId = computed(() => aclStore.getUserId)
|
||||
const recommendQuestions = ref<string[]>([])
|
||||
const isDataLoaded = ref(false)
|
||||
const isFullscreen = ref(false)
|
||||
|
||||
// 固定的 appId
|
||||
const APP_ID = '29286309-5c1c-4fea-84a6-406c69936a45'
|
||||
|
||||
// 知识库相关状态
|
||||
const knowledgeDialogVisible = ref(false)
|
||||
const boundKnowledgeBases = ref<Array<{ id: string; name: string; description?: string }>>([])
|
||||
const allKnowledgeBases = ref<Array<{ id: string; name: string; description?: string }>>([])
|
||||
const selectedKnowledgeBaseIds = ref<string[]>([])
|
||||
const isLoadingKnowledgeBases = ref(false)
|
||||
const isBinding = ref(false)
|
||||
|
||||
// 计算可用的知识库(排除已绑定的)
|
||||
const availableKnowledgeBases = computed(() => {
|
||||
const boundIds = boundKnowledgeBases.value.map(kb => kb.id)
|
||||
return allKnowledgeBases.value.filter(kb => !boundIds.includes(kb.id))
|
||||
})
|
||||
|
||||
// 获取推荐问题
|
||||
|
||||
|
||||
// 获取所有可用知识库
|
||||
const fetchAllKnowledgeBases = async () => {
|
||||
try {
|
||||
isLoadingKnowledgeBases.value = true
|
||||
const response: any = await getAllUserDatasets()
|
||||
|
||||
if (response && response.code === 200 && response.data) {
|
||||
const userDatasets = response.data
|
||||
|
||||
// 提取所有唯一的知识库
|
||||
const knowledgeBasesMap = new Map()
|
||||
userDatasets.forEach((item: any) => {
|
||||
if (!knowledgeBasesMap.has(item.datasetId)) {
|
||||
knowledgeBasesMap.set(item.datasetId, {
|
||||
id: item.datasetId,
|
||||
name: item.datasetName,
|
||||
description: item.datasetDescription,
|
||||
createAt: item.datasetCreateAt,
|
||||
updateAt: item.datasetUpdateAt
|
||||
})
|
||||
}
|
||||
})
|
||||
allKnowledgeBases.value = Array.from(knowledgeBasesMap.values())
|
||||
console.log('获取所有知识库成功:', allKnowledgeBases.value)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取所有知识库失败:', error)
|
||||
ElMessage.error('获取知识库列表失败')
|
||||
} finally {
|
||||
isLoadingKnowledgeBases.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前对话绑定的知识库
|
||||
const fetchBoundKnowledgeBases = async () => {
|
||||
try {
|
||||
const response: any = await getWorkflowAndDatasetTableData()
|
||||
|
||||
if (response && response.code === 200 && response.data) {
|
||||
// 查找当前 APP_ID 对应的数据
|
||||
const currentApp = response.data.find((item: any) => item.appId === APP_ID)
|
||||
|
||||
if (currentApp && currentApp.datasetIds && currentApp.datasetIds.length > 0) {
|
||||
boundKnowledgeBases.value = []
|
||||
for (let i = 0; i < currentApp.datasetIds.length; i++) {
|
||||
const datasetId = currentApp.datasetIds[i]
|
||||
const datasetName = currentApp.datasetNames[i] || datasetId
|
||||
|
||||
boundKnowledgeBases.value.push({
|
||||
id: datasetId,
|
||||
name: datasetName
|
||||
})
|
||||
}
|
||||
console.log('获取已绑定知识库成功:', boundKnowledgeBases.value)
|
||||
} else {
|
||||
boundKnowledgeBases.value = []
|
||||
console.log('当前对话未绑定知识库')
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取已绑定知识库失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 打开知识库选择对话框
|
||||
const openKnowledgeDialog = async () => {
|
||||
knowledgeDialogVisible.value = true
|
||||
selectedKnowledgeBaseIds.value = []
|
||||
|
||||
// 如果还没有加载过知识库列表,则加载
|
||||
if (allKnowledgeBases.value.length === 0) {
|
||||
await fetchAllKnowledgeBases()
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭知识库选择对话框
|
||||
const closeKnowledgeDialog = () => {
|
||||
knowledgeDialogVisible.value = false
|
||||
selectedKnowledgeBaseIds.value = []
|
||||
}
|
||||
|
||||
// 确认绑定知识库
|
||||
const confirmBindKnowledgeBases = async () => {
|
||||
if (!selectedKnowledgeBaseIds.value || selectedKnowledgeBaseIds.value.length === 0) {
|
||||
ElMessage.warning('请选择要绑定的知识库')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
isBinding.value = true
|
||||
|
||||
// 调用API绑定知识库
|
||||
const response: any = await addDatasetsToWorkflow({
|
||||
appId: APP_ID,
|
||||
datasetIds: selectedKnowledgeBaseIds.value
|
||||
})
|
||||
|
||||
if (response.code === 200) {
|
||||
// 更新本地数据
|
||||
selectedKnowledgeBaseIds.value.forEach(id => {
|
||||
const kb = allKnowledgeBases.value.find(k => k.id === id)
|
||||
if (kb && !boundKnowledgeBases.value.find(b => b.id === id)) {
|
||||
boundKnowledgeBases.value.push(kb)
|
||||
}
|
||||
})
|
||||
|
||||
ElMessage.success(`成功绑定 ${selectedKnowledgeBaseIds.value.length} 个知识库`)
|
||||
selectedKnowledgeBaseIds.value = []
|
||||
closeKnowledgeDialog()
|
||||
} else {
|
||||
ElMessage.error(response.msg || '绑定失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('绑定知识库失败:', error)
|
||||
ElMessage.error('绑定失败:' + (error.message || error))
|
||||
} finally {
|
||||
isBinding.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 移除知识库绑定
|
||||
const removeKnowledgeBase = async (knowledgeBaseId: string) => {
|
||||
try {
|
||||
// 调用API解绑知识库
|
||||
const response: any = await removeDatasetsFromWorkflow({
|
||||
appId: APP_ID,
|
||||
datasetIds: [knowledgeBaseId]
|
||||
})
|
||||
|
||||
if (response.code === 200) {
|
||||
// 更新本地数据
|
||||
const index = boundKnowledgeBases.value.findIndex(kb => kb.id === knowledgeBaseId)
|
||||
if (index > -1) {
|
||||
const removedKb = boundKnowledgeBases.value[index]
|
||||
boundKnowledgeBases.value.splice(index, 1)
|
||||
ElMessage({
|
||||
message: `已移除知识库:${removedKb.name}`,
|
||||
type: 'info',
|
||||
duration: 2000,
|
||||
showClose: true,
|
||||
customClass: isFullscreen.value ? 'fullscreen-message' : ''
|
||||
})
|
||||
}
|
||||
} else {
|
||||
ElMessage({
|
||||
message: response.msg || '移除失败',
|
||||
type: 'error',
|
||||
duration: 2000,
|
||||
showClose: true,
|
||||
customClass: isFullscreen.value ? 'fullscreen-message' : ''
|
||||
})
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('移除知识库失败:', error)
|
||||
ElMessage({
|
||||
message: '移除失败:' + (error.message || error),
|
||||
type: 'error',
|
||||
duration: 2000,
|
||||
showClose: true,
|
||||
customClass: isFullscreen.value ? 'fullscreen-message' : ''
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ChatBox组件事件处理器
|
||||
|
||||
// 处理流式请求完成事件
|
||||
const handleStreamComplete = (data: {
|
||||
conversationId: string
|
||||
messageId: string
|
||||
content: string
|
||||
}) => {
|
||||
console.log('通用问答 - 对话完成:', data)
|
||||
}
|
||||
|
||||
// 处理文件来源点击事件
|
||||
const handleSourceClick = (source: any) => {
|
||||
console.log('通用问答 - 点击文件来源:', source)
|
||||
ElMessage.info(`点击了文件: ${source.fileName}`)
|
||||
}
|
||||
|
||||
// 处理消息接收事件
|
||||
const handleMessageReceived = (message: any) => {
|
||||
console.log('通用问答 - 收到新消息:', message)
|
||||
}
|
||||
|
||||
// 处理新会话创建事件
|
||||
const handleChatCreated = (chatIndex: number) => {
|
||||
console.log('通用问答 - 创建新会话:', chatIndex)
|
||||
}
|
||||
|
||||
// 处理会话切换事件
|
||||
const handleChatSwitched = (chatIndex: number) => {
|
||||
console.log('通用问答 - 切换会话:', chatIndex)
|
||||
}
|
||||
|
||||
// 全屏功能
|
||||
const toggleFullscreen = () => {
|
||||
isFullscreen.value = !isFullscreen.value
|
||||
|
||||
// 隐藏或显示布局的其他部分
|
||||
const layoutWrapper = document.querySelector('.vue-admin-better-wrapper') as HTMLElement
|
||||
if (layoutWrapper) {
|
||||
if (isFullscreen.value) {
|
||||
layoutWrapper.style.overflow = 'hidden'
|
||||
// 给 body 添加全屏模式类名,用于控制弹出框 z-index
|
||||
document.body.classList.add('chat-fullscreen-mode')
|
||||
} else {
|
||||
layoutWrapper.style.overflow = ''
|
||||
// 移除全屏模式类名
|
||||
document.body.classList.remove('chat-fullscreen-mode')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 监听 ESC 键退出全屏
|
||||
const handleEscKey = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape' && isFullscreen.value) {
|
||||
toggleFullscreen()
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载时初始化
|
||||
onMounted(async () => {
|
||||
console.log('组件已挂载,开始加载数据...')
|
||||
// await fetchRecommendations()
|
||||
|
||||
// 加载已绑定的知识库
|
||||
await fetchBoundKnowledgeBases()
|
||||
|
||||
isDataLoaded.value = true
|
||||
console.log('数据加载完成')
|
||||
|
||||
// 添加键盘事件监听
|
||||
window.addEventListener('keydown', handleEscKey)
|
||||
})
|
||||
|
||||
// 组件卸载时清理
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('keydown', handleEscKey)
|
||||
|
||||
// 确保退出全屏状态
|
||||
if (isFullscreen.value) {
|
||||
const layoutWrapper = document.querySelector('.vue-admin-better-wrapper') as HTMLElement
|
||||
if (layoutWrapper) {
|
||||
layoutWrapper.style.overflow = ''
|
||||
}
|
||||
// 移除全屏模式类名
|
||||
document.body.classList.remove('chat-fullscreen-mode')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.main-container {
|
||||
position: relative;
|
||||
height: 100vh;
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 全屏按钮 */
|
||||
.fullscreen-button {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
z-index: 100;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--el-bg-color);
|
||||
border: 1px solid var(--el-border-color);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.fullscreen-button:hover {
|
||||
background: var(--el-color-primary);
|
||||
color: white;
|
||||
border-color: var(--el-color-primary);
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.fullscreen-button :deep(.el-icon) {
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.fullscreen-button:hover :deep(.el-icon) {
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 知识库按钮 */
|
||||
.knowledge-button {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 64px;
|
||||
z-index: 100;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--el-bg-color);
|
||||
border: 1px solid var(--el-border-color);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.knowledge-button:hover {
|
||||
background: var(--el-color-primary);
|
||||
color: white;
|
||||
border-color: var(--el-color-primary);
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.knowledge-button :deep(.el-icon) {
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.knowledge-button:hover :deep(.el-icon) {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.knowledge-button :deep(.el-badge__content) {
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* 全屏模式样式 */
|
||||
.main-container.fullscreen-mode {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
z-index: 9999;
|
||||
background: var(--el-bg-color);
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.main-container.fullscreen-mode .fullscreen-button,
|
||||
.main-container.fullscreen-mode .knowledge-button {
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
/* 知识库对话框样式 */
|
||||
.knowledge-dialog-content {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin-bottom: 12px;
|
||||
font-weight: 500;
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.bound-knowledge {
|
||||
margin-bottom: 24px;
|
||||
padding: 16px;
|
||||
background-color: #f5f7fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.knowledge-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.no-binding-hint {
|
||||
margin-bottom: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.add-knowledge {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.el-tag {
|
||||
font-size: 13px;
|
||||
padding: 6px 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
/* 全局样式:全屏模式下的下拉框和弹出框 z-index */
|
||||
.fullscreen-select-dropdown {
|
||||
z-index: 20001 !important;
|
||||
}
|
||||
|
||||
/* 全屏模式下的消息提示框样式 */
|
||||
.fullscreen-message {
|
||||
z-index: 20003 !important;
|
||||
position: fixed !important;
|
||||
top: 20px !important;
|
||||
left: 50% !important;
|
||||
transform: translateX(-50%) !important;
|
||||
min-width: 380px !important;
|
||||
padding: 15px 20px !important;
|
||||
border-radius: 8px !important;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important;
|
||||
}
|
||||
|
||||
/* 全屏模式下的其他弹出框 */
|
||||
.main-container.fullscreen-mode ~ .el-dialog__wrapper {
|
||||
z-index: 20000 !important;
|
||||
}
|
||||
|
||||
.main-container.fullscreen-mode ~ .el-loading-mask {
|
||||
z-index: 20001 !important;
|
||||
}
|
||||
|
||||
.main-container.fullscreen-mode ~ .el-notification {
|
||||
z-index: 20003 !important;
|
||||
}
|
||||
|
||||
/* 全屏模式下的遮罩层 */
|
||||
.main-container.fullscreen-mode ~ .v-modal {
|
||||
z-index: 19999 !important;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,474 @@
|
|||
<template>
|
||||
<div class="centered-input-wrapper">
|
||||
<div class="large-input-container">
|
||||
<el-input
|
||||
v-model="localMessage"
|
||||
:placeholder="placeholder"
|
||||
:autosize="{ minRows: 2, maxRows: 6 }"
|
||||
type="textarea"
|
||||
resize="none"
|
||||
@keydown.enter.exact="handleSend"
|
||||
class="large-message-input"
|
||||
/>
|
||||
|
||||
<!-- 底部工具栏 -->
|
||||
<div class="input-toolbar">
|
||||
<!-- 左侧:选择知识库和附件 -->
|
||||
<div class="toolbar-left">
|
||||
<!-- <el-select
|
||||
v-model="selectedKnowledgeBase"
|
||||
placeholder="选择知识库"
|
||||
size="default"
|
||||
class="knowledge-select"
|
||||
clearable
|
||||
>
|
||||
<el-option
|
||||
v-for="kb in knowledgeBases"
|
||||
:key="kb.value"
|
||||
:label="kb.label"
|
||||
:value="kb.value"
|
||||
/>
|
||||
</el-select>
|
||||
|
||||
<el-upload
|
||||
ref="uploadRef"
|
||||
:show-file-list="false"
|
||||
:on-change="handleFileChange"
|
||||
:auto-upload="false"
|
||||
accept="image/*,.pdf,.doc,.docx,.txt"
|
||||
class="upload-wrapper"
|
||||
>
|
||||
<el-button
|
||||
text
|
||||
:icon="Paperclip"
|
||||
class="attach-btn"
|
||||
title="添加附件"
|
||||
>
|
||||
<span v-if="attachedFiles.length > 0" class="file-count">{{ attachedFiles.length }}</span>
|
||||
</el-button>
|
||||
</el-upload>
|
||||
|
||||
<div v-if="attachedFiles.length > 0" class="attached-files">
|
||||
<el-tag
|
||||
v-for="(file, index) in attachedFiles"
|
||||
:key="index"
|
||||
closable
|
||||
@close="removeFile(index)"
|
||||
size="small"
|
||||
type="info"
|
||||
>
|
||||
{{ file.name }}
|
||||
</el-tag>
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
<!-- 右侧:操作按钮和提示 -->
|
||||
<div class="toolbar-right">
|
||||
<span v-if="localMessage.trim() && !isLoading" class="char-count">{{ localMessage.length }}</span>
|
||||
<span v-if="!isLoading" class="shortcut-hint">Enter</span>
|
||||
<el-button
|
||||
v-if="!isLoading"
|
||||
circle
|
||||
text
|
||||
@click="handleVoiceInput"
|
||||
:icon="Microphone"
|
||||
class="action-btn"
|
||||
title="语音输入"
|
||||
/>
|
||||
<!-- 发送按钮 / 停止按钮切换 -->
|
||||
<el-button
|
||||
v-if="!isLoading"
|
||||
circle
|
||||
type="primary"
|
||||
@click="handleSend"
|
||||
:disabled="!localMessage.trim()"
|
||||
:icon="Position"
|
||||
class="send-btn"
|
||||
title="发送消息 (Enter)"
|
||||
/>
|
||||
<el-button
|
||||
v-else
|
||||
circle
|
||||
@click="handleStop"
|
||||
class="stop-btn"
|
||||
title="停止生成"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" class="stop-icon">
|
||||
<rect x="4" y="4" width="15" height="15" rx="3" />
|
||||
</svg>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { Microphone, Position, Paperclip } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { UploadFile } from 'element-plus'
|
||||
|
||||
interface Props {
|
||||
modelValue: string
|
||||
placeholder?: string
|
||||
isLoading?: boolean
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: string): void
|
||||
(e: 'send-message', message: string): void
|
||||
(e: 'stop-stream'): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
placeholder: '给AI助手发送消息',
|
||||
isLoading: false
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const localMessage = ref(props.modelValue)
|
||||
const selectedKnowledgeBase = ref('')
|
||||
const attachedFiles = ref<UploadFile[]>([])
|
||||
const uploadRef = ref()
|
||||
|
||||
// 同步双向绑定
|
||||
watch(() => props.modelValue, (val) => {
|
||||
localMessage.value = val
|
||||
})
|
||||
|
||||
watch(localMessage, (val) => {
|
||||
emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
// 知识库列表
|
||||
const knowledgeBases = ref([
|
||||
{ label: '通用知识库', value: 'general' },
|
||||
{ label: '技术文档库', value: 'tech' },
|
||||
{ label: '业务知识库', value: 'business' },
|
||||
{ label: '检修手册库', value: 'manual' },
|
||||
])
|
||||
|
||||
// 处理文件上传
|
||||
const handleFileChange = (file: UploadFile) => {
|
||||
if (attachedFiles.value.length >= 5) {
|
||||
ElMessage.warning('最多只能上传5个文件')
|
||||
return
|
||||
}
|
||||
attachedFiles.value.push(file)
|
||||
ElMessage.success(`已添加文件: ${file.name}`)
|
||||
}
|
||||
|
||||
// 移除文件
|
||||
const removeFile = (index: number) => {
|
||||
attachedFiles.value.splice(index, 1)
|
||||
}
|
||||
|
||||
// 发送消息
|
||||
const handleSend = (event?: KeyboardEvent | MouseEvent) => {
|
||||
if (event) {
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
if (!localMessage.value.trim() || props.isLoading) {
|
||||
return
|
||||
}
|
||||
|
||||
emit('send-message', localMessage.value)
|
||||
localMessage.value = ''
|
||||
attachedFiles.value = []
|
||||
selectedKnowledgeBase.value = ''
|
||||
}
|
||||
|
||||
// 停止生成
|
||||
const handleStop = () => {
|
||||
emit('stop-stream')
|
||||
}
|
||||
|
||||
// 语音输入
|
||||
const handleVoiceInput = () => {
|
||||
ElMessage.info('语音输入功能开发中...')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.centered-input-wrapper {
|
||||
width: 100%;
|
||||
max-width: 900px;
|
||||
animation: fadeInUp 0.6s ease-out;
|
||||
}
|
||||
|
||||
.large-input-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #ffffff;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
transition: all 0.3s ease;
|
||||
overflow: hidden;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 6px 30px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.large-message-input {
|
||||
:deep(.el-textarea__inner) {
|
||||
border: none;
|
||||
border-radius: 16px;
|
||||
padding: 20px 24px;
|
||||
padding-bottom: 70px;
|
||||
font-size: 15px;
|
||||
line-height: 1.8;
|
||||
min-height: 120px;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: none;
|
||||
background: transparent;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: #9ca3af;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.input-toolbar {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 24px;
|
||||
background: transparent;
|
||||
|
||||
.toolbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.knowledge-select {
|
||||
width: 200px;
|
||||
|
||||
:deep(.el-input__wrapper) {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 0 0 1px #e5e7eb inset;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 0 1px #6366f1 inset;
|
||||
}
|
||||
|
||||
&.is-focus {
|
||||
box-shadow: 0 0 0 1px #6366f1 inset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.upload-wrapper {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.attach-btn {
|
||||
position: relative;
|
||||
font-size: 20px;
|
||||
color: #6b7280;
|
||||
transition: all 0.3s ease;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: rgba(99, 102, 241, 0.08);
|
||||
|
||||
&:hover {
|
||||
color: #6366f1;
|
||||
background: rgba(99, 102, 241, 0.15);
|
||||
}
|
||||
|
||||
.file-count {
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
right: -4px;
|
||||
background: #ef4444;
|
||||
color: #ffffff;
|
||||
font-size: 10px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.attached-files {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar-right {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
|
||||
.char-count {
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
padding: 0 8px;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
.shortcut-hint {
|
||||
font-size: 11px;
|
||||
color: #9ca3af;
|
||||
background: rgba(99, 102, 241, 0.06);
|
||||
padding: 4px 8px;
|
||||
border-radius: 6px;
|
||||
font-family: monospace;
|
||||
border: 1px solid rgba(99, 102, 241, 0.1);
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
font-size: 20px;
|
||||
color: #6b7280;
|
||||
transition: all 0.3s ease;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: rgba(99, 102, 241, 0.08);
|
||||
|
||||
&:hover {
|
||||
color: #6366f1;
|
||||
transform: scale(1.1);
|
||||
background: rgba(99, 102, 241, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
.send-btn {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
||||
border: none;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.4);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: #93c5fd;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.stop-btn {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
||||
border: none;
|
||||
transition: all 0.3s ease;
|
||||
animation: pulseGlow 2s ease-in-out infinite;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.5);
|
||||
animation: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.stop-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
fill: white;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
&:hover .stop-icon {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulseGlow {
|
||||
0%, 100% {
|
||||
box-shadow: 0 2px 8px rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 4px 16px rgba(239, 68, 68, 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.large-input-container {
|
||||
.large-message-input {
|
||||
:deep(.el-textarea__inner) {
|
||||
padding: 16px;
|
||||
padding-bottom: 120px;
|
||||
min-height: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.input-toolbar {
|
||||
padding: 12px 16px;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
align-items: stretch;
|
||||
|
||||
.toolbar-left {
|
||||
width: 100%;
|
||||
|
||||
.knowledge-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.attached-files {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar-right {
|
||||
justify-content: space-between;
|
||||
|
||||
.shortcut-hint {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
Loading…
Reference in New Issue