主要修改: - 完善员工门户CRM模块(经销商、线索管理) - 添加ERP客户选择器集成 - 优化登录认证和租户选择 - 添加超时配置、企业微信集成等文档 - 更新docker-compose配置 - 将.pid临时文件加入gitignore Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
252 lines
6.2 KiB
Vue
252 lines
6.2 KiB
Vue
<!-- ChatWidget AI对话组件 -->
|
||
<script setup lang="ts">
|
||
import { Sender } from 'vue-element-plus-x';
|
||
import { getKnowledgeList } from '@/api/chat';
|
||
import ModelSelect from '@/components/ModelSelect/index.vue';
|
||
import { useUserStore } from '@/stores';
|
||
import { useChatStore } from '@/stores/modules/chat';
|
||
import { useFilesStore } from '@/stores/modules/files';
|
||
import { useSessionStore } from '@/stores/modules/session';
|
||
|
||
const userStore = useUserStore();
|
||
const sessionStore = useSessionStore();
|
||
const filesStore = useFilesStore();
|
||
const chatStore = useChatStore();
|
||
|
||
const senderValue = ref('');
|
||
const senderRef = ref<InstanceType<typeof Sender> | null>(null);
|
||
const isWidgetExpanded = ref(true);
|
||
|
||
// 知识库列表
|
||
const knowledgeList = ref<any[]>([]);
|
||
const selectedKnowledgeId = ref<string>('');
|
||
const selectedKnowledgeName = ref<string>('知识库');
|
||
|
||
// 推理开关
|
||
const isReasoningEnabled = ref(false);
|
||
|
||
// 当前会话ID(使用 computed 避免 v-model 绑定可选链)
|
||
const currentSessionId = computed({
|
||
get: () => sessionStore.currentSession?.id || '',
|
||
set: (value: string) => {
|
||
const session = sessionStore.sessionList.find(s => s.id === value);
|
||
if (session) {
|
||
sessionStore.setCurrentSession(session);
|
||
}
|
||
},
|
||
});
|
||
|
||
// 加载知识库列表
|
||
async function loadKnowledgeList() {
|
||
try {
|
||
const response = await getKnowledgeList();
|
||
if (response?.rows && Array.isArray(response.rows)) {
|
||
knowledgeList.value = response.rows.map((item: any) => ({
|
||
id: item.id,
|
||
name: item.name,
|
||
icon: 'Document',
|
||
}));
|
||
}
|
||
}
|
||
catch {
|
||
// 静默失败,知识库列表为空即可,不影响其他功能
|
||
}
|
||
}
|
||
|
||
// 选择知识库
|
||
function insertKnowledgeTag(knowledgeId: string) {
|
||
const knowledge = knowledgeList.value.find(k => k.id === knowledgeId);
|
||
if (knowledge) {
|
||
selectedKnowledgeId.value = knowledgeId;
|
||
selectedKnowledgeName.value = knowledge.name;
|
||
chatStore.setKnowledgeId(knowledgeId);
|
||
}
|
||
}
|
||
|
||
// 发送消息
|
||
async function handleSend() {
|
||
const messageContent = senderValue.value;
|
||
senderValue.value = '';
|
||
await sessionStore.createSessionList({
|
||
userId: userStore.userInfo?.userId as number,
|
||
sessionContent: messageContent,
|
||
sessionTitle: messageContent.slice(0, 10),
|
||
remark: messageContent.slice(0, 10),
|
||
});
|
||
}
|
||
|
||
// 切换展开状态
|
||
function toggleWidget() {
|
||
isWidgetExpanded.value = !isWidgetExpanded.value;
|
||
}
|
||
|
||
onMounted(() => {
|
||
loadKnowledgeList();
|
||
});
|
||
</script>
|
||
|
||
<template>
|
||
<div class="chat-widget-card" :class="{ 'widget-collapsed': !isWidgetExpanded }">
|
||
<div class="card-header">
|
||
<div class="header-left">
|
||
<el-icon class="header-icon" color="#1d5af3">
|
||
<ChatLineRound />
|
||
</el-icon>
|
||
<h3 class="card-title">
|
||
AI 对话助手
|
||
</h3>
|
||
</div>
|
||
<el-button
|
||
class="toggle-btn"
|
||
:icon="isWidgetExpanded ? 'ArrowDown' : 'ArrowUp'"
|
||
size="small"
|
||
circle
|
||
@click="toggleWidget"
|
||
/>
|
||
</div>
|
||
|
||
<div v-if="isWidgetExpanded" class="card-body">
|
||
<!-- 会话选择器 -->
|
||
<div class="session-selector">
|
||
<el-select
|
||
v-model="currentSessionId"
|
||
placeholder="选择会话"
|
||
size="small"
|
||
style="width: 100%"
|
||
>
|
||
<el-option
|
||
v-for="session in sessionStore.sessionList"
|
||
:key="session.id"
|
||
:label="session.sessionTitle"
|
||
:value="session.id"
|
||
/>
|
||
</el-select>
|
||
</div>
|
||
|
||
<!-- 消息输入区 -->
|
||
<div class="message-input-wrapper">
|
||
<Sender
|
||
ref="senderRef"
|
||
v-model="senderValue"
|
||
class="chat-widget-sender"
|
||
:auto-size="{ maxRows: 4, minRows: 2 }"
|
||
variant="updown"
|
||
clearable
|
||
allow-speech
|
||
@submit="handleSend"
|
||
>
|
||
<template #header>
|
||
<div class="sender-header">
|
||
<ModelSelect />
|
||
<el-dropdown trigger="click" @command="insertKnowledgeTag">
|
||
<el-button size="small" type="primary" plain>
|
||
<el-icon><FolderOpened /></el-icon>
|
||
{{ selectedKnowledgeName }}
|
||
</el-button>
|
||
<template #dropdown>
|
||
<el-dropdown-menu>
|
||
<el-dropdown-item
|
||
v-for="item in knowledgeList"
|
||
:key="item.id"
|
||
:command="item.id"
|
||
>
|
||
{{ item.name }}
|
||
</el-dropdown-item>
|
||
</el-dropdown-menu>
|
||
</template>
|
||
</el-dropdown>
|
||
</div>
|
||
</template>
|
||
</Sender>
|
||
</div>
|
||
|
||
<!-- 提示信息 -->
|
||
<div class="widget-footer">
|
||
<el-button size="small" text @click="$router.push('/chat')">
|
||
<el-icon><FullScreen /></el-icon>
|
||
打开完整对话界面
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped lang="scss">
|
||
.chat-widget-card {
|
||
background-color: #ffffff;
|
||
border-radius: var(--radius-lg);
|
||
padding: 20px;
|
||
border: 1px solid var(--color-border);
|
||
transition: all var(--transition);
|
||
|
||
.card-header {
|
||
display: flex;
|
||
gap: 12px;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 16px;
|
||
|
||
.header-left {
|
||
display: flex;
|
||
gap: 12px;
|
||
align-items: center;
|
||
|
||
.header-icon {
|
||
width: 24px;
|
||
height: 24px;
|
||
font-size: 24px;
|
||
}
|
||
|
||
.card-title {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: var(--color-text-primary);
|
||
margin: 0;
|
||
}
|
||
}
|
||
|
||
.toggle-btn {
|
||
flex-shrink: 0;
|
||
}
|
||
}
|
||
|
||
.card-body {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
|
||
.session-selector {
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.message-input-wrapper {
|
||
.chat-widget-sender {
|
||
:deep(.el-textarea__inner) {
|
||
font-size: 14px;
|
||
}
|
||
}
|
||
|
||
.sender-header {
|
||
display: flex;
|
||
gap: 8px;
|
||
align-items: center;
|
||
padding: 8px;
|
||
}
|
||
}
|
||
|
||
.widget-footer {
|
||
display: flex;
|
||
justify-content: center;
|
||
padding-top: 8px;
|
||
border-top: 1px solid var(--color-border-light);
|
||
}
|
||
}
|
||
|
||
&.widget-collapsed {
|
||
.card-body {
|
||
display: none;
|
||
}
|
||
}
|
||
}
|
||
</style>
|