ソースを参照

客户管家AI数据分析模块

QuYueTing 1 ヶ月 前
コミット
8f39947033

+ 23 - 0
fhKeeper/formulahousekeeper/customerBuler-crm/src/element-plus.d.ts

@@ -0,0 +1,23 @@
+declare module 'element-plus' {
+  import { Plugin } from 'vue'
+  const ElementPlus: Plugin
+  export default ElementPlus
+}
+
+declare module '@element-plus/icons-vue' {
+  import { DefineComponent } from 'vue'
+  
+  export const ChatLineRound: DefineComponent
+  export const User: DefineComponent  
+  export const Download: DefineComponent
+  export const CopyDocument: DefineComponent
+  export const Delete: DefineComponent
+  export const CirclePlus: DefineComponent
+  export const Edit: DefineComponent
+  export const CirclePlusFilled: DefineComponent
+  export const Search: DefineComponent
+  export const QuestionFilled: DefineComponent
+  
+  const component: DefineComponent
+  export default component
+}

+ 33 - 1
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/analysis/api.ts

@@ -1,4 +1,4 @@
-import { post, get } from '@/utils/request';
+import { post, get, uploadFile as requestUploadFile } from '@/utils/request';
 
 export type RequestProps = {
   /**
@@ -92,3 +92,35 @@ export async function getBulletinData(
 ): Promise<BulletinData> {
   return await post('/order/salesKit', payload);
 }
+
+export type AIQuestionParams = {
+  questionDataSource: number;
+  sourceContent: string;
+  content: string;
+  startDate: string;
+  endDate: string;
+  url?: string;
+};
+
+export type AIQuestionResponse = {
+  code: string;
+  data: string;
+};
+
+export async function askAIQuestion(
+  payload: AIQuestionParams
+): Promise<AIQuestionResponse> {
+  return await post('/aiQuestion/ask', payload);
+}
+
+export type UploadFileResponse = {
+  code: string;
+  data: string;
+};
+
+export async function uploadFileApi(file: File): Promise<UploadFileResponse> {
+  const formData = new FormData();
+  formData.append('multipartFile', file);
+  
+  return await requestUploadFile('/common/uploadFile', formData);
+}

+ 449 - 0
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/analysis/components/AIChat.vue

@@ -0,0 +1,449 @@
+<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>
+    
+    <!-- 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;">
+      <div class="p-3 flex flex-col gap-3">
+        <div v-for="(message, index) in messages" :key="index" class="flex" :class="{'justify-end': message.role === 'user', 'justify-start': message.role === 'assistant'}">
+          <div v-if="message.role === 'assistant'" class="flex items-start gap-2">
+            <el-avatar :size="24" class="bg-gray-200 flex items-center justify-center">
+              <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="markdown-body" v-html="renderMarkdown(message.content)"></div>
+              <div v-if="message.loading" class="loading-dots">
+                <span></span>
+                <span></span>
+                <span></span>
+              </div>
+              <el-button 
+                v-if="!message.loading && message.role === 'assistant' && index > 0"
+                @click="exportToWord(message.content)"
+                size="small" 
+                type="text" 
+                class="absolute -bottom-3 -right-3"
+                :icon="Download"
+              />
+            </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>
+            <el-avatar :size="24" class="bg-gray-200 flex items-center justify-center">
+              <el-icon><User /></el-icon>
+            </el-avatar>
+          </div>
+        </div>
+      </div>
+    </div>
+    
+    <!-- Data source selection -->
+    <div class="mb-3 flex gap-2">
+      <div>
+        <span class="text-sm mr-2">数据来源</span>
+        <el-select v-model="dataSource" size="small" style="width: 120px">
+          <el-option label="系统表" value="system" />
+          <el-option label="自定义报表" value="custom" />
+          <el-option label="本地上传" value="upload" />
+          <el-option label="自由交流" value="free" />
+        </el-select>
+      </div>
+      
+      <div v-if="dataSource === 'system'">
+        <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="contract" />
+          <el-option label="销售订单" value="order" />
+          <el-option label="产品" value="product" />
+        </el-select>
+      </div>
+      
+      <div v-if="dataSource === 'upload'">
+        <el-upload
+          :show-file-list="false"
+          :before-upload="beforeUpload"
+          :http-request="handleUpload"
+          accept=".xlsx,.xls"
+        >
+          <el-button size="small" type="primary">上传Excel</el-button> <span style="margin-left:5px;color:orange">{{ uploadedFilePath?'上传成功':'' }}</span>
+        </el-upload>
+      </div>
+      
+      <div class="ml-auto">
+        <span class="text-sm mr-2">时间段</span>
+        <el-date-picker
+          v-model="dateRange"
+          type="daterange"
+          size="small"
+          range-separator="/"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          format="YYYY-MM-DD"
+        />
+      </div>
+    </div>
+    
+    <!-- Input area -->
+    <div class="flex gap-2 items-end mt-auto">
+      <el-input
+        v-model="inputMessage"
+        type="textarea"
+        :rows="2"
+        placeholder="请进行数据分析,给一个总结报告,不超过300字"
+        class="flex-1"
+        resize="none"
+      />
+      <el-button
+        type="primary"
+        @click="sendMessage"
+        :disabled="loading || !inputMessage.trim()"
+        size="small"
+        style="height: 53px; "
+      >
+        发送
+      </el-button>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, reactive, nextTick, computed } from 'vue';
+import { marked } from 'marked';
+import { ChatLineRound, User, Download } from '@element-plus/icons-vue';
+import { Document, Paragraph, TextRun, Packer } from 'docx';
+import { askAIQuestion, uploadFileApi, type AIQuestionParams, type UploadFileResponse } from '../api';
+import { ElMessage } from 'element-plus/es'
+const renderMarkdown = (content: string): string => {
+  // Configure marked with options
+  marked.setOptions({
+    breaks: true,
+    gfm: true
+  });
+
+  try {
+    return marked.parse(content) as string;
+  } catch (error) {
+    console.error('Markdown parsing error:', error);
+    return content.replace(/</g, '<').replace(/>/g, '>'); // Escape HTML if parsing fails
+  }
+};
+
+const exportToWord = async (content: string) => {
+  // 先将markdown转换为HTML
+  let htmlContent = '';
+  try {
+    const result = await marked.parse(content, {
+      breaks: true,
+      gfm: true
+    });
+    htmlContent = result;
+  } catch (error) {
+    console.error('Markdown parsing error:', error);
+    htmlContent = content; // Fallback to raw content
+  }
+  
+  // 创建一个临时的div来解析HTML
+  const tempDiv = document.createElement('div');
+  tempDiv.innerHTML = htmlContent;
+  
+  // 准备Word文档的段落
+  const docChildren: Paragraph[] = [];
+  
+  // 处理每个HTML元素并转换为Word文档元素
+  Array.from(tempDiv.childNodes).forEach((node) => {
+    if (node.nodeType === Node.TEXT_NODE) {
+      // 处理纯文本节点
+      if (node.textContent && node.textContent.trim()) {
+        docChildren.push(
+          new Paragraph({
+            children: [new TextRun({ text: node.textContent.trim() })],
+          })
+        );
+      }
+    } else if (node.nodeType === Node.ELEMENT_NODE) {
+      const element = node as HTMLElement;
+      
+      // 根据HTML标签类型创建不同的Word元素
+      if (element.tagName === 'H1' || element.tagName === 'H2' || element.tagName === 'H3') {
+        docChildren.push(
+          new Paragraph({
+            heading: element.tagName === 'H1' ? 'Heading1' : element.tagName === 'H2' ? 'Heading2' : 'Heading3',
+            children: [
+              new TextRun({
+                text: element.textContent || '',
+                bold: true,
+                size: element.tagName === 'H1' ? 36 : element.tagName === 'H2' ? 32 : 28,
+              }),
+            ],
+          })
+        );
+      } else if (element.tagName === 'P') {
+        docChildren.push(
+          new Paragraph({
+            children: [new TextRun({ text: element.textContent || '' })],
+            spacing: { after: 200 },
+          })
+        );
+      } else if (element.tagName === 'UL' || element.tagName === 'OL') {
+        // 处理列表
+        Array.from(element.children).forEach((li, index) => {
+          docChildren.push(
+            new Paragraph({
+              children: [
+                new TextRun({ text: element.tagName === 'UL' ? '• ' : `${index + 1}. ` }),
+                new TextRun({ text: li.textContent || '' }),
+              ],
+              indent: { left: 720 }, // 缩进
+              spacing: { after: 120 },
+            })
+          );
+        });
+      } else if (element.tagName === 'BLOCKQUOTE') {
+        docChildren.push(
+          new Paragraph({
+            children: [new TextRun({ text: element.textContent || '', italics: true })],
+            indent: { left: 720 },
+            border: { left: { color: '#CCCCCC', size: 6, space: 15, style: 'single' } },
+            spacing: { after: 200 },
+          })
+        );
+      } else if (element.tagName === 'PRE') {
+        // 代码块
+        docChildren.push(
+          new Paragraph({
+            children: [new TextRun({ 
+              text: element.textContent || '', 
+              font: { name: 'Courier New' } 
+            })],
+            shading: { 
+              fill: '#F6F8FA',
+              type: 'clear'
+            },
+            spacing: { after: 200 },
+          })
+        );
+      } else {
+        // 其他元素默认处理
+        docChildren.push(
+          new Paragraph({
+            children: [new TextRun({ text: element.textContent || '' })],
+          })
+        );
+      }
+    }
+  });
+  
+  // 创建Word文档
+  const doc = new Document({
+    sections: [{
+      properties: {},
+      children: docChildren,
+    }],
+  });
+
+  const blob = await Packer.toBlob(doc);
+  const url = URL.createObjectURL(blob);
+  const a = document.createElement('a');
+  a.href = url;
+  a.download = 'AI分析报告.docx';
+  a.click();
+  URL.revokeObjectURL(url);
+};
+
+type DataSourceType = 'system' | 'custom' | 'upload' | 'free';
+type SystemTableType = 'clue' | 'business_opportunity' | 'customer' | 'contact' | 'contract' | 'order' | 'product';
+
+interface ChatMessage {
+  role: 'user' | 'assistant';
+  content: string;
+  loading?: boolean;
+}
+
+// Data source selection
+const dataSource = ref<DataSourceType>('system');
+const systemTable = ref<SystemTableType>('clue');
+const getFirstDayOfMonth = () => {
+  const date = new Date();
+  return new Date(date.getFullYear(), date.getMonth(), 1);
+};
+
+const uploadFile = ref<File | null>(null);
+const uploadedFilePath = ref<string>('');
+
+const beforeUpload = (file: File) => {
+  uploadFile.value = file;
+  uploadedFilePath.value = '';
+  return true;
+};
+
+const handleUpload = async (options: any) => {
+  try {
+    const result = await uploadFileApi(options.file);
+    if (result.code === 'ok') {
+      uploadedFilePath.value = result.data;
+      // Show upload success message
+      ElMessage.success("上传成功,请输入问题并发送")
+    } else {
+      console.error('Upload failed:', result);
+    }
+  } catch (error) {
+    console.error('Upload error:', error);
+  }
+};
+
+const dateRange = ref([getFirstDayOfMonth(), new Date()]);
+
+// Chat functionality
+const inputMessage = ref('请进行数据分析,给一个总结报告,不超过300字');
+const loading = ref(false);
+const messages = reactive<ChatMessage[]>([
+  { role: 'assistant', content: '你好,需要分析查询哪些数据,请交给我' },
+]);
+
+const sendMessage = async () => {
+  if (!inputMessage.value.trim() || loading.value) return;
+
+  loading.value = true;
+  const userMessage: ChatMessage = { role: 'user', content: inputMessage.value };
+  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 = {
+      questionDataSource: dataSourceMap[dataSource.value],
+      sourceContent: dataSource.value === 'system' ? systemTable.value : '',
+      content: inputMessage.value,
+      startDate: dateRange.value[0]?.toISOString().split('T')[0],
+      endDate: dateRange.value[1]?.toISOString().split('T')[0],
+      url: dataSource.value === 'upload' ? uploadedFilePath.value : ''
+    };
+
+    const result = await askAIQuestion(params);
+    messages[thinkingIndex] = {
+      role: 'assistant',
+      content: result.data || '根据您的请求,我已分析了相关数据。分析结果显示...',
+      loading: false
+    };
+  } catch (error) {
+    console.error('API error:', error);
+    messages[thinkingIndex] = {
+      role: 'assistant',
+      content: '抱歉,请求处理失败,请稍后再试',
+      loading: false
+    };
+  }
+
+  inputMessage.value = '';
+  loading.value = false;
+  // 触发滚动到底部
+  nextTick(() => {
+    const container = document.querySelector('.overflow-y-auto');
+    if (container) {
+      container.scrollTop = container.scrollHeight;
+    }
+  });
+};
+</script>
+
+<style scoped>
+:deep(.el-textarea__inner) {
+  resize: none;
+}
+
+.markdown-body {
+  line-height: 1.5;
+  word-wrap: break-word;
+}
+
+.markdown-body :deep(h1),
+.markdown-body :deep(h2),
+.markdown-body :deep(h3),
+.markdown-body :deep(h4),
+.markdown-body :deep(h5),
+.markdown-body :deep(h6) {
+  margin-top: 0.5em;
+  margin-bottom: 0.5em;
+  font-weight: bold;
+}
+
+.markdown-body :deep(p) {
+  margin-bottom: 1em;
+}
+
+.markdown-body :deep(ul),
+.markdown-body :deep(ol) {
+  padding-left: 2em;
+  margin-bottom: 1em;
+}
+
+.markdown-body :deep(li) {
+  margin-bottom: 0.5em;
+}
+
+.markdown-body :deep(code) {
+  background-color: rgba(175, 184, 193, 0.2);
+  border-radius: 3px;
+  padding: 0.2em 0.4em;
+  font-family: monospace;
+}
+
+.markdown-body :deep(pre) {
+  background-color: #f6f8fa;
+  border-radius: 3px;
+  padding: 1em;
+  overflow: auto;
+  margin-bottom: 1em;
+}
+
+.markdown-body :deep(blockquote) {
+  border-left: 4px solid #dfe2e5;
+  color: #6a737d;
+  padding-left: 1em;
+  margin-left: 0;
+  margin-bottom: 1em;
+}
+
+.loading-dots {
+  display: inline-flex;
+  align-items: center;
+  gap: 2px;
+}
+
+.loading-dots span {
+  display: inline-block;
+  width: 6px;
+  height: 6px;
+  border-radius: 50%;
+  background-color: currentColor;
+  animation: bounce 1.4s infinite ease-in-out both;
+}
+
+.loading-dots span:nth-child(1) {
+  animation-delay: -0.32s;
+}
+
+.loading-dots span:nth-child(2) {
+  animation-delay: -0.16s;
+}
+
+@keyframes bounce {
+  0%, 80%, 100% { 
+    transform: scale(0);
+  } 40% { 
+    transform: scale(1.0);
+  }
+}
+</style>

+ 265 - 257
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/analysis/index.vue

@@ -3,9 +3,11 @@ import { ref, reactive, onMounted, watchEffect } from 'vue';
 import TrendCard from './components/TrendCard.vue';
 import SimpleCard from './components/SimpleCard.vue';
 import Divider from './components/Divider.vue';
+import AIChat from './components/AIChat.vue';
 import Echarts from '@/components/ReEcharts/index.vue';
 import { EChartsOption, use } from 'echarts';
 import { dayjs } from 'element-plus';
+import { QuestionFilled } from '@element-plus/icons-vue';
 import { useStore } from "../../store/index"
 const { userInfo } = useStore()
 import {
@@ -67,20 +69,25 @@ const select1 = ref<HTMLDivElement>();
 const select2 = ref<HTMLDivElement>();
 const select3 = ref<HTMLDivElement>();
 
+const getFirstDayOfMonth = () => {
+  const now = new Date();
+  return new Date(now.getFullYear(), now.getMonth(), 1);
+};
+
 const bulletinPrompt = reactive<PromptType>({
   permission: 0,
   date: 0,
-  sliceDate: [new Date(), new Date()]
+  sliceDate: [getFirstDayOfMonth(), new Date()]
 });
 const summaryPrompt = reactive<PromptType>({
   permission: 0,
   date: 0,
-  sliceDate: [new Date(), new Date()]
+  sliceDate: [getFirstDayOfMonth(), new Date()]
 });
 const stagePrompt = reactive<PromptType>({
   permission: 0,
   date: 0,
-  sliceDate: [new Date(), new Date()]
+  sliceDate: [getFirstDayOfMonth(), new Date()]
 });
 
 const requestData = reactive<{
@@ -173,284 +180,285 @@ watchEffect(() => {
 
 <template>
   <div class="m-5 bg-white min-h-full p-4 rounded">
-    <section>
-      <!-- <div class="h-[800px] w-full bg-[red] mb-5">
-        <iframe
-          src="http://47.101.180.183:9080/webapp/agent"
-          style="border: none;"
-          class="w-full h-full"
-        />
-      </div> -->
-      <div class="flex gap-3 mb-4">
-        <div class="w-40">
-          <el-select
-            size="small"
-            :model-value="bulletinPrompt.permission"
-            @change="(value: any) => (bulletinPrompt.permission = value)"
-          >
-            <el-option
-              v-for="permission in permissionOptions"
-              :label="permission.label"
-              :value="permission.value"
-            />
-          </el-select>
-        </div>
-        <div class="w-40">
-          <el-select
-            ref="select1"
-            size="small"
-            :model-value="bulletinPrompt.date"
-            @change="(value: any) => (bulletinPrompt.date = value)"
-          >
-            <el-option v-for="date in dateOptions" :label="date.label" :value="date.value" />
-            <el-option label="自定义" value="ignore" disabled>
-              <div class="flex gap-2 w-80">
-                <el-date-picker
-                  size="small"
-                  :clearable="false"
-                  type="daterange"
-                  class="w-12"
-                  v-model="bulletinPrompt.sliceDate"
-                  start-placeholder="开始日期"
-                  end-placeholder="结束日期"
-                />
-                <el-button
-                  size="small"
-                  type="primary"
-                  @click="
-                    () => {
-                      select1?.blur();
-                      bulletinPrompt.date = 'ignore';
-                    }
-                  "
-                  >确认</el-button
-                >
-              </div>
-            </el-option>
-          </el-select>
-        </div>
-      </div>
-      <div class="border-gray-200 border rounded p-3">
-        <div class="flex gap-1.5 items-center mb-3">
-          <img width="16" src="../../assets/trend.png" />
-          <div class="text-sm font-medium">销售简报</div>
-          <el-icon><QuestionFilled class="text-gray-500 text-sm" /></el-icon>
-        </div>
-        <div class="grid xl:grid-cols-4 lg:grid-cols-3 grid-cols-2 gap-4">
-          <TrendCard
-            title="新增客户"
-            unit="人"
-            :selectVal="bulletinPrompt"
-            :number="requestData?.bulletin?.custom.customCount"
-            :compare="requestData?.bulletin?.custom.customPromote"
-          />
-          <TrendCard
-            title="新增联系人"
-            unit="人"
-            :selectVal="bulletinPrompt"
-            :number="requestData?.bulletin?.contacts.contactsCount"
-            :compare="requestData?.bulletin?.contacts.contactsPromote"
-          />
-          <TrendCard
-            :title="`新增${businessLabel}`"
-            unit="个"
-            :selectVal="bulletinPrompt"
-            :number="requestData?.bulletin?.businessOpportunity.businessOpportunityCount"
-            :compare="requestData?.bulletin?.businessOpportunity.businessOpportunityPromote"
-          />
-          <TrendCard
-            title="新增销售订单"
-            unit="个"
-            :selectVal="bulletinPrompt"
-            :number="requestData?.bulletin?.salesOrder.salesOrderCount"
-            :compare="requestData?.bulletin?.salesOrder.salesOrderPromote"
-          />
-          <TrendCard
-            title="销售订单金额"
-            unit="元"
-            :selectVal="bulletinPrompt"
-            :number="requestData?.bulletin?.salesOrdersPrice.salesOrdersPrice"
-            :compare="requestData?.bulletin?.salesOrdersPrice.salesOrderPricePromote"
-          />
-          <TrendCard
-            :title="`${businessLabel}金额`"
-            unit="元"
-            :selectVal="bulletinPrompt"
-            :number="requestData?.bulletin?.businessOpportunityPrice.businessOpportunityPrice"
-            :compare="requestData?.bulletin?.businessOpportunityPrice.businessOpportunityPromote"
-          />
-          <TrendCard
-            title="新增线索"
-            unit="个"
-            :selectVal="bulletinPrompt"
-            :number="requestData?.bulletin?.clue.clueCount"
-            :compare="requestData?.bulletin?.clue.cluePromote"
-          />
+    <section class="flex gap-4">
+      <!-- Left side - Original functionality -->
+      <div class="w-1/2 min-w-[650px] flex-shrink-0">
+        <div class="flex gap-3 mb-4">
+          <div class="w-40">
+            <el-select
+              size="small"
+              :model-value="bulletinPrompt.permission"
+              @change="(value: any) => (bulletinPrompt.permission = value)"
+            >
+              <el-option
+                v-for="permission in permissionOptions"
+                :label="permission.label"
+                :value="permission.value"
+              />
+            </el-select>
+          </div>
+          <div class="w-40">
+            <el-select
+              ref="select1"
+              size="small"
+              :model-value="bulletinPrompt.date"
+              @change="(value: any) => (bulletinPrompt.date = value)"
+            >
+              <el-option v-for="date in dateOptions" :label="date.label" :value="date.value" />
+              <el-option label="自定义" value="ignore" disabled>
+                <div class="flex gap-2 w-80">
+                      <el-date-picker
+                        size="small"
+                        :clearable="false"
+                        type="daterange"
+                        class="w-40"
+                        v-model="bulletinPrompt.sliceDate"
+                        start-placeholder="开始日期"
+                        end-placeholder="结束日期"
+                      />
+                  <el-button
+                    size="small"
+                    type="primary"
+                    @click="
+                      () => {
+                        select1?.blur();
+                        bulletinPrompt.date = 'ignore';
+                      }
+                    "
+                    >确认</el-button
+                  >
+                </div>
+              </el-option>
+            </el-select>
+          </div>
         </div>
-      </div>
-      <div class="my-8 flex gap-4 items-start">
-        <div class="border-gray-200 border rounded p-3 flex-1">
-          <div class="text-sm font-medium">数据汇总</div>
-          <div class="flex gap-3 mb-8 mt-2">
-            <div class="w-40">
-              <el-select
-                clearable
-                size="small"
-                :model-value="summaryPrompt.permission"
-                @change="(value: any) => (summaryPrompt.permission = value)"
-              >
-                <el-option
-                  v-for="permission in permissionOptions"
-                  :label="permission.label"
-                  :value="permission.value"
-                />
-              </el-select>
-            </div>
-            <div class="w-40">
-              <el-select
-                ref="select2"
-                clearable
-                size="small"
-                :model-value="summaryPrompt.date"
-                @change="(value: any) => (summaryPrompt.date = value)"
-              >
-                <el-option v-for="date in dateOptions" :label="date.label" :value="date.value" />
-                <el-option label="自定义" value="ignore" disabled>
-                  <div class="flex gap-2 w-80">
-                    <el-date-picker
-                      size="small"
-                      :clearable="false"
-                      type="daterange"
-                      class="w-12"
-                      v-model="summaryPrompt.sliceDate"
-                      start-placeholder="开始日期"
-                      end-placeholder="结束日期"
-                    />
-                    <el-button
-                      size="small"
-                      type="primary"
-                      @click="
-                        () => {
-                          select2?.blur();
-                          summaryPrompt.date = 'ignore';
-                        }
-                      "
-                      >确认</el-button
-                    >
-                  </div>
-                </el-option>
-              </el-select>
-            </div>
+        <div class="border-gray-200 border rounded p-3">
+          <div class="flex gap-1.5 items-center mb-3">
+            <img width="16" src="../../assets/trend.png" />
+            <div class="text-sm font-medium">销售简报</div>
+            <el-icon><QuestionFilled class="text-gray-500 text-sm" /></el-icon>
           </div>
-          <Divider title="客户汇总" />
-          <div class="my-6 grid grid-cols-4 gap-2">
-            <SimpleCard
+          <div class="grid xl:grid-cols-4 lg:grid-cols-3 grid-cols-2 gap-4">
+            <TrendCard
               title="新增客户"
-              unit="个"
-              :number="requestData.summary?.customDataSummary.newNum"
+              unit="人"
+              :selectVal="bulletinPrompt"
+              :number="requestData?.bulletin?.custom.customCount"
+              :compare="requestData?.bulletin?.custom.customPromote"
             />
-            <SimpleCard
-              title="转成交客户"
-              unit="个"
-              :number="requestData.summary?.customDataSummary.closeDealNum"
-              v-if="false"
+            <TrendCard
+              title="新增联系人"
+              unit="人"
+              :selectVal="bulletinPrompt"
+              :number="requestData?.bulletin?.contacts.contactsCount"
+              :compare="requestData?.bulletin?.contacts.contactsPromote"
             />
-          </div>
-          <Divider :title="`${businessLabel}汇总`" />
-          <div class="my-6 grid grid-cols-4 gap-2">
-            <SimpleCard
+            <TrendCard
               :title="`新增${businessLabel}`"
               unit="个"
-              :number="requestData.summary?.businessOpportunityDataSummary.newNum"
+              :selectVal="bulletinPrompt"
+              :number="requestData?.bulletin?.businessOpportunity.businessOpportunityCount"
+              :compare="requestData?.bulletin?.businessOpportunity.businessOpportunityPromote"
             />
-            <SimpleCard
-              :title="`赢单${businessLabel}`"
+            <TrendCard
+              title="新增销售订单"
               unit="个"
-              :number="requestData.summary?.businessOpportunityDataSummary.winning"
+              :selectVal="bulletinPrompt"
+              :number="requestData?.bulletin?.salesOrder.salesOrderCount"
+              :compare="requestData?.bulletin?.salesOrder.salesOrderPromote"
             />
-            <SimpleCard
-              :title="`输单${businessLabel}`"
-              unit="个"
-              :number="requestData.summary?.businessOpportunityDataSummary.losting"
+            <TrendCard
+              title="销售订单金额"
+              unit="元"
+              :selectVal="bulletinPrompt"
+              :number="requestData?.bulletin?.salesOrdersPrice.salesOrdersPrice"
+              :compare="requestData?.bulletin?.salesOrdersPrice.salesOrderPricePromote"
             />
-            <SimpleCard
-              :title="`${businessLabel}金额`"
+            <TrendCard
+              :title="`${businessLabel}金额`"
               unit="元"
-              :number="requestData.summary?.businessOpportunityDataSummary.allAmountOfMoney"
+              :selectVal="bulletinPrompt"
+              :number="requestData?.bulletin?.businessOpportunityPrice.businessOpportunityPrice"
+              :compare="requestData?.bulletin?.businessOpportunityPrice.businessOpportunityPromote"
             />
-          </div>
-          <Divider title="线索汇总" />
-          <div class="my-6 grid grid-cols-4 gap-2">
-            <SimpleCard
+            <TrendCard
               title="新增线索"
               unit="个"
-              :number="requestData.summary?.clueDataSummary.newNum"
-            />
-            <SimpleCard
-              :title="`线索转${businessLabel}`"
-              unit="个"
-              :number="requestData.summary?.clueDataSummary.changeNum"
+              :selectVal="bulletinPrompt"
+              :number="requestData?.bulletin?.clue.clueCount"
+              :compare="requestData?.bulletin?.clue.cluePromote"
             />
           </div>
         </div>
-        <div class="border-gray-200 border rounded p-3 flex-1">
-          <div class="text-sm font-medium">{{ `${businessLabel}阶段` }}</div>
-          <div class="flex gap-3 mb-8 mt-2">
-            <div class="w-40">
-              <el-select
-                clearable
-                size="small"
-                :model-value="stagePrompt.permission"
-                @change="(value: any) => (stagePrompt.permission = value)"
-              >
-                <el-option
-                  v-for="permission in permissionOptions"
-                  :label="permission.label"
-                  :value="permission.value"
-                />
-              </el-select>
+        <div class="my-8 flex gap-4 items-start">
+          <div class="border-gray-200 border rounded p-3 flex-1">
+            <div class="text-sm font-medium">数据汇总</div>
+            <div class="flex gap-3 mb-8 mt-2">
+              <div class="w-40">
+                <el-select
+                  clearable
+                  size="small"
+                  :model-value="summaryPrompt.permission"
+                  @change="(value: any) => (summaryPrompt.permission = value)"
+                >
+                  <el-option
+                    v-for="permission in permissionOptions"
+                    :label="permission.label"
+                    :value="permission.value"
+                  />
+                </el-select>
+              </div>
+              <div class="w-40">
+                <el-select
+                  ref="select2"
+                  clearable
+                  size="small"
+                  :model-value="summaryPrompt.date"
+                  @change="(value: any) => (summaryPrompt.date = value)"
+                >
+                  <el-option v-for="date in dateOptions" :label="date.label" :value="date.value" />
+                  <el-option label="自定义" value="ignore" disabled>
+                    <div class="flex gap-2 w-80">
+                      <el-date-picker
+                        size="small"
+                        :clearable="false"
+                        type="daterange"
+                        class="w-40"
+                        v-model="summaryPrompt.sliceDate"
+                        start-placeholder="开始日期"
+                        end-placeholder="结束日期"
+                      />
+                      <el-button
+                        size="small"
+                        type="primary"
+                        @click="
+                          () => {
+                            select2?.blur();
+                            summaryPrompt.date = 'ignore';
+                          }
+                        "
+                        >确认</el-button
+                      >
+                    </div>
+                  </el-option>
+                </el-select>
+              </div>
+            </div>
+            <Divider title="客户汇总" />
+            <div class="my-6 grid grid-cols-4 gap-2">
+              <SimpleCard
+                title="新增客户"
+                unit="个"
+                :number="requestData.summary?.customDataSummary.newNum"
+              />
+              <SimpleCard
+                title="转成交客户"
+                unit="个"
+                :number="requestData.summary?.customDataSummary.closeDealNum"
+                v-if="false"
+              />
             </div>
-            <div class="w-40">
-              <el-select
-                ref="select3"
-                clearable
-                size="small"
-                :model-value="stagePrompt.date"
-                @change="(value: any) => (stagePrompt.date = value)"
-              >
-                <el-option v-for="date in dateOptions" :label="date.label" :value="date.value" />
-                <el-option label="自定义" value="ignore" disabled>
-                  <div class="flex gap-2 w-80">
-                    <el-date-picker
-                      size="small"
-                      :clearable="false"
-                      type="daterange"
-                      class="w-12"
-                      v-model="stagePrompt.sliceDate"
-                      start-placeholder="开始日期"
-                      end-placeholder="结束日期"
-                    />
-                    <el-button
-                      size="small"
-                      type="primary"
-                      @click="
-                        () => {
-                          select3?.blur();
-                          stagePrompt.date = 'ignore';
-                        }
-                      "
-                      >确认</el-button
-                    >
-                  </div>
-                </el-option>
-              </el-select>
+            <Divider :title="`${businessLabel}汇总`" />
+            <div class="my-6 grid grid-cols-4 gap-2">
+              <SimpleCard
+                :title="`新增${businessLabel}`"
+                unit="个"
+                :number="requestData.summary?.businessOpportunityDataSummary.newNum"
+              />
+              <SimpleCard
+                :title="`赢单${businessLabel}`"
+                unit="个"
+                :number="requestData.summary?.businessOpportunityDataSummary.winning"
+              />
+              <SimpleCard
+                :title="`输单${businessLabel}`"
+                unit="个"
+                :number="requestData.summary?.businessOpportunityDataSummary.losting"
+              />
+              <SimpleCard
+                :title="`${businessLabel}总金额`"
+                unit="元"
+                :number="requestData.summary?.businessOpportunityDataSummary.allAmountOfMoney"
+              />
+            </div>
+            <Divider title="线索汇总" />
+            <div class="my-6 grid grid-cols-4 gap-2">
+              <SimpleCard
+                title="新增线索"
+                unit="个"
+                :number="requestData.summary?.clueDataSummary.newNum"
+              />
+              <SimpleCard
+                :title="`线索转${businessLabel}`"
+                unit="个"
+                :number="requestData.summary?.clueDataSummary.changeNum"
+              />
             </div>
           </div>
-          <div class="h-60">
-            <Echarts :option="chartOptions"></Echarts>
+          <div class="border-gray-200 border rounded p-3 flex-1">
+            <div class="text-sm font-medium">{{ `${businessLabel}阶段` }}</div>
+            <div class="flex gap-3 mb-8 mt-2">
+              <div class="w-40">
+                <el-select
+                  clearable
+                  size="small"
+                  :model-value="stagePrompt.permission"
+                  @change="(value: any) => (stagePrompt.permission = value)"
+                >
+                  <el-option
+                    v-for="permission in permissionOptions"
+                    :label="permission.label"
+                    :value="permission.value"
+                  />
+                </el-select>
+              </div>
+              <div class="w-40">
+                <el-select
+                  ref="select3"
+                  clearable
+                  size="small"
+                  :model-value="stagePrompt.date"
+                  @change="(value: any) => (stagePrompt.date = value)"
+                >
+                  <el-option v-for="date in dateOptions" :label="date.label" :value="date.value" />
+                  <el-option label="自定义" value="ignore" disabled>
+                    <div class="flex gap-2 w-80">
+                      <el-date-picker
+                        size="small"
+                        :clearable="false"
+                        type="daterange"
+                        class="w-40"
+                        v-model="stagePrompt.sliceDate"
+                        start-placeholder="开始日期"
+                        end-placeholder="结束日期"
+                      />
+                      <el-button
+                        size="small"
+                        type="primary"
+                        @click="
+                          () => {
+                            select3?.blur();
+                            stagePrompt.date = 'ignore';
+                          }
+                        "
+                        >确认</el-button
+                      >
+                    </div>
+                  </el-option>
+                </el-select>
+              </div>
+            </div>
+            <div class="h-60">
+              <Echarts :option="chartOptions"></Echarts>
+            </div>
           </div>
         </div>
       </div>
+      
+      <!-- Right side - AI Chat -->
+      <div class="w-1/2">
+        <AIChat />
+      </div>
     </section>
   </div>
 </template>

+ 1 - 1
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/team/index.vue

@@ -39,6 +39,7 @@
           </el-select>
         </div>
         <el-button type="primary" v-if="userInfo.userNameNeedTranslate == 1" @click="transitionOperation('exportUser', '')" v-permission="['teamExport']">导出人员</el-button>
+        <el-button type="primary" v-if="userInfo.userNameNeedTranslate == 1" @click="dialogFrom.newSyncWithCorpWxDayloadVisable = true">同步企微通讯录</el-button>
         <el-dropdown v-if="userInfo.userNameNeedTranslate != 1">
           <el-button type="primary">
             更多操作<el-icon class="el-icon--right"><arrow-down /></el-icon>
@@ -48,7 +49,6 @@
               <el-dropdown-item @click="addPersone(false)" v-permission="['teamAdd']">添加人员</el-dropdown-item>
               <el-dropdown-item @click="transitionOperation('exportUser', '')" v-permission="['teamExport']">导出人员</el-dropdown-item>
               <el-dropdown-item @click="transitionOperation('importUser', '')" v-permission="['teamImport']">批量导入</el-dropdown-item>
-              <el-dropdown-item v-if="userInfo.userNameNeedTranslate == 1" @click="dialogFrom.newSyncWithCorpWxDayloadVisable = true">同步企微通讯录</el-dropdown-item>
             </el-dropdown-menu>
           </template>
         </el-dropdown>

+ 21 - 0
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/test/aitest.vue

@@ -0,0 +1,21 @@
+<template>
+  <div class="m-5 bg-white min-h-full p-4 rounded">
+    <h1 class="text-xl mb-4">AI Chat Test Page</h1>
+    <div class="flex gap-4">
+      <div class="w-1/2">
+        <div class="border-gray-200 border rounded p-3">
+          <div class="text-sm font-medium mb-3">Original Content</div>
+          <p>This is a test page to verify the AI Chat component.</p>
+        </div>
+      </div>
+      
+      <div class="w-1/2">
+        <AIChat />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import AIChat from '../analysis/components/AIChat.vue';
+</script>

+ 5 - 0
fhKeeper/formulahousekeeper/customerBuler-crm/src/router/index.ts

@@ -43,6 +43,11 @@ export const routes: RouteRecordRaw[] = [
     path: "/testEcharts",
     component: () => import("../pages/test/echarts.vue"),
   },
+  {
+    name: "aitest",
+    path: "/test/aitest",
+    component: () => import("../pages/test/aitest.vue"),
+  },
 ];
 
 const router = createRouter({

+ 2 - 2
fhKeeper/formulahousekeeper/customerBuler-crm/src/router/routerGuards.ts

@@ -9,7 +9,7 @@ export function createRouterGuards(router: Router) {
     const routers = router.getRoutes();
     const { setAsyncRoutesMark, asyncRoutesMark, getToken } = useStore();
     const token = getToken;
-    const skipPath = ["/login", "/register", "/test", "/testEcharts"];
+    const skipPath = ["/login", "/register", "/test", "/testEcharts", "/test/aitest"];
     if (skipPath.includes(to.path)) {
       next();
     } else {
@@ -81,4 +81,4 @@ export function createRouterGuards(router: Router) {
   return {
     beforeEach,
   };
-}
+}

+ 1 - 1
fhKeeper/formulahousekeeper/customerBuler-crm/src/utils/request.ts

@@ -75,7 +75,7 @@ export async function post(url: string, data?: any): Promise<any> {
     instance
       .post(url, data, {
         headers: {
-          "Content-type": " application/x-www-form-urlencoded; charset=UTF-8",
+          "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
         },
       })
       .then(({ data }: any) => {

+ 6 - 1
fhKeeper/formulahousekeeper/customerBuler-crm/tsconfig.json

@@ -24,7 +24,12 @@
     "paths": {
       "@/*": ["./src/*"]
     },
-    "types": ["element-plus/global"]
+    "types": ["element-plus/global"],
+    "typeRoots": [
+      "./node_modules/@types",
+      "./src"
+    ],
+    "baseUrl": "."
   },
   "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
   "references": [{ "path": "./tsconfig.node.json" }]

+ 6 - 2
fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/service/impl/AIQuestionServiceImpl.java

@@ -32,6 +32,7 @@ import javax.annotation.Resource;
 import javax.servlet.http.HttpServletRequest;
 import javax.sql.DataSource;
 import java.io.ByteArrayOutputStream;
+import java.io.File;
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
@@ -284,14 +285,17 @@ public class AIQuestionServiceImpl extends ServiceImpl<AIQuestionMapper, AIQuest
             MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
             body.add("question", questionBO.getContent());
             ByteArrayResource byteArrayResource = null;
+            File f = new File(path,questionBO.getUrl());
             try {
-                MultipartFile multipartFileFromPath = getMultipartFileFromPath(questionBO.getUrl());
+                System.out.println("本地文件路径=="+f.getAbsolutePath());
+                MultipartFile multipartFileFromPath = getMultipartFileFromPath(f.getAbsolutePath());
                 byteArrayResource = new ByteArrayResource(multipartFileFromPath.getBytes()) {
                     @Override
                     public String getFilename() {
                         return multipartFileFromPath.getOriginalFilename();
                     }
                 };
+                System.out.println("字节长度=="+byteArrayResource.contentLength());
             }catch (IOException e){
                 e.printStackTrace();
             }
@@ -326,7 +330,7 @@ public class AIQuestionServiceImpl extends ServiceImpl<AIQuestionMapper, AIQuest
             HttpEntity<MultiValueMap<String, Object>> requestEntity =
                     new HttpEntity<>(body, headers);
             ResponseEntity<String> response = restTemplate.exchange(
-                    aiFileAskUrl,
+                    aiAskUrl,
                     HttpMethod.POST,
                     requestEntity,
                     String.class);