|
@@ -1,6 +1,9 @@
|
|
|
<template>
|
|
|
- <div class="border-gray-200 border rounded p-3 h-full flex flex-col" style="min-height: 750px; max-height: 750px;">
|
|
|
- <div class="text-sm font-medium mb-3">DeepSeek大模型CRM数据分析</div>
|
|
|
+ <div class="border-gray-200 p-3 h-full flex flex-col" style="min-height: 750px; max-height: 750px;">
|
|
|
+ <div class="text-sm font-medium mb-3 flex justify-between">
|
|
|
+ <span style="color: #075985">DeepSeek大模型CRM数据分析</span>
|
|
|
+ <span class="text-gray-500 text-right">开会做PPT利器!</span>
|
|
|
+ </div>
|
|
|
|
|
|
<!-- Chat messages container with fixed height and scrolling -->
|
|
|
<div class="mb-3 border-gray-200 border rounded overflow-y-auto flex-grow" style="min-height: 0;">
|
|
@@ -11,6 +14,9 @@
|
|
|
<el-icon><ChatLineRound /></el-icon>
|
|
|
</el-avatar>
|
|
|
<div class="border-gray-200 border rounded p-2 text-sm max-w-[80%] bg-gray-50 relative">
|
|
|
+ <div class="text-xs text-gray-500 mb-1" v-if="message.dataSource">
|
|
|
+ [{{ message.dataSource }}{{ message.dataName ? '-' + message.dataName : '' }}]
|
|
|
+ </div>
|
|
|
<div class="markdown-body" v-html="renderMarkdown(message.content)"></div>
|
|
|
<div v-if="message.loading" class="loading-dots">
|
|
|
<span></span>
|
|
@@ -18,7 +24,7 @@
|
|
|
<span></span>
|
|
|
</div>
|
|
|
<el-button
|
|
|
- v-if="!message.loading && message.role === 'assistant' && index > 0"
|
|
|
+ v-if="!message.loading && message.role === 'assistant' && index > 0 && '抱歉,请求处理失败,请稍后再试' != message.content && '无数据' != message.content"
|
|
|
@click="exportToWord(message.content)"
|
|
|
size="small"
|
|
|
type="text"
|
|
@@ -28,7 +34,11 @@
|
|
|
</div>
|
|
|
</div>
|
|
|
<div v-if="message.role === 'user'" class="flex items-start gap-2 justify-end">
|
|
|
- <div class="border-gray-200 border rounded p-2 text-sm max-w-[80%] bg-blue-50 markdown-body" v-html="renderMarkdown(message.content)">
|
|
|
+ <div class="border-gray-200 border rounded p-2 text-sm max-w-[80%] bg-blue-50">
|
|
|
+ <div class="text-xs text-gray-500 mb-1" v-if="message.dataSource">
|
|
|
+ [{{ message.dataSource }}{{ message.dataName ? '-' + message.dataName : '' }}]
|
|
|
+ </div>
|
|
|
+ <div class="markdown-body" v-html="renderMarkdown(message.content)"></div>
|
|
|
</div>
|
|
|
<el-avatar :size="24" class="bg-gray-200 flex items-center justify-center">
|
|
|
<el-icon><User /></el-icon>
|
|
@@ -54,13 +64,30 @@
|
|
|
<el-select v-model="systemTable" size="small" style="width: 120px">
|
|
|
<el-option label="线索" value="clue" />
|
|
|
<el-option label="商机" value="business_opportunity" />
|
|
|
- <el-option label="客户" value="customer" />
|
|
|
- <el-option label="联系人" value="contact" />
|
|
|
+ <el-option label="客户" value="custom" />
|
|
|
+ <el-option label="联系人" value="contacts" />
|
|
|
<el-option label="合同" value="contract" />
|
|
|
- <el-option label="销售订单" value="order" />
|
|
|
+ <el-option label="销售订单" value="sales_order" />
|
|
|
<el-option label="产品" value="product" />
|
|
|
</el-select>
|
|
|
</div>
|
|
|
+
|
|
|
+ <div v-if="dataSource === 'custom'">
|
|
|
+ <el-select
|
|
|
+ v-model="selectedReportId"
|
|
|
+ size="small"
|
|
|
+ style="width: 200px"
|
|
|
+ placeholder="选择报表"
|
|
|
+ @focus="loadCustomReports"
|
|
|
+ >
|
|
|
+ <el-option
|
|
|
+ v-for="report in customReports"
|
|
|
+ :key="report.relateFormId"
|
|
|
+ :label="report.storeName"
|
|
|
+ :value="report.relateFormId"
|
|
|
+ />
|
|
|
+ </el-select>
|
|
|
+ </div>
|
|
|
|
|
|
<div v-if="dataSource === 'upload'">
|
|
|
<el-upload
|
|
@@ -73,7 +100,7 @@
|
|
|
</el-upload>
|
|
|
</div>
|
|
|
|
|
|
- <div class="ml-auto">
|
|
|
+ <div v-if="dataSource === 'system'" class="ml-auto">
|
|
|
<span class="text-sm mr-2">时间段</span>
|
|
|
<el-date-picker
|
|
|
v-model="dateRange"
|
|
@@ -96,6 +123,7 @@
|
|
|
placeholder="请进行数据分析,给一个总结报告,不超过300字"
|
|
|
class="flex-1"
|
|
|
resize="none"
|
|
|
+ @keyup.enter="inputMessage.trim() && !loading ? sendMessage() : null"
|
|
|
/>
|
|
|
<el-button
|
|
|
type="primary"
|
|
@@ -119,11 +147,13 @@ import {
|
|
|
askAIQuestion,
|
|
|
uploadFileApi,
|
|
|
getLatestQuestionList,
|
|
|
+ getCustomReports,
|
|
|
type AIQuestionParams,
|
|
|
type UploadFileResponse,
|
|
|
type ChatContent,
|
|
|
type LatestQuestionResponse,
|
|
|
- type AIQuestionResponse
|
|
|
+ type AIQuestionResponse,
|
|
|
+ type CustomReport
|
|
|
} from '../api';
|
|
|
import { ElMessage } from 'element-plus/es'
|
|
|
import * as internal from 'stream';
|
|
@@ -265,17 +295,21 @@ const exportToWord = async (content: string) => {
|
|
|
};
|
|
|
|
|
|
type DataSourceType = 'system' | 'custom' | 'upload' | 'free';
|
|
|
-type SystemTableType = 'clue' | 'business_opportunity' | 'customer' | 'contact' | 'contract' | 'order' | 'product';
|
|
|
+type SystemTableType = 'clue' | 'business_opportunity' | 'custom' | 'contacts' | 'contract' | 'sales_order' | 'product';
|
|
|
|
|
|
interface ChatMessage {
|
|
|
role: 'user' | 'assistant';
|
|
|
content: string;
|
|
|
loading?: boolean;
|
|
|
+ dataSource?: string;
|
|
|
+ dataName?: string;
|
|
|
}
|
|
|
|
|
|
// Data source selection
|
|
|
const dataSource = ref<DataSourceType>('system');
|
|
|
const systemTable = ref<SystemTableType>('clue');
|
|
|
+const customReports = ref<CustomReport[]>([]);
|
|
|
+const selectedReportId = ref<string>('');
|
|
|
const getFirstDayOfMonth = () => {
|
|
|
const date = new Date();
|
|
|
return new Date(date.getFullYear(), date.getMonth(), 1);
|
|
@@ -313,6 +347,20 @@ const loading = ref(false);
|
|
|
const messages = reactive<ChatMessage[]>([]);
|
|
|
const questionId = ref<number | null>(null);
|
|
|
|
|
|
+const loadCustomReports = async () => {
|
|
|
+ if (customReports.value.length === 0) {
|
|
|
+ try {
|
|
|
+ const result = await getCustomReports();
|
|
|
+ if (result.code === 'ok') {
|
|
|
+ customReports.value = result.data;
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Failed to load custom reports:', error);
|
|
|
+ ElMessage.error('加载自定义报表失败');
|
|
|
+ }
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
const isSameDay = (dateString: string, compareDate: Date) => {
|
|
|
// Parse yyyy-MM-dd hh:mm:ss format
|
|
|
const [datePart] = dateString.split(' ');
|
|
@@ -348,30 +396,75 @@ onMounted(async () => {
|
|
|
console.error('Failed to load chat history:', error);
|
|
|
messages.push({ role: 'assistant', content: '你好,需要分析查询哪些数据,请交给我' });
|
|
|
}
|
|
|
+
|
|
|
+ // 触发滚动到底部
|
|
|
+ nextTick(() => {
|
|
|
+ const container = document.querySelector('.overflow-y-auto');
|
|
|
+ if (container) {
|
|
|
+ container.scrollTop = container.scrollHeight;
|
|
|
+ }
|
|
|
+ });
|
|
|
});
|
|
|
|
|
|
const sendMessage = async () => {
|
|
|
if (!inputMessage.value.trim() || loading.value) return;
|
|
|
-
|
|
|
+ type DataSourceType = 'system' | 'custom' | 'upload' | 'free';
|
|
|
+ const dataSourceNameMap: Record<DataSourceType, string> = {
|
|
|
+ 'system': '系统表',
|
|
|
+ 'custom': '自定义报表',
|
|
|
+ 'upload': '本地上传',
|
|
|
+ 'free': '自由交流'
|
|
|
+ };
|
|
|
+ const dataSourceMap: Record<DataSourceType, number> = {
|
|
|
+ 'system': 1,
|
|
|
+ 'custom': 2,
|
|
|
+ 'upload': 3,
|
|
|
+ 'free': 4
|
|
|
+ };
|
|
|
+ const sourceName = dataSourceNameMap[dataSource.value]
|
|
|
+ const dataName = dataSource.value === 'system' ?
|
|
|
+ systemTable.value === 'clue' ? '线索' :
|
|
|
+ systemTable.value === 'business_opportunity' ? '商机' :
|
|
|
+ systemTable.value === 'custom' ? '客户' :
|
|
|
+ systemTable.value === 'contacts' ? '联系人' :
|
|
|
+ systemTable.value === 'contract' ? '合同' :
|
|
|
+ systemTable.value === 'sales_order' ? '销售订单' : '产品' :
|
|
|
+ dataSource.value === 'custom' ?
|
|
|
+ customReports.value.find(r => r.relateFormId === selectedReportId.value)?.storeName || '' :
|
|
|
+ dataSource.value === 'upload' ?
|
|
|
+ uploadFile.value?.name || '' : ''
|
|
|
+ const finalContent = '['+sourceName+(dataName?('-'+dataName):'')+']'+'<br>' + inputMessage.value;
|
|
|
+ if (!dataName) {
|
|
|
+ if (dataSource.value == 'custom') {
|
|
|
+ //提示:请选择报表
|
|
|
+ ElMessage.error({
|
|
|
+ message: "请选择报表",
|
|
|
+ type: "error",
|
|
|
+ duration: 2000,
|
|
|
+ })
|
|
|
+ return
|
|
|
+ } else if (dataSource.value == 'upload') {
|
|
|
+ ElMessage.error({
|
|
|
+ message: "请上传数据文件(仅支持excel格式)",
|
|
|
+ type: "error",
|
|
|
+ duration: 2000,
|
|
|
+ })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ }
|
|
|
loading.value = true;
|
|
|
- const userMessage: ChatMessage = { role: 'user', content: inputMessage.value };
|
|
|
+ const userMessage: ChatMessage = { role: 'user', content: finalContent };
|
|
|
messages.push(userMessage);
|
|
|
const thinkingIndex = messages.length;
|
|
|
messages.push({ role: 'assistant', content: 'AI正在思考', loading: true });
|
|
|
|
|
|
try {
|
|
|
- type DataSourceType = 'system' | 'custom' | 'upload' | 'free';
|
|
|
- const dataSourceMap: Record<DataSourceType, number> = {
|
|
|
- 'system': 1,
|
|
|
- 'custom': 2,
|
|
|
- 'upload': 3,
|
|
|
- 'free': 4
|
|
|
- };
|
|
|
-
|
|
|
+
|
|
|
const params: AIQuestionParams & { questionId?: number } = {
|
|
|
questionDataSource: dataSourceMap[dataSource.value],
|
|
|
- sourceContent: dataSource.value === 'system' ? systemTable.value : '',
|
|
|
- content: inputMessage.value,
|
|
|
+ sourceContent: dataSource.value === 'system' ? systemTable.value :
|
|
|
+ dataSource.value === 'custom' ? selectedReportId.value : '',
|
|
|
+ content: finalContent,
|
|
|
startDate: dateRange.value[0]?.toISOString().split('T')[0],
|
|
|
endDate: dateRange.value[1]?.toISOString().split('T')[0],
|
|
|
url: dataSource.value === 'upload' ? uploadedFilePath.value : ''
|
|
@@ -381,7 +474,15 @@ const sendMessage = async () => {
|
|
|
if (questionId.value) {
|
|
|
params.questionId = questionId.value;
|
|
|
}
|
|
|
-
|
|
|
+ // reset message input
|
|
|
+ inputMessage.value = '';
|
|
|
+ //上方聊天记录页面自动滚动到最下面
|
|
|
+ nextTick(() => {
|
|
|
+ const container = document.querySelector('.overflow-y-auto');
|
|
|
+ if (container) {
|
|
|
+ container.scrollTop = container.scrollHeight;
|
|
|
+ }
|
|
|
+ });
|
|
|
const result = await askAIQuestion(params);
|
|
|
messages[thinkingIndex] = {
|
|
|
role: 'assistant',
|
|
@@ -398,7 +499,6 @@ const sendMessage = async () => {
|
|
|
};
|
|
|
}
|
|
|
|
|
|
- inputMessage.value = '';
|
|
|
loading.value = false;
|
|
|
// 触发滚动到底部
|
|
|
nextTick(() => {
|