Преглед изворни кода

feat:新增邮件自动发送智能体

韩洋 пре 5 месеци
родитељ
комит
43933c2c3d
31 измењених фајлова са 2999 додато и 54 уклоњено
  1. 45 1
      public/config.js
  2. 0 0
      src/assets/agent/agent_icon_5.svg
  3. 1 0
      src/assets/chat-smart/edit.svg
  4. 14 2
      src/modules/agent/AgentList.vue
  5. 20 0
      src/modules/chat-item/Answer.vue
  6. 381 0
      src/modules/chat-item/DialogEmailContent.vue
  7. 12 15
      src/modules/chat-item/DialogThinkContent.vue
  8. 3 1
      src/modules/common/use-icon-map.js
  9. 21 1
      src/modules/conversation-chat/ChatTips.vue
  10. 2 1
      src/modules/conversation-chat/DialogInfo.vue
  11. 116 11
      src/modules/conversation-chat/DialogInput.vue
  12. 14 0
      src/modules/conversation-chat/dialog-input/InputActions.vue
  13. 123 0
      src/modules/conversation-chat/dialog-input/TaskConfig.vue
  14. 116 0
      src/modules/conversation-chat/dialog-input/email-input/ReportTypeList.vue
  15. 100 0
      src/modules/conversation-chat/dialog-input/email-input/TextAreaEmail.vue
  16. 122 0
      src/modules/conversation-chat/dialog-input/email-input/TypeItem.vue
  17. 0 0
      src/modules/conversation-chat/dialog-input/email-input/moke_data.js
  18. 59 0
      src/modules/conversation-dialog/api-dialog.js
  19. 6 0
      src/modules/conversation-dialog/format-dialog-answer-from-history.js
  20. 166 0
      src/modules/conversation-dialog/format-dialog-answer-from-sse_receive_message_email.js
  21. 185 0
      src/modules/conversation-dialog/use-sse-email.js
  22. 12 14
      src/modules/smart-chat/DialogThinkContent.vue
  23. 318 0
      src/modules/task-config/AddTaskModal.vue
  24. 363 0
      src/modules/task-config/EditTaskModal.vue
  25. 197 0
      src/modules/task-config/TaskConfigModal.vue
  26. 339 0
      src/modules/task-config/TaskTable.vue
  27. 32 0
      src/modules/task-config/common_data.js
  28. 55 6
      src/views/About.vue
  29. 5 0
      src/views/Agent/AgentCentric.vue
  30. 2 1
      src/views/conversation/ChatAsideMenu.vue
  31. 170 1
      src/views/conversation/ConversationChatContainer.vue

+ 45 - 1
public/config.js

@@ -25,7 +25,10 @@ window.APP_ID_AL_WS = "c16b35ec-85a7-4bb8-a841-645569e99e53"
 // window.APP_VERSION_AL = "1750039714487"
 window.APP_VERSION_AL_WS = "1754962461212"
 
-
+// 阿里百炼智能体参数(邮件)
+window.APP_ID_AL_EMAIL = "69f9d850-bce6-4176-b710-7325c1093870"
+// window.APP_VERSION_AL_EMAIL = "1750039714487"
+window.APP_VERSION_AL_EMAIL = "1772246767838"
 
 // 手机端i国网下载链接
 // 苹果手机连接
@@ -40,6 +43,7 @@ window.AGENT = {
   bgsc: true,
   znws: true,
   bgcl: true,
+  znyj: true
 }
 
 
@@ -48,3 +52,43 @@ window.bgcl_question = "数据处理,并标注表格中的异常值"
 window.bgcl_content = '表格解析完成,请配置表格数据校验规则'
 window.bgcl_error_content = '表格合并失败,请稍候重试!'
 
+// 控制灵活模板输入框是否显示上传模板文件
+window.show_outline_upload_exe = false
+
+// 邮件自动发送-报表类型列表
+window.report_type_list = [
+  {
+    key: '1',
+    title: '频停周报',
+    desc: '频繁停电台区、停电线路等数据',
+    type: '1',
+    date: '上周',
+    email_type: 'week_report_1',
+  },
+  {
+    key: '2',
+    title: '配网运营分析月报',
+    desc: '供电可靠性、整线跳闸、投诉建议工单等数据',
+    type: '2',
+    date: '上月',
+    email_type: 'monthly_report_1',
+  },
+  {
+    key: '3',
+    title: '配网运营分析周报',
+    desc: '供电可靠性、整线跳闸、投诉建议工单等数据',
+    type: '3',
+    date: '上周',
+    email_type: 'week_report_2',
+  },
+  {
+    key: '4',
+    title: '配网数字化月报',
+    desc: '配网管控指标',
+    type: '4',
+    date: '上月',
+    email_type: 'monthly_report_2',
+  },
+
+]
+

Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
src/assets/agent/agent_icon_5.svg


+ 1 - 0
src/assets/chat-smart/edit.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="18" height="17.970590591430664" viewBox="0 0 18 17.970590591430664"><path d="M6.2426395,15.970591L18,15.970591L18,17.970591L0,17.970591L0,13.72789L9.8994999,3.8284199L14.142099,8.0710907L6.2426395,15.970591ZM11.3137,2.4142101L13.434999,0.29289007C13.8256,-0.097630024,14.4587,-0.097630024,14.849199,0.29289007L17.6777,3.12132C18.068199,3.5118401,18.068199,4.14501,17.6777,4.5355301L15.556299,6.6568499L11.3137,2.4142101Z" fill="#0A817E" fill-opacity="1" style="mix-blend-mode:passthrough"/></svg>

+ 14 - 2
src/modules/agent/AgentList.vue

@@ -19,6 +19,7 @@ import icon_1 from '../../assets/agent/agent_icon_1.svg';
 import icon_2 from '../../assets/agent/agent_icon_2.svg';
 import icon_3 from '../../assets/agent/agent_icon_3.svg';
 import icon_4 from '../../assets/agent/agent_icon_4.svg';
+import icon_5 from '../../assets/agent/agent_icon_5.svg';
 
 // import { updateAgentUsedList } from '../chat-user/useAgentUsed';
 
@@ -69,6 +70,15 @@ const agent_list = ref([
     des_2: '精准识别异常数据',
     icon: icon_4,
     is_show: props.auth.bgcl
+  },
+  {
+    idx: '05',
+    id: 'znyj',
+    title: '邮件自动发送',
+    des_1: '自动发送邮件',
+    des_2: '无需手动操作',
+    icon: icon_5,
+    is_show: props.auth.znyj
   }
 ]);
 
@@ -125,13 +135,15 @@ onMounted(() => {
   justify-content: center;
   cursor: pointer;
   background: #ffffff;
-
+  border: 1px solid transparent;
   border-radius: 20px;
-  box-shadow: 0px 6 11px 0px rgba(10, 129, 126, 0.1);
+  box-shadow: 0px 6px 11px 0px rgba(10, 129, 126, 0.1);
+  transition: all 0.3s ease;
 }
 
 .intell-item:hover {
   border: 1px solid var(--agent_hover);
+  box-shadow: 0px 8px 16px 0px rgba(10, 129, 126, 0.15);
 }
 
 .item-icon {

+ 20 - 0
src/modules/chat-item/Answer.vue

@@ -96,6 +96,25 @@
           />
         </DialogReportContent>
       </div>
+      <div class="answer-item-content" v-if="chat_type === 'znyj'">
+        <!-- 智能问数的组件渲染和报告生成的一致,先使用同一个,后续有变动再单独拆分 -->
+        <DialogEmailContent
+          :is_loading="is_content_loading"
+          :type="type"
+          :answer_status="answer_status"
+          :result_list="report_content"
+        >
+          <DialogActions
+            v-if="answer_status == 2"
+            class="answer-actions-btns"
+            :type="type"
+            :like_status="answer_evaluation"
+            @copy="handleReportCopy"
+            @like="handleLike"
+            @hate="handleHate"
+          />
+        </DialogEmailContent>
+      </div>
       <div class="answer-item-content" v-if="chat_type === 'bgcl'">
         <DialogBgclContent :content="bgcl_content" v-if="analyzeOrMerge">
           <RuleAction
@@ -139,6 +158,7 @@ import { Message } from '@arco-design/web-vue';
 import DialogContent from './DialogContent.vue';
 import DialogReportOutline from './DialogReportOutline.vue';
 import DialogReportContent from './DialogReportContent.vue';
+import DialogEmailContent from './DialogEmailContent.vue';
 import DialogBgclContent from './DialogBgclContent.vue';
 import DialogBgclMerge from './DialogBgclMerge.vue';
 import RuleAction from './RuleAction.vue';

+ 381 - 0
src/modules/chat-item/DialogEmailContent.vue

@@ -0,0 +1,381 @@
+<template>
+  <div class="dialog-chat-answer-content">
+    <div v-if="is_loading && type == 'znyj'" class="loading">
+      <a-spin></a-spin> <span>{{ loadingText }}</span>
+    </div>
+    <div v-if="is_loading && type == 'znws'" class="loading">
+      <a-spin></a-spin> <span>{{ loadingText }}</span>
+    </div>
+    <div class="answer-content-text answer-content-report" v-for="(item, index) in result_list" :key="index">
+      <div ref="answerRef" class="answer-content" v-if="typeof item === 'string'" v-html="renderHtml(item)"></div>
+      <HomeChart v-if="typeof item === 'object' && item.type != 'excel'" :origin="'home'" :data="item"></HomeChart>
+    </div>
+    <slot></slot>
+    <div class="report-loading" v-if="answer_status === 1">
+      <img src="../../assets/chat-report/report-ing.png" alt="" />
+      <span>{{ loadingText }}</span>
+    </div>
+  </div>
+  <!-- <div v-html="html_1"></div> -->
+</template>
+
+<script setup>
+import { ref, computed, watch, onMounted, onUnmounted, render, h } from 'vue';
+import HomeChart from '../chat_echart/HomeChart.vue';
+import marked from '../../3rd-libs/marked';
+import { Tooltip } from '@arco-design/web-vue';
+import { IconDownload } from '@arco-design/web-vue/es/icon';
+import * as XLSX from 'xlsx';
+// import TableHasDownload from '../chat_files/TableHasDownload.vue';
+// 接收markdown数据,进行一些转换
+const props = defineProps({
+  /**
+   * 内容类型
+   */
+  type: String,
+  /**
+   * 文本内容数组
+   */
+  result_list: Array,
+  /**
+   * 状态值
+   */
+  is_loading: Boolean,
+  /**
+   * 会话状态
+   * */
+  answer_status: Boolean
+});
+const answerRef = ref(null);
+const exportButtonList = ref([]);
+// 随机生成3-5之间的一位小数
+const generateRandomDecimal = () => {
+  // 生成3到5之间的随机数
+  const randomNum = 3 + Math.random() * 2;
+  // 保留一位小数
+  return Number(randomNum.toFixed(1));
+};
+
+// 加载文本状态
+const loadingText = ref('正在查询中...');
+const loadingTime = generateRandomDecimal();
+const loadingTexts = ['正在查询中...', '正在生成内容...', '正在处理数据...', '正在发送邮件...', '即将完成...'];
+let loadingInterval = null;
+let currentLoadingIndex = 0;
+
+// 开始顺序展示加载文本
+const startLoadingTextCycle = () => {
+  if (loadingInterval) clearInterval(loadingInterval);
+  currentLoadingIndex = 0;
+  loadingText.value = loadingTexts[currentLoadingIndex] + '(预计' + loadingTime + '分钟完成)';
+  // 生成随机数并添加到加载文本
+  // const randomTime = generateRandomDecimal();
+  // loadingText.value = `${loadingTexts[currentLoadingIndex]} (预计${randomTime}分钟后完成)`;
+
+  // 每分钟展示一条,不重复
+  loadingInterval = setInterval(() => {
+    currentLoadingIndex++;
+    if (currentLoadingIndex < loadingTexts.length) {
+      loadingText.value = loadingTexts[currentLoadingIndex] + '(预计' + loadingTime + '分钟完成)';
+      // const newRandomTime = generateRandomDecimal();
+      // loadingText.value = `${loadingTexts[currentLoadingIndex]} (预计${newRandomTime}秒后完成)`;
+    } else {
+      // 展示到最后一条后停止
+      clearInterval(loadingInterval);
+      loadingInterval = null;
+    }
+  }, 60000); // 每60秒切换一次
+};
+
+// 停止切换加载文本
+const stopLoadingTextCycle = () => {
+  if (loadingInterval) {
+    clearInterval(loadingInterval);
+    loadingInterval = null;
+  }
+};
+
+const content_style = computed(() => {
+  if (props.type === 'q') {
+    return {
+      background: 'var(--primary-default)',
+      color: '#fff'
+    };
+  }
+});
+
+const renderHtml = (item) => {
+  let renderContent = marked.parse(item);
+  renderContent = renderContent.replace(/^ {2}/gm, '&nbsp;&nbsp;&nbsp;&nbsp;');
+  renderContent = renderContent
+    .replace(/<p>(.*?)<\/p>/gs, (match, content) => {
+      return `<p class="indented" style="text-indent: 0em;">${content}</p>`;
+      // return `<p>${content}</p>`
+    })
+    .replace(/<br\s*\/?>([^<]*)/g, (match, content) => {
+      if (content.trim() === '') return match;
+      return `<br/><span style="display:inline-block;text-indent:0em;">${content}</span>`;
+    });
+  renderContent = renderContent
+    .replace(/<pre><code>/gs, '<p style="text-indent: 0em;">')
+    .replace(/<\/code><\/pre>/gs, '</p>');
+  return renderContent;
+};
+
+const createSingTableExportBtn = (table, index) => {
+  table.style.display = 'none';
+  // 创建包含标题和导出按钮的容器
+  const tableContainer = document.createElement('div');
+  tableContainer.className = 'custom-table-container';
+
+  // 创建标题区域
+  const titleDiv = document.createElement('div');
+  titleDiv.className = 'table-title-container';
+  titleDiv.innerHTML = '<span class="table-title">表格数据</span>';
+
+  // 创建按钮容器
+  const btnContainer = document.createElement('div');
+  btnContainer.className = 'arco-export-btn';
+  btnContainer.dataset.tableIndex = index; //标记表格索引
+  console.log(btnContainer, 112121);
+  // 渲染组件
+  render(
+    h(
+      Tooltip,
+      { content: `导出表格${index + 1}` },
+      {
+        default: () =>
+          h(IconDownload, {
+            size: 18,
+            style: { cursor: 'pointer', color: 'var(--color-primary)' },
+            onClick: () => handleSingleTableExport(table, index)
+          })
+      }
+    ),
+    btnContainer
+  );
+
+  // 将按钮添加到标题区域
+  titleDiv.appendChild(btnContainer);
+  tableContainer.appendChild(titleDiv);
+  // 将容器插入到表格前面
+  table.parentNode.insertBefore(tableContainer, table);
+  exportButtonList.value.push(btnContainer);
+};
+const handleSingleTableExport = (table, tableIndex) => {
+  const workbook = XLSX.utils.book_new();
+  const worksheet = XLSX.utils.table_to_sheet(table);
+  // 添加工作表到工作簿
+  XLSX.utils.book_append_sheet(workbook, worksheet, `表格${tableIndex + 1}数据`);
+
+  // 生成并下载Excel文件
+  XLSX.writeFile(workbook, `表格${tableIndex + 1}_数据导出.xlsx`);
+};
+watch(
+  () => props.answer_status,
+  (new_value) => {
+    // console.log('变化', props.answer_status);
+    if (new_value / 1 == 1) {
+      // 开始加载,重置索引
+      currentLoadingIndex = 0;
+      startLoadingTextCycle();
+    } else if (new_value / 1 == 2) {
+      // 加载完成
+      stopLoadingTextCycle();
+      if (props.type == 'znws') {
+        setTimeout(() => {
+          const tableContainers = document.querySelectorAll('.answer-content');
+          console.log(tableContainers);
+          const tableContainers_filter = Array.from(tableContainers).filter((item) => !item.getAttribute('data-table'));
+          const allTables = [];
+          tableContainers_filter.forEach((item) => {
+            item.setAttribute('data-table', '1');
+            if (item.querySelectorAll('table') && item.querySelectorAll('table').length) {
+              allTables.push(...Array.from(item.querySelectorAll('table')));
+            }
+          });
+          if (!allTables || !allTables.length) return;
+          Array.from(allTables).forEach((table, index) => {
+            createSingTableExportBtn(table, index);
+          });
+        }, 1000);
+      }
+    }
+  }
+);
+
+// 监听is_loading变化
+watch(
+  () => props.is_loading,
+  (newValue) => {
+    if (newValue) {
+      // 每次进入加载状态时重置索引,从第一条开始
+      currentLoadingIndex = 0;
+      startLoadingTextCycle();
+    } else {
+      stopLoadingTextCycle();
+    }
+  }
+);
+onMounted(() => {
+  if (props.result_list && props.result_list.length) {
+    if (props.type == 'znws') {
+      const tableContainers = document.querySelectorAll('.answer-content');
+      console.log(tableContainers);
+      const tableContainers_filter = Array.from(tableContainers).filter((item) => !item.getAttribute('data-table'));
+      const allTables = [];
+      tableContainers_filter.forEach((item) => {
+        item.setAttribute('data-table', '1');
+        if (item.querySelectorAll('table') && item.querySelectorAll('table').length) {
+          allTables.push(...Array.from(item.querySelectorAll('table')));
+        }
+      });
+      // const allTables = tableContainer?.querySelectorAll('table');
+      if (!allTables || !allTables.length) return;
+      Array.from(allTables).forEach((table, index) => {
+        createSingTableExportBtn(table, index);
+      });
+    }
+  }
+
+  // 初始检查加载状态
+  if (props.is_loading || props.answer_status === 1) {
+    startLoadingTextCycle();
+  }
+});
+
+// 组件卸载时清除定时器
+onUnmounted(() => {
+  stopLoadingTextCycle();
+});
+</script>
+
+<style lang="css" scoped>
+.dialog-chat-answer-content {
+  position: relative;
+  background-color: var(--color-bg-1);
+  color: var(--color-text-1);
+  border-radius: 12px;
+
+  padding-top: 15px;
+  padding-bottom: 15px;
+  padding-left: 30px;
+  padding-right: 30px;
+}
+
+.answer-content-report .answer-content * p {
+  text-indent: 2em !important;
+}
+
+.answer-content * p {
+  text-indent: 2em !important;
+}
+
+p {
+  text-indent: 2em !important;
+}
+
+.dialog-chat-answer-content code {
+  overflow-x: auto;
+}
+
+.answer-content-text {
+  line-height: 24px;
+}
+
+.loading {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+}
+</style>
+<style>
+.refferences {
+  background: rgb(238, 243, 254);
+  border: 1px solid rgb(255, 255, 255);
+  box-shadow: rgba(29, 48, 91, 0.2) 0px 1px 1px 0px;
+
+  padding: 2px 6px;
+  cursor: pointer;
+}
+
+.refferences:hover {
+  background-color: var(--primary-default);
+  color: #fff;
+}
+
+.indented br + * {
+  display: inline-block;
+  text-indent: 2em;
+}
+.report-loading {
+  margin-top: 10px;
+}
+.report-loading img {
+  width: 32px;
+}
+</style>
+<style>
+/* 表格基础样式 */
+.answer-content-text table {
+  width: 100%;
+  border-collapse: collapse; /* 合并边框 */
+  border-spacing: 0;
+  font-size: 14px;
+  line-height: 1.5;
+}
+
+/* 表头样式 */
+.answer-content-text th {
+  padding: 12px 16px;
+  background-color: #f5f5f5; /* 浅灰色背景 */
+  border: 1px solid #e8e8e8; /* 边框颜色 */
+  font-weight: 500;
+  text-align: center; /* 表头文字居中 */
+}
+
+/* 单元格样式 */
+.answer-content-text td {
+  padding: 12px 16px;
+  border: 1px solid #e8e8e8;
+  text-align: left; /* 默认左对齐,会被Markdown的对齐方式覆盖 */
+}
+
+/* 隔行变色(可选) */
+.answer-content-text tr:nth-child(even) {
+  background-color: #fafafa;
+}
+
+/* 鼠标悬停效果(可选) */
+.answer-content-text tr:hover {
+  background-color: #f0f7ff;
+}
+.arco-export-btn {
+  margin: 3px 0 0 0;
+  display: inline-block;
+}
+.arco-export-btn :deep(.arco-icon) {
+  transition: color 0.2s ease;
+}
+.arco-export-btn :deep(.arco-icon:hover) {
+  color: var(--primary-default);
+}
+
+.custom-table-container {
+  /* margin: 3px 0; */
+}
+
+.table-title-container {
+  background-color: #f5f5f5;
+  padding: 8px 16px;
+  border-radius: 4px;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.table-title {
+  font-weight: 600;
+  color: #333;
+}
+</style>

+ 12 - 15
src/modules/chat-item/DialogThinkContent.vue

@@ -31,13 +31,12 @@ const show_think_title = computed(() => {
 const handleThinkClick = () => {
   show_think_page.value = !show_think_page.value;
 };
-
 </script>
 <style scoped>
 .think {
   /* background: #D6F8F8; */
   /* background: rgba(10, 129, 126, 0.06); */
-  color: #2C2C2C;
+  color: #2c2c2c;
   margin-bottom: 10px;
 }
 
@@ -52,18 +51,17 @@ const handleThinkClick = () => {
   font-weight: 600;
   font-family: 思源黑体;
   font-size: 14px;
-  color: #0A817E;
+  color: #0a817e;
   padding: 19px 20px;
-
-  .down {
-    margin-left: 10px;
-    color: #0A817E;
-  }
+}
+.think-select .down {
+  margin-left: 10px;
+  color: #0a817e;
 }
 
 .think_select span {
   padding: 0px 10px;
-  color: #0A817E;
+  color: #0a817e;
   font-size: 14px;
   font-weight: 600;
   font-family: 思源黑体;
@@ -73,12 +71,11 @@ const handleThinkClick = () => {
 .think-select:hover {
   font-weight: 600;
   font-family: 思源黑体;
-  color: #0A817E;
+  color: #0a817e;
   background: rgba(10, 129, 126, 0.06);
   opacity: 0.8;
 }
 
-
 .think_content {
   /* background: #D6F8F8; */
   background: rgba(10, 129, 126, 0.06);
@@ -96,7 +93,7 @@ const handleThinkClick = () => {
   gap: 10px;
   cursor: pointer;
   border-radius: 12px;
-  color: #0A817E;
+  color: #0a817e;
   font-weight: 600;
   font-family: 思源黑体;
 }
@@ -107,10 +104,10 @@ const handleThinkClick = () => {
   color: var(--color-text-2); */
 }
 
-.content-title-left {}
+.content-title-left {
+}
 
 .content-text {
-
   /* margin-left: 20px;
   border-left: 1px solid #ccc; */
   padding: 0px 20px 10px 20px;
@@ -135,7 +132,7 @@ const handleThinkClick = () => {
   text-align: left;
 }
 
-.content-text .katex-display>.katex {
+.content-text .katex-display > .katex {
   white-space: pre-wrap !important;
 }
 </style>

+ 3 - 1
src/modules/common/use-icon-map.js

@@ -77,7 +77,9 @@ import production_icon_3 from '../../assets/chat-tips/production-3.svg'
 export const production_icon_map = {
   production_icon_1: production_icon_1,
   production_icon_2: production_icon_2,
-  production_icon_3: production_icon_3
+  production_icon_3: production_icon_3,
+  production_icon_4: production_icon_1,
+  production_icon_5: production_icon_2,
 }
 
 export const icon_map_a = {

+ 21 - 1
src/modules/conversation-chat/ChatTips.vue

@@ -50,7 +50,8 @@ const chat_type_map = {
   znwd: '智库问答',
   bgsc: '报告自动生成',
   znws: '智能问数',
-  bgcl: '智能报表'
+  bgcl: '智能报表',
+  znyj: '邮件自动发送'
 };
 
 const theme_map = {
@@ -155,6 +156,23 @@ const production_bgcl = ref([
     icon: 'production_icon_3'
   }
 ]);
+const production_znyj = ref([
+  {
+    title: '邮件自动发送',
+    description: '提供标准化一站式邮件自动发送能力,支持任务创建、发送与全流程管理,保障高效稳定投递。',
+    icon: 'production_icon_1'
+  },
+  {
+    title: '自然语言全链路投递',
+    description: '打通智能对话接口,支持自然语言完成数据、图表、校验与投递全流程,实现零代码便捷操作。',
+    icon: 'production_icon_2'
+  },
+  {
+    title: '定时与实时任务',
+    description: '支持定时无人值守与手动实时触发双模式,灵活适配各类邮件发送场景',
+    icon: 'production_icon_3'
+  }
+]);
 const production_list = computed(() => {
   if (props.type == 'znwd') {
     console.log('znwd');
@@ -166,6 +184,8 @@ const production_list = computed(() => {
     return production_znws.value;
   } else if (props.type == 'bgcl') {
     return production_bgcl.value;
+  } else if (props.type == 'znyj') {
+    return production_znyj.value;
   }
 });
 const questions_znwd = ref([

+ 2 - 1
src/modules/conversation-chat/DialogInfo.vue

@@ -56,7 +56,8 @@ const chat_type_map = {
   znwd: '智库问答',
   bgsc: '报告自动生成',
   znws: '智能问数',
-  bgcl: '智能报表'
+  bgcl: '智能报表',
+  znyj: '邮件自动发送'
 };
 
 const theme_map = {

+ 116 - 11
src/modules/conversation-chat/DialogInput.vue

@@ -10,11 +10,16 @@
           :file="upload_file_for_outline"
           @removeFile="removeFileForOutLine"
         ></DocxFile>
+        <ReportTypeList
+          v-if="chat_type == 'znyj'"
+          :report_type_list="report_type_list"
+          @select-report-type="handleSelectReportType"
+        ></ReportTypeList>
       </slot>
 
       <div class="textarea-container" v-if="has_textarea">
         <a-textarea
-          v-if="chat_type != 'bgsc'"
+          v-if="chat_type != 'bgsc' && chat_type != 'znyj'"
           ref="chat_input_textarea"
           class="chat-textarea-input"
           :placeholder="text_placeholder"
@@ -23,7 +28,7 @@
           @input="handleTextInput"
           @keyup="handleTextKeyUp"
         />
-        <div v-else style="width: 100%">
+        <div v-if="chat_type == 'bgsc'" style="width: 100%">
           <TextAreaTemplate
             v-if="is_template_input && !show_template_input"
             :target_template_item="selected_template"
@@ -39,20 +44,19 @@
             @input="handleTextInput"
             @keyup="handleTextKeyUp"
           />
-          <!-- <div
-            class="template_textarea"
-            v-html="template_text_value"
-            contenteditable
-            @input="handleTextInput"
-            @keyup="handleTextKeyUp"
-            v-if="!is_template_input && show_template_input"
-          ></div> -->
           <TextAreaTemplateSelected
             v-if="!is_template_input && show_template_input"
             :target_template_item="selected_template"
             @submit_selected_template="handleSubmitSelectedTemplate"
           ></TextAreaTemplateSelected>
         </div>
+        <div v-if="chat_type == 'znyj'">
+          <TextAreaEmail
+            v-if="chat_type === 'znyj'"
+            ref="emailAreaRef"
+            :target_report_type="target_report_type"
+          ></TextAreaEmail>
+        </div>
       </div>
       <InputActions
         :has_input_text="has_input_text"
@@ -70,6 +74,7 @@
         @open_template_select="openTemplateSelect"
         @open_draft="openDraft"
         @open_data_source="openDataSource"
+        @open_task_config="openTaskConfig"
       />
       <slot name="bottom"></slot>
     </div>
@@ -96,6 +101,11 @@
       @open_data_source_manage="openDataSourceManage"
       @edit-data-source="handleEditDataSource"
     ></DataSourceModal>
+    <TaskConfigModal
+      v-if="show_task_config_model"
+      :show_modal="show_task_config_model"
+      @close-task-config-modal="closeTaskConfigModal"
+    ></TaskConfigModal>
   </div>
 </template>
 
@@ -103,13 +113,16 @@
 import { ref, onMounted, onUnmounted, watch, computed, nextTick } from 'vue';
 import InputActions from './dialog-input/InputActions.vue';
 import UploadList from './upload-action/UploadList.vue';
+import ReportTypeList from './dialog-input/email-input/ReportTypeList.vue';
 import DocxFile from './upload-action/DocxFile.vue';
 import TextAreaTemplate from './dialog-input/TextAreaTemplate.vue';
 import TextAreaTemplateSelected from './dialog-input/TextAreaTemplateSelected.vue';
+import TextAreaEmail from './dialog-input/email-input/TextAreaEmail.vue';
 
 import TemplateSelectModal from '../template-manage/TempalteSelectModal.vue';
 import DraftModal from '../draft-manage/DraftModal.vue';
 import DataSourceModal from '../data-source-manage/DataSourceModal.vue';
+import TaskConfigModal from '../task-config/TaskConfigModal.vue';
 // import FileUploadPreviewV1 from '../file-preview/FileUploadPreviewV1.vue';
 // import useFileSelect from './use-file-select';
 // import useFileSelectedV2 from './use-file-select-v2';
@@ -161,6 +174,8 @@ const text_placeholder = ref('请输入你的问题');
 
 // 文件列表-用于展示
 const upload_file_list = ref([]);
+// 邮件自动发送-报表类型列表
+const report_type_list = ref(window.report_type_list);
 // 文件队列-用于流程
 const upload_queue = ref([]);
 // 是否正在上传
@@ -177,6 +192,14 @@ const show_template_select_model = ref(false);
 const show_draft_model = ref(false);
 // 是否展示数据源弹窗
 const show_data_source_model = ref(false);
+// 是否展示任务配置弹窗
+const show_task_config_model = ref(false);
+// 邮件自动发送-选中的报表类型
+const target_report_type = ref('1');
+
+// 邮件自动发送组件
+const emailAreaRef = ref(null);
+
 // 选中的模板
 const selected_template = ref(null);
 // 是否显示上传模板按钮
@@ -237,6 +260,27 @@ const handleSubmit = () => {
     input_text_value.value = window.bgcl_question;
     input_file_list.value = upload_file_list.value;
   }
+  if (props.chat_type === 'znyj') {
+    const has_content = checkEmailSubmitFormat(emailAreaRef.value);
+    console.log('has_content');
+    if (!has_content) return;
+
+    const { date, subject, toEmail, ccEmail } = emailAreaRef.value;
+    const is_toEmail_valid = checkEmailFormat(toEmail.value);
+    if (!is_toEmail_valid) {
+      Message.error('收件邮箱格式错误,多个邮箱间以逗号隔开');
+      return;
+    }
+    const is_ccEmail_valid = checkEmailFormat(toEmail.value);
+    if (!is_ccEmail_valid) {
+      Message.error('抄送邮箱格式错误,多个邮箱间以逗号隔开');
+      return;
+    }
+
+    input_text_value.value = `帮我生成${date}的全省供电可靠性报告,主题为${subject},收件邮箱为${toEmail},抄送人邮箱为${ccEmail}`;
+
+    console.log(date, subject, toEmail, ccEmail);
+  }
   // 检查是否是禁用状态
   if (!input_text_value.value.trim()) {
     selected_template.value = null;
@@ -250,12 +294,46 @@ const handleSubmit = () => {
     text: input_text_value.value,
     files: input_file_list.value,
     event_id: event_id.value || '',
-    template_id: selected_template.value?.id || ''
+    template_id: selected_template.value?.id || '',
+    email_type: window.report_type_list.find((item) => item.key === target_report_type.value)?.email_type || ''
     // config: action_config.value
   });
+  emailAreaRef.value.clear();
   is_template_input.value = false;
   show_template_input.value = false;
 };
+// 邮件自动发送-检查提交格式
+const checkEmailSubmitFormat = (obj) => {
+  const { date, subject, toEmail, ccEmail } = obj;
+  if (!date) {
+    Message.error('请填写日期');
+    return false;
+  }
+  if (!subject) {
+    Message.error('请填写主题');
+    return false;
+  }
+  if (!toEmail) {
+    Message.error('请填写收件邮箱');
+    return false;
+  }
+  if (!ccEmail) {
+    Message.error('请填写抄送邮箱');
+    return false;
+  }
+  return true;
+};
+// 邮件自动发送-检查邮箱格式
+const checkEmailFormat = (email_str) => {
+  if (!email_str) return true;
+
+  const emails = email_str
+    .split(',')
+    .map((email) => email.trim())
+    .filter((email) => email);
+  const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
+  return emails.every((email) => emailRegex.test(email));
+};
 const handleStop = () => {
   emit('stop');
 };
@@ -295,6 +373,10 @@ const handleSubmitSelectedTemplate = (text) => {
     is_template_input.value = false;
   }
 };
+// 邮件自动发送选中的报表类型
+const handleSelectReportType = (type) => {
+  target_report_type.value = type;
+};
 
 const uploadFileForOutline = (file) => {
   upload_file_for_outline.value = {
@@ -412,6 +494,9 @@ const clear = () => {
   selected_template.value = null;
   show_upload_for_ouline.value = true;
   upload_file_for_outline.value = {};
+  if (props.chat_type === 'znyj') {
+    input_text_value.value = '帮我生成{date}的全省供电可靠性报告,主题为{title},收件邮箱为{toEmail},抄送邮箱为{ccEmail}';
+  }
 };
 const toRecommend = (txt) => {
   input_text_value.value = txt;
@@ -432,6 +517,14 @@ const closeDraftModal = () => {
 const openDataSource = () => {
   show_data_source_model.value = true;
 };
+// 打开任务配置弹窗
+const openTaskConfig = () => {
+  show_task_config_model.value = true;
+};
+// 关闭任务配置弹窗
+const closeTaskConfigModal = () => {
+  show_task_config_model.value = false;
+};
 // 关闭数据源弹窗
 const closeDataSourceModal = () => {
   show_data_source_model.value = false;
@@ -578,6 +671,18 @@ watch(
   }
 );
 
+watch(
+  () => props.chat_type,
+  () => {
+    if (props.chat_type === 'znyj') {
+      input_text_value.value =
+        '帮我生成{date}的全省供电可靠性报告,主题为{title},收件邮箱为{toEmail},抄送邮箱为{ccEmail}';
+    }
+  },
+  {
+    immediate: true
+  }
+);
 onMounted(() => {
   // 对话输入框自动获得焦点
   autoFocus();

+ 14 - 0
src/modules/conversation-chat/dialog-input/InputActions.vue

@@ -11,6 +11,7 @@
       <RecommendActions v-if="has_recommend" @click="handleRecommend"></RecommendActions>
       <DraftConfig v-if="has_draft" @open_draft="openDraft"></DraftConfig>
       <DataSourceManage v-if="has_data_source" @open_data_source="openDataSource"></DataSourceManage>
+      <TaskConfig v-if="has_task_conifg" @open_task_config="openTaskConfig"></TaskConfig>
     </div>
 
     <div class="dialog-input-action-right">
@@ -61,6 +62,7 @@ import RecommendModal from './RecommendModal.vue';
 import TemplateConfig from './TemplateConfig.vue';
 import DraftConfig from './DraftConfig.vue';
 import DataSourceManage from './DataSourceManage.vue';
+import TaskConfig from './TaskConfig.vue';
 import { Message } from '@arco-design/web-vue';
 import submit_normal_a from '../../../assets/chat-input/submit_normal.png';
 import submit_normal_b from '../../../assets/chat-input/submit_normal_b.png';
@@ -264,6 +266,9 @@ const has_draft = computed(() => {
 const has_data_source = computed(() => {
   return props.chat_type === 'bgsc';
 });
+const has_task_conifg = computed(() => {
+  return props.chat_type === 'znyj';
+});
 
 /**
  * 按钮状态
@@ -278,6 +283,12 @@ const btn_status = computed(() => {
     } else {
       return 0;
     }
+  } else if (props.chat_type === 'znyj') {
+    if (props.is_input_ing) {
+      return 2;
+    } else {
+      return 1;
+    }
   } else {
     if (props.is_input_ing) {
       return 2;
@@ -300,6 +311,9 @@ const openDraft = () => {
 const openDataSource = () => {
   emit('open_data_source');
 };
+const openTaskConfig = () => {
+  emit('open_task_config');
+};
 
 const handleSubmit = () => {
   emit('submit');

+ 123 - 0
src/modules/conversation-chat/dialog-input/TaskConfig.vue

@@ -0,0 +1,123 @@
+<template>
+  <a-dropdown position="top" class="my_custom_drop">
+    <a-button class="btn" @click="toOpenTaskConfig">
+      <img :src="icon_table" alt="" />
+      <span>任务配置</span>
+      <icon-down />
+    </a-button>
+  </a-dropdown>
+</template>
+
+<script setup>
+import { ref, onMounted, computed } from 'vue';
+
+import icon_file_a from '../../../assets/chat-report/drafy_icon_1.svg';
+import icon_file_b from '../../../assets/chat-report/drafy_icon_2.svg';
+import icon_file_c from '../../../assets/chat-report/drafy_icon_3.svg';
+
+import { useThemeStore } from '../../../base/store/use-theme';
+import { THEME_LIGHT, THEME_DARK, THEME_GREEN, THEME_PARTY } from '../../../base/variables';
+
+const themeStore = useThemeStore();
+
+const emit = defineEmits(['open_task_config']);
+
+const icon_table = computed(() => {
+  if (themeStore.value == THEME_GREEN) {
+    return icon_file_a;
+  } else if (themeStore.value == THEME_PARTY) {
+    return icon_file_b;
+  } else if (themeStore.value == THEME_LIGHT) {
+    return icon_file_c;
+  } else {
+    return icon_file_a;
+  }
+});
+const toOpenTaskConfig = () => {
+  // console.log('toTemplateManage');
+  emit('open_task_config');
+};
+onMounted(() => {});
+</script>
+<style scoped lang="css">
+.icon {
+  cursor: pointer;
+  width: 24px;
+}
+
+.btn {
+  background: var(--bg-title);
+  border-radius: 20px;
+  font-weight: 500;
+  font-size: 14px;
+  color: var(--primary-default);
+  display: flex;
+  align-items: center;
+  gap: 5px;
+}
+
+.btn:hover {
+  background: var(--bg-title);
+  color: var(--primary-default);
+  opacity: 0.8;
+}
+
+.dopt-item {
+  padding: 0px 20px;
+}
+
+.arco-dropdown-open .arco-icon-down {
+  transform: rotate(180deg);
+}
+.dop {
+  width: 160px;
+  display: flex;
+  justify-content: space-between;
+  padding: 5px;
+  border-bottom: 1px solid #e5e5e5;
+}
+.dop .dop-item {
+  display: flex;
+  flex-direction: column;
+}
+.dop-item .item-title {
+  font-family: 思源黑体;
+  font-weight: 600;
+  line-height: normal;
+  letter-spacing: 0px;
+  color: #2c2c2c;
+}
+.dop-item .item-desc {
+  font-family: 思源黑体;
+  font-size: 12px;
+  font-weight: normal;
+  line-height: normal;
+  letter-spacing: 0px;
+  color: #687272;
+}
+.dop-item-selected {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+.dop-item-selected img {
+  width: 16px;
+}
+.dop-bottom {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 5px;
+  cursor: pointer;
+  padding: 15px 0px 5px 0px;
+}
+.dop-bottom .icon {
+  width: 16px;
+}
+</style>
+<style>
+.my_custom_drop .arco-dropdown {
+  padding: 10px 0;
+  border-radius: 12px;
+}
+</style>

+ 116 - 0
src/modules/conversation-chat/dialog-input/email-input/ReportTypeList.vue

@@ -0,0 +1,116 @@
+<template>
+  <div class="upload-list">
+    <img class="to-left" src="../../../../assets/chat-input/to_left.svg" alt="" @click="scrollLeft" />
+    <img class="to-right" src="../../../../assets/chat-input/to_right.svg" alt="" @click="scrollRight" />
+
+    <div class="scroll-container" ref="scrollContainer">
+      <TypeItem
+        v-for="item in report_type_list"
+        :key="item.key"
+        :file="item"
+        :is-selected="item.key === selectedKey"
+        @removeFile="remove(item.key)"
+        @select="handleSelect"
+      >
+      </TypeItem>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, nextTick, onMounted } from 'vue';
+import TypeItem from './TypeItem.vue';
+const props = defineProps({
+  report_type_list: Array
+});
+
+const emit = defineEmits(['removeFile', 'select-report-type']);
+// 获取滚动容器的 ref
+const scrollContainer = ref(null);
+// 跟踪选中的文件
+const selectedKey = ref('1');
+// 假设每个文件项(含间隙)的宽度,可根据实际样式调试,比如这里算上 gap 等,假设为 120px
+// const itemWidth = 120;
+// 动态计算第一个文件项宽度并滚动
+const scrollByFirstItemWidth = (direction) => {
+  nextTick(() => {
+    if (!scrollContainer.value || !scrollContainer.value.firstElementChild) return;
+
+    // 获取第一个文件项的实际宽度 + 间距
+    const firstItem = scrollContainer.value.firstElementChild;
+    const gap = parseInt(getComputedStyle(scrollContainer.value).gap) || 0;
+    const itemWidth = firstItem.offsetWidth + gap;
+
+    // 根据方向滚动
+    scrollContainer.value.scrollBy({
+      left: direction === 'right' ? itemWidth : -itemWidth,
+      behavior: 'smooth' // 添加平滑滚动效果
+    });
+  });
+};
+
+// 向左滚动方法
+const scrollLeft = () => scrollByFirstItemWidth('left');
+
+// 向右滚动方法
+const scrollRight = () => scrollByFirstItemWidth('right');
+
+const remove = (key) => {
+  emit('removeFile', key);
+};
+
+// 处理选择文件
+const handleSelect = (key) => {
+  selectedKey.value = key;
+  emit('select-report-type', selectedKey.value);
+};
+onMounted(() => {
+  selectedKey.value = props.report_type_list[0]?.key || '1';
+  emit('select-report-type', selectedKey.value);
+});
+</script>
+<style scoped lang="css">
+.upload-list {
+  position: relative;
+  padding: 0px 20px 10px 20px;
+  display: flex;
+  /* 父容器需给滚动容器留出空间,这里去掉 gap,交给 scroll-container 内部处理 */
+  /* gap: 20px;  */
+}
+
+.to-left {
+  position: absolute;
+  left: -10px;
+  top: 32px;
+  z-index: 3;
+  cursor: pointer;
+}
+
+.to-right {
+  position: absolute;
+  right: -10px;
+  top: 32px;
+  z-index: 3;
+  cursor: pointer;
+}
+
+/* 滚动容器样式 */
+.scroll-container {
+  box-sizing: border-box;
+  padding-top: 5px;
+  display: flex;
+  gap: 20px;
+  /* 允许横向滚动 */
+  overflow-x: auto;
+  /* 隐藏滚动条,也可根据需求保留,调整样式 */
+  scrollbar-width: none;
+  /* 限制可视宽度,4 个文件 + 间隙的大致宽度,可根据实际微调 */
+  width: 100%;
+  z-index: 3;
+}
+
+/* 隐藏 Chrome 等浏览器的滚动条(可选) */
+.scroll-container::-webkit-scrollbar {
+  display: none;
+}
+</style>

+ 100 - 0
src/modules/conversation-chat/dialog-input/email-input/TextAreaEmail.vue

@@ -0,0 +1,100 @@
+<template>
+  <div class="email-textarea">
+    <div class="email-content">
+      <span>帮我生成</span>
+      <a-input
+        v-model="date"
+        placeholder="选择日期"
+        style="width: 100px; margin: 0 8px; color: var(--primary-default); background-color: var(--tip-bg-start)"
+      />
+      <span>的全省供电可靠性报告,主题为</span>
+      <a-input
+        v-model="subject"
+        placeholder="输入主题"
+        style="width: 150px; margin: 0 8px; color: var(--primary-default); background-color: var(--tip-bg-start)"
+      />
+      <span title="多个邮箱之间使用逗号(,)隔开">,收件邮箱为</span>
+      <a-input
+        v-model="toEmail"
+        title="多个邮箱之间使用逗号(,)隔开"
+        placeholder="输入收件邮箱"
+        style="width: 400px; margin: 0 8px; color: var(--primary-default); background-color: var(--tip-bg-start)"
+      />
+      <span title="多个邮箱之间使用逗号(,)隔开">,抄送邮箱为</span>
+      <a-input
+        v-model="ccEmail"
+        title="多个邮箱之间使用逗号(,)隔开"
+        placeholder="输入抄送邮箱"
+        style="width: 400px; margin: 0 8px; color: var(--primary-default); background-color: var(--tip-bg-start)"
+      />
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, watch } from 'vue';
+
+// 接收父组件传递的报表类型
+const props = defineProps({
+  target_report_type: {
+    type: String,
+    default: '1'
+  }
+});
+
+// 响应式数据
+const date = ref('');
+const subject = ref('频停周报');
+const toEmail = ref('');
+const ccEmail = ref('');
+
+const clear = () => {
+  toEmail.value = '';
+  ccEmail.value = '';
+};
+
+watch(
+  () => props.target_report_type,
+  (new_val) => {
+    const data = window.report_type_list.find((item) => item.key === new_val);
+    if (data.date) {
+      date.value = data.date;
+    }
+    if (data.title) {
+      subject.value = data.title;
+    }
+  },
+  {
+    immediate: true
+  }
+);
+defineExpose({
+  date,
+  subject,
+  toEmail,
+  ccEmail,
+  clear
+});
+</script>
+
+<style scoped lang="css">
+.email-textarea {
+  width: 100%;
+  padding: 16px;
+  /* background-color: #f5f5f5; */
+  border-radius: 8px;
+}
+
+.email-content {
+  display: flex;
+  align-items: center;
+  flex-wrap: wrap;
+  gap: 8px;
+  font-size: 14px;
+  line-height: 1.5;
+}
+
+.email-content span {
+  white-space: nowrap;
+}
+</style>

+ 122 - 0
src/modules/conversation-chat/dialog-input/email-input/TypeItem.vue

@@ -0,0 +1,122 @@
+<template>
+  <div class="report-file" :class="{ selected: isSelected }" @click="handleClick">
+    <img class="icon" :src="DOCX" alt="" />
+    <div class="info">
+      <a-tooltip :content="file.title">
+        <div class="title">{{ ellipsisMiddle(file.title) }}</div>
+      </a-tooltip>
+      <div class="size">{{ file.desc }}</div>
+    </div>
+    <!-- <div class="close" :class="[file.status === 1 ? 'close_disabled' : '']" @click="remove(file)">
+      <img src="../../../../assets/chat-input/to_close.svg" alt="" />
+    </div> -->
+  </div>
+</template>
+
+<script setup>
+import { ref, computed } from 'vue';
+import DOCX from '../../../../assets/chat-report/word-icon.svg';
+
+const props = defineProps({
+  file: Object,
+  isSelected: {
+    type: Boolean,
+    default: false
+  }
+});
+
+const emit = defineEmits(['toDownload', 'removeFile', 'select']);
+
+const handleClick = () => {
+  emit('select', props.file.key);
+};
+
+function ellipsisMiddle(str) {
+  if (str.length <= 11) {
+    return str; // 不超过 11 个字符直接返回
+  }
+
+  const leftLength = Math.ceil(4); // 左侧保留 5 个字符(向上取整)
+  const rightLength = 6; // 右侧保留 5 个字符
+
+  return str.slice(0, leftLength) + '...' + str.slice(-rightLength);
+}
+const remove = (file) => {
+  // 正在上传文件不允许删除
+  if (file.status / 1 === 1) return;
+  emit('removeFile');
+};
+</script>
+
+<style scoped lang="css">
+.report-file {
+  position: relative;
+  width: 213px;
+  min-width: 213px;
+  /* margin-top: 15px; */
+  background-color: #fff;
+  padding: 15px 20px;
+  display: flex;
+  gap: 10px;
+  border-radius: 10px;
+  border: 1px solid #e5e5e5;
+  cursor: pointer;
+}
+.report-file:hover {
+  background-color: rgba(10, 129, 126, 0.05);
+}
+
+.icon {
+  width: 46px;
+}
+
+.info {
+  font-family: '思源黑体';
+  font-weight: 600;
+  display: flex;
+  flex-direction: column;
+  /* align-items: center; */
+  justify-content: center;
+  gap: 8px;
+}
+
+.info .title {
+  line-height: 1.5;
+}
+
+.size {
+  font-family: SourceHanSansCN, SourceHanSansCN;
+  font-weight: 400;
+  font-size: 14px;
+  color: #b0bebd;
+}
+
+.error_status {
+  color: #d63030;
+}
+
+.ing_status {
+  color: var(--primary-default);
+}
+
+.close {
+  position: absolute;
+  top: -5px;
+  right: -5px;
+  cursor: pointer;
+  z-index: 999;
+}
+
+.close_disabled {
+  cursor: not-allowed;
+}
+
+.report-file.selected {
+  border-color: var(--primary-default);
+  background-color: rgba(10, 129, 126, 0.05);
+}
+
+.report-file.selected .title {
+  color: var(--primary-default);
+}
+</style>

+ 0 - 0
src/modules/conversation-chat/dialog-input/email-input/moke_data.js


+ 59 - 0
src/modules/conversation-dialog/api-dialog.js

@@ -565,3 +565,62 @@ export const uploadForOutline = (data) => {
       });
   });
 }
+
+// 邮件自动阀发送 -- 任务列表
+export const getEmailList = ({ page, page_size, select_name }) => {
+  return new Promise((resolve, reject) => {
+    ajax
+      .post(
+        `/iaserverapi/v1/aiEmail/task/view?page=${page}&page_size=${page_size}&select_name=${select_name}`,
+        {
+          headers: {
+            "Content-Type": 'application/json'
+          },
+
+        }
+      )
+      .then((res) => {
+        resolve({ code: res.code, data: res.data, msg: res.msg || '' });
+      })
+  })
+}
+
+// 邮件自动阀发送 -- 任务编辑
+export const editEmailTask = (data) => {
+  return new Promise((resolve, reject) => {
+    ajax
+      .post(
+        `/iaserverapi/v1/aiEmail/task/edit`,
+        data,
+        {
+          headers: {
+            "Content-Type": 'application/json'
+          },
+
+        }
+      )
+      .then((res) => {
+        resolve({ code: res.code, data: res.data, msg: res.msg || '' });
+      })
+  })
+}
+
+// 邮件自动阀发送 -- 新增任务
+export const addEmailTask = (data) => {
+  return new Promise((resolve, reject) => {
+    ajax
+      .post(
+        `/iaserverapi/v1/aiEmail/task/add`,
+        data,
+        {
+          headers: {
+            "Content-Type": 'application/json'
+          },
+        }
+      )
+      .then((res) => {
+        resolve({ code: res.code, data: res.data, msg: res.msg || '' });
+      })
+  })
+}
+

+ 6 - 0
src/modules/conversation-dialog/format-dialog-answer-from-history.js

@@ -277,6 +277,12 @@ export default function formatDialogAnswerFromHistory(item, type, fileLength = 0
     history_item.answer_origin = item.answer
     history_item.answer_evaluation = item.status
   }
+  if (type === '5') {
+    const result_list = splitStringByDelimiter(item.answer, 'chart', item.references?.markdown_chart)
+    history_item.result_list = result_list
+    history_item.answer_origin = item.answer
+    history_item.answer_evaluation = item.status
+  }
   if (type === '4') {
     const obj = JSON.parse(item.answer)
     console.log(obj, item, fileLength, 9090, '表格')

+ 166 - 0
src/modules/conversation-dialog/format-dialog-answer-from-sse_receive_message_email.js

@@ -0,0 +1,166 @@
+import { jsonParse, jsonStringify } from '../../utils/utils';
+import generateClientUniqueId from '../../utils/generate-client-unique-id';
+
+import { formatCodePython, formatCodeSql } from './format-code-string';
+// import { getIcon4Answer } from './get-icon-4-dialog';
+// import { formatMarkdownString } from './format-anwser-string';
+// import { formatStaticAssetsUrl } from '../../utils/utils';
+
+import {
+  formatRefferenceString,
+  formatRefferenceFlag,
+  formatFileDownloadStr
+} from './format-dialog-answer-custom-markdown-str';
+
+import formatDialogAnswerAttachedHtmlBeforeRender from './format-dialog-answer-attached-html-before-render';
+// import { isDate } from 'lodash-es';
+
+/**
+ * 格式化sse发送消息的参数
+ * 此方法返回的数据项,为智能体返回的回答的数据
+ *
+ * 请注意,通常情况下,此文件改动,可能意味着 ./format-dialog-answer-from-history.js 文件也需要改动
+ * 此文件是针对 实时对话 进行的回答字符串的处理
+ * ./format-dialog-answer-from-history.js 文件是针对历史会话回答的字符串进行处理
+ * @param {string} message_str
+ * @param {object} preMsg - 前一次的消息,如果是流式消息,则需要上一次的Content进行拼接,从而形成新的消息
+ * @returns
+ */
+// let fountThink = false;
+// let message_id = '';
+export default function formatDialogAnswerFromSseReceiveMessageReport(res_data, message_obj, preMsg, is_end, markdown_chart) {
+  let res_content
+  if (Array.isArray(message_obj)) {
+    res_content = message_obj[0];
+    if (!res_content) {
+      res_content = { "text": { "value": "##" }, "type": "text" }
+
+    }
+  }
+
+  // console.log(message_obj, 999)
+  // console.log(res_content, 888)
+
+  // 复制一份原消息内容
+  const copyPreMsg = jsonParse(jsonStringify(preMsg));
+
+  // const icon_src = getIcon4Answer(agentInfo);
+
+  /**
+   * 类别:
+   * message_end
+   * error
+   * message
+   * stream
+   * system
+   */
+  // 报表合并返回的数据:需要进行文件下载
+
+  // 本次对话的消息内容
+  const currentMsg = {
+    /**
+     * 对话消息类别
+     * 0 历史消息
+     * 1 新消息(本次进入页面新建的消息,但是已经结束轮次)
+     * 2 当前的消息(此刻正在进行中的消息)
+     */
+    client_custom_chat_type: 2,
+    /**
+     * 此项的类别
+     * a 智能体的回答
+     * q 客户端的提问
+     */
+    client_custom_item_type: 'a', // 回答的答案
+    /**
+     * 会话展示时,可以通过此值,判断该如何进行内容显示
+     * 当值为0时,可能显示加载中动画
+     * 当值为1时,展示消息内容,展示 停止生成按钮
+     * 当值为2时,可能展示反馈操作按钮
+     */
+    client_custom_chat_status: 1, // 此轮会话的状态值 0 加载中 1 进行中 2 已结束
+
+    // 图标
+    // client_custom_icon: icon_src,
+    client_custom_icon: '',
+
+    question: res_content?.question, // 用户的提问内容
+
+    // 进行中的消息: 系统消息
+    client_custom_running_msg: '',
+    client_custom_chat_message_id: '',
+    deep_answer: '',
+    parent_id: '',
+  };
+
+
+
+  // 0 未开始 1 进行中 2 已结束 3 错误
+  let chat_status = 0;
+  let chat_msg = '';
+
+
+
+  chat_status = 1
+  currentMsg.result_list = []
+  if (preMsg && preMsg.result_list) {
+    currentMsg.result_list = [...preMsg.result_list]
+    if (res_content.text.value === 'chart') {
+      console.log(markdown_chart, 8989)
+      if (markdown_chart) {
+        currentMsg.result_list.push(markdown_chart)
+      }
+    } else {
+      // 上一项是字符串
+      let lastItem = currentMsg.result_list.pop();
+      if (typeof lastItem === 'object') {
+        currentMsg.result_list.push(lastItem)
+        currentMsg.result_list.push(res_content.text.value)
+      } else {
+        lastItem += res_content.text.value
+        currentMsg.result_list.push(lastItem)
+      }
+    }
+
+  } else {
+
+    currentMsg.result_list.push(res_content?.text?.value)
+    currentMsg.answer_origin = res_content?.text?.value
+
+
+  }
+
+
+  // if (preMsg && preMsg.answer && preMsg.answer_origin) {
+
+  //   currentMsg.answer = preMsg.answer + (res_content.text.value || '')
+  //   currentMsg.answer_origin = preMsg.answer_origin + (res_content.text.value || '')
+  // } else {
+  //   currentMsg.answer = res_content?.text?.value
+  //   currentMsg.answer_origin = res_content?.text?.value
+  // }
+
+
+  if (is_end) {
+    chat_status = 2
+    currentMsg.client_custom_chat_status = 2;
+    currentMsg.client_custom_chat_message_id = res_data.id
+  }
+
+  /**
+   * 依据智能体回答的内容
+   * 生成一个唯一的ID
+   */
+  currentMsg.client_custom_unique_id = generateClientUniqueId(currentMsg.answer);
+
+
+  formatDialogAnswerAttachedHtmlBeforeRender(currentMsg);
+  // if (message_obj.message_id) {
+  //   currentMsg.client_custom_chat_message_id = message_obj.message_id;
+  // }
+  // currentMsg.client_custom_chat_message_id = message_obj.message_id || '';
+  return {
+    status: chat_status, // 本轮会话的状态:0 未开始 1 进行中 2 已结束 3 错误
+    message: chat_msg,
+    data: currentMsg
+  };
+}

+ 185 - 0
src/modules/conversation-dialog/use-sse-email.js

@@ -0,0 +1,185 @@
+import { ref } from 'vue';
+
+import fetchEventSource from '../../3rd-libs/microsoft-feat-event-source';
+import { kuky_Authorization } from '../../base/storage';
+
+import { isElectron } from '../../utils/env'
+
+
+
+/**
+ * 区分智能体类型
+ * 参数:mode
+ * 1.聊天助手 [agent-dialog,advanced-chat]
+ * 2.Agent [agent-chat]
+ * 3.工作流 [workflow]
+ * */
+
+/**
+ * 获取sse连接的地址
+ * @param {string} menu_id - 对话类型,可选值:1/2/3/4/5/6/7/8/9
+ * @param {string} agent_id - 对话使用的agent ID
+ * @param {string} chat_id - 对话记录的ID
+ * @param {string} mode - 对话类型
+ * [agent-dialog], ragflow 智能体
+ * [agent-chat,chat,'advanced-chat'],dify智能体
+ * [workflow] dify智能体
+ * [agent-basic]
+ * @returns
+ */
+function getSseUrl() {
+  let base_url = '';
+
+  // 获取环境配置并动态替换 URL
+  if (import.meta.env.MODE === 'production') {
+    base_url = window.API_BASE_URL;
+  } else {
+    base_url = '';
+  }
+
+  // base_url += `/iaserverapi/v1/report/app/conversation/runs`; //流式接口路径
+  base_url += `/iaserverapi/v1/report/app/conversation/runs/email`; //流式接口路径
+  console.log(base_url, 222)
+  return base_url;
+}
+
+/**
+ *获取token
+ */
+function getToken() {
+  return kuky_Authorization.get();
+}
+
+export default function useFetchEventSourceReport() {
+  const messages_list = ref([]); // 存储接收到的消息
+  const isLoading = ref(false); // 是否正在连接
+  const error = ref(null); // 错误信息
+  const isConnected = ref(false); // 是否已连接
+  let abortController = null;
+  /**
+   * queryParams: sse接口需要传递的参数
+   * */
+  const startEmailStream = (queryParams, onMessage = defaultOnMessage) => {
+    isLoading.value = true;
+    isConnected.value = false;
+    error.value = null;
+    // const { agent_id, session_id, mode, query, files, inputs, knowledgeId, chatMode, isDeep, parentId } = queryParams;
+    // const { app_id, conversation_id, file_ids, query, response_mode } = queryParams;
+
+    const {
+      start_date,
+      end_date,
+      app_id,
+      conversation_id,
+      file_ids,
+      query,
+      stream,
+      user_id,
+      template_id,
+      word_file_id,
+      email_type,
+    } = queryParams;
+
+    console.log('sse_url');
+    let params;
+    // params = {
+    //   session_id,
+    //   stream,
+    //   delta,
+    //   trace,
+    //   message: {
+    //     text,
+    //     metadata,
+    //     attachments: []
+    //   },
+    //   response_mode
+    // };
+    params = {
+      app_id,
+      conversation_id,
+      // file_ids,
+      query,
+      // stream,
+      user_id,
+      template_id,
+      email_type,
+      // start_date,
+      // end_date,
+
+    };
+    if (word_file_id) {
+      params.word_file_id = word_file_id
+    }
+
+    const sse_url = getSseUrl();
+    if (!sse_url) {
+      return;
+    }
+    abortController = new AbortController();
+    fetchEventSource(sse_url, {
+      method: 'POST',
+      headers: {
+        // Authorization: `Bearer ${getToken()}`,
+        Authorization: `Bearer ${window.API_KEY_BD}`,
+        'X-Authorization': `Bearer ${window.API_KEY_BD}`,
+        'Content-Type': 'application/json',
+        Accept: 'text/event-stream',
+        Connection: 'keep-alive',
+        "Cache-Control": "no-cache"
+        // csrftoken:'2025-04-27T14:39:07Z|d56f8faa27ba8d58da766eb3628d477c'
+      },
+      // openWhenHidden: true,
+      body: JSON.stringify(params),
+      signal: abortController.signal, // 支持取消请求messages
+      openWhenHidden: true, //页面退至后台后保持连接
+      onmessage(ev) {
+
+        onMessage(ev);
+
+      },
+      onclose() {
+        console.log('Connection closed by server');
+        isConnected.value = false;
+        isLoading.value = false;
+      },
+      onerror(err) {
+        console.error('Error received:', err);
+        error.value = err;
+        isLoading.value = false;
+        isConnected.value = false;
+        throw Error('Error received:', err);
+      }
+    });
+  };
+  /**
+   * 终止当前的流连接
+   */
+  const stopEmailStream = () => {
+    abortController?.abort();
+
+    isLoading.value = false;
+    isConnected.value = false;
+    console.log('Stream aborted');
+  };
+  /**
+   * 默认的消息处理函数
+   */
+  const defaultOnMessage = (ev) => {
+    try {
+      const message = JSON.parse(ev.data); // 假设返回的是JSON数据
+      console.log('Received message:', message);
+      // messages_list.value.push(message); // 将消息存入 messages_list
+    } catch (e) {
+      console.error('Failed to parse message:', e);
+    }
+  };
+
+  return {
+    startEmailStream,
+    stopEmailStream,
+    messages_list,
+    isLoading,
+    error,
+    isConnected
+  };
+}

+ 12 - 14
src/modules/smart-chat/DialogThinkContent.vue

@@ -31,13 +31,12 @@ const show_think_title = computed(() => {
 const handleThinkClick = () => {
   show_think_page.value = !show_think_page.value;
 };
-
 </script>
 <style scoped>
 .think {
   /* background: #D6F8F8; */
   /* background: rgba(10, 129, 126, 0.06); */
-  color: #2C2C2C;
+  color: #2c2c2c;
   margin-bottom: 10px;
 }
 
@@ -52,17 +51,16 @@ const handleThinkClick = () => {
   font-weight: 600;
   font-family: 思源黑体;
   font-size: 14px;
-  color: #0A817E;
+  color: #0a817e;
   padding: 2px 20px;
-
-  .down {
-    margin-left: 10px;
-    color: #0A817E;
-  }
+}
+.think-select .down {
+  margin-left: 10px;
+  color: #0a817e;
 }
 
 .think_select span {
-  color: #0A817E;
+  color: #0a817e;
   font-size: 14px;
   font-weight: 600;
   font-family: 思源黑体;
@@ -72,12 +70,11 @@ const handleThinkClick = () => {
 .think-select:hover {
   font-weight: 600;
   font-family: 思源黑体;
-  color: #0A817E;
+  color: #0a817e;
   background: rgba(10, 129, 126, 0.06);
   opacity: 0.8;
 }
 
-
 .think_content {
   /* background: #D6F8F8; */
   background: rgba(10, 129, 126, 0.06);
@@ -95,7 +92,7 @@ const handleThinkClick = () => {
   gap: 10px;
   cursor: pointer;
   border-radius: 12px;
-  color: #0A817E;
+  color: #0a817e;
   font-weight: 600;
   font-family: 思源黑体;
 }
@@ -104,7 +101,8 @@ const handleThinkClick = () => {
   opacity: 0.8;
 }
 
-.content-title-left {}
+.content-title-left {
+}
 
 .content-text {
   padding: 0px 20px 10px 20px;
@@ -129,7 +127,7 @@ const handleThinkClick = () => {
   text-align: left;
 }
 
-.content-text .katex-display>.katex {
+.content-text .katex-display > .katex {
   white-space: pre-wrap !important;
 }
 </style>

+ 318 - 0
src/modules/task-config/AddTaskModal.vue

@@ -0,0 +1,318 @@
+<template>
+  <a-modal
+    v-model:visible="visible"
+    width="40vw"
+    title-align="start"
+    :esc-to-close="false"
+    @cancel="handleCancel"
+    class="my_custom_task_config"
+    :footer="false"
+  >
+    <template #title>
+      <div class="title">
+        <div class="title-text">添加任务</div>
+      </div>
+    </template>
+    <div class="container">
+      <a-form layout="horizontal" class="add-task-form" :model="formData" ref="formRef" :rules="formRules">
+        <a-form-item label="主题" name="taskName" field="taskName" validate-trigger="blur">
+          <a-input v-model="formData.taskName" placeholder="请输入主题" />
+        </a-form-item>
+
+        <a-form-item
+          label="收件邮箱"
+          tooltip="多个邮箱间用逗号隔开"
+          name="recipient"
+          field="recipient"
+          validate-trigger="blur"
+        >
+          <a-input v-model="formData.recipient" placeholder="邮箱1,邮箱2" style="width: 100%" />
+        </a-form-item>
+
+        <a-form-item label="抄送邮箱" tooltip="多个邮箱间用逗号隔开" name="cc" field="cc" validate-trigger="blur">
+          <a-input v-model="formData.cc" placeholder="邮箱1,邮箱2" style="width: 100%" />
+        </a-form-item>
+
+        <a-form-item label="发送周期">
+          <!--
+            关键修改 1: 添加 @change 事件
+            关键修改 2 (可选): 如果下面 JS 逻辑仍不生效,可以加上 :key="formData.sendCycle.join('-')" 强制重渲染组件
+          -->
+          <a-cascader
+            v-model="formData.sendCycle"
+            :options="sendCycleOptions"
+            :path-mode="true"
+            style="width: 100%"
+            placeholder="请选择发送周期"
+            @change="handleCycleChange"
+            allow-clear
+          />
+        </a-form-item>
+
+        <a-form-item label="数据类型">
+          <a-select v-model="formData.func_name" style="width: 100%" placeholder="请选择数据类型">
+            <a-option v-for="item in func_nameOptions" :key="item.value" :value="item.label">{{ item.label }}</a-option>
+          </a-select>
+        </a-form-item>
+
+        <a-form-item label="附件">
+          <div class="attachments">
+            <span class="attachment-item">频停周报.word</span>
+            <span class="attachment-item">频停线路.xlsx</span>
+            <span class="attachment-item">频停台区.xlsx</span>
+          </div>
+        </a-form-item>
+
+        <!-- <a-form-item label="邮件正文">
+          <a-textarea v-model="formData.content" placeholder="请输入邮件正文" :rows="5" style="width: 100%" />
+        </a-form-item> -->
+
+        <div class="form-actions">
+          <a-button @click="handleCancel" style="margin-right: 16px">取消</a-button>
+          <a-button type="primary" :loading="is_loading" @click="handleSubmit">确定</a-button>
+        </div>
+      </a-form>
+    </div>
+  </a-modal>
+</template>
+
+<script setup lang="js">
+import { ref, computed, nextTick } from 'vue'
+import {cycleOptions} from './common_data'
+import {addEmailTask} from '../conversation-dialog/api-dialog'
+import { Message } from '@arco-design/web-vue'
+
+const props = defineProps({
+  show_modal: {
+    type: Boolean,
+    default: false
+  }
+})
+
+const emit = defineEmits(['close-add-task-modal','success-add-task-modal'])
+
+const visible = computed(() => {
+  return props.show_modal;
+})
+
+// 表单引用
+const formRef = ref(null);
+
+// 加载状态
+const is_loading = ref(false)
+
+// 发送周期选项
+const sendCycleOptions = ref(cycleOptions);
+
+// 报告类型选项
+const func_nameOptions = ref([
+  { value: 'week_report_1', label: '频停周报' },
+  { value: 'monthly_report_1', label: '配网运营分析月报' },
+  { value: 'week_report_2', label: '配网运营分析周报' },
+  { value: 'monthly_report_2', label: '配网数字化月报' },
+]);
+
+// 表单校验规则
+const formRules = {
+  taskName:[
+    {
+      required:true,
+      message:"主题是必填项",
+      trigger: 'blur'
+    }
+  ],
+  recipient: [
+    {
+      required:true,
+      message:"收件邮箱是必填项",
+      trigger: 'blur'
+    },
+    {
+      validator: ( value, callback) => {
+        if (!value) {
+          callback();
+          return;
+        }
+        const emails = value.split(',').map(email => email.trim()).filter(email => email);
+        const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
+        const allValid = emails.every(email => emailRegex.test(email));
+        if (allValid) {
+          callback();
+        } else {
+          callback(new Error('请输入有效的邮箱地址,多个邮箱请用逗号分隔'));
+        }
+      },
+      trigger: 'blur'
+    }
+  ],
+  cc: [
+    {
+      required:true,
+      message:"抄送邮箱是必填项",
+      trigger: 'blur'
+    },
+    {
+      validator: ( value, callback) => {
+        if (!value) {
+          callback();
+          return;
+        }
+        const emails = value.split(',').map(email => email.trim()).filter(email => email);
+        const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
+        const allValid = emails.every(email => emailRegex.test(email));
+        if (allValid) {
+          callback();
+        } else {
+          callback(new Error('请输入有效的邮箱地址,多个邮箱请用逗号分隔'));
+        }
+      },
+      trigger: 'blur'
+    }
+  ]
+};
+
+// 表单数据
+const formData = ref({
+  taskName: '',
+  recipient: '',
+  cc: '',
+  sendCycle: ['daily', '3'], // 默认每日 3点
+  func_name: '频停周报',
+  // content: '7月28日至今,全省频繁停电共新增预警、催办、督办的线路分别为7条、1条、2条,配变分别为0台、0台、0台。\n截至当前,今年全省频繁停电预警0条、催办17条、督办504条,分别占0.0%、3.26%、96.74%。预警85台、催办1102台、督办24195台,分别占0.33%、4.34%、95.32%.'
+})
+
+/**
+ * 核心修复逻辑
+ * 当层级深度变化时(如从2层变3层),Cascader 有时不会正确更新显示文本。
+ * 通过先置空再赋值,强制触发组件的内部更新机制。
+ */
+const handleCycleChange = async (value) => {
+  console.log('原始选中值:', value);
+
+  if (!value || value.length === 0) {
+    formData.value.sendCycle = [];
+    return;
+  }
+
+  // 1. 保存新值
+  const newValue = [...value];
+
+  // 2. 关键步骤:先置空,打破当前的引用和内部状态
+  formData.value.sendCycle = [];
+
+  // 3. 等待 DOM 和组件内部状态稍微更新(微任务队列)
+  await nextTick();
+
+  // 4. 再赋回新值,此时组件会重新计算显示路径
+  formData.value.sendCycle = newValue;
+
+  console.log('最终更新后的值:', formData.value.sendCycle);
+};
+
+const handleCancel = () => {
+  emit('close-add-task-modal')
+}
+
+const handleSubmit = async () => {
+  try{
+    is_loading.value = true;
+    const res = await formRef.value.validate();
+    console.log(res)
+    if(!res){
+      const data = {
+        task_name: formData.value.taskName,
+        recipient: formData.value.recipient,
+        cc: formData.value.cc,
+        sendCycle: formData.value.sendCycle,
+        func_name: formData.value.func_name,
+        is_active: 0,
+      }
+      if(formData.value.recipient){
+        data.recipient = formData.value.recipient.split(',')
+      }
+      if(formData.value.cc){
+        data.cc = formData.value.cc.split(',')
+      }
+      if(formData.value.sendCycle.length > 0){
+        data.cycle_type = formData.value.sendCycle[0];
+        if(formData.value.sendCycle.length >2){
+          data.cycle_value = formData.value.sendCycle[1];
+          data.exec_hour = formData.value.sendCycle[2];
+        }else {
+          data.exec_hour = formData.value.sendCycle[1];
+        }
+      }
+      console.log(data)
+      await addEmailTaskFun(data);
+    }
+  }catch(errors){
+    console.log(errors)
+    return;
+  }finally {
+    is_loading.value = false;
+  }
+}
+
+const addEmailTaskFun = async (data) => {
+  try{
+    const res = await addEmailTask(data);
+    if(res.code /1=== 200){
+      Message.success('新增任务成功');
+      emit('success-add-task-modal');
+    }else {
+      Message.error(res.msg || '新增任务失败');
+    }
+  }catch(e){
+    Message.error('服务异常');
+  }finally {
+    handleCancel();
+  }
+}
+
+</script>
+
+<style scoped lang="css">
+.container {
+  box-sizing: border-box;
+  padding: 0px 10px 12px 10px;
+  font-family: '思源黑体';
+}
+
+.add-task-form {
+  width: 100%;
+}
+
+.add-task-form :deep(.arco-form-item-label) {
+  text-align: left;
+}
+
+.add-task-form :deep(.arco-form-item-control) {
+  flex: 1;
+  min-width: 200px;
+}
+
+.attachments {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 12px;
+}
+
+.attachment-item {
+  padding: 4px 12px;
+  border-radius: 4px;
+  font-size: 14px;
+  color: #666;
+}
+
+.form-actions {
+  margin-top: 24px;
+  display: flex;
+  justify-content: flex-end;
+}
+</style>
+
+<style>
+.my_custom_task_config {
+  /* 视情况调整高度 */
+}
+</style>

+ 363 - 0
src/modules/task-config/EditTaskModal.vue

@@ -0,0 +1,363 @@
+<template>
+  <a-modal
+    v-model:visible="visible"
+    width="40vw"
+    title-align="start"
+    :esc-to-close="false"
+    @cancel="handleCancel"
+    class="my_custom_task_config"
+    :footer="false"
+  >
+    <template #title>
+      <div class="title">
+        <div class="title-text">编辑任务</div>
+      </div>
+    </template>
+    <div class="container">
+      <a-form layout="horizontal" class="add-task-form" :model="formData" ref="formRef" :rules="formRules">
+        <a-form-item label="主题" name="taskName" field="taskName" validate-trigger="blur">
+          <a-input v-model="formData.taskName" placeholder="请输入主题" />
+        </a-form-item>
+
+        <a-form-item
+          label="收件邮箱"
+          tooltip="多个邮箱间用逗号隔开"
+          name="recipient"
+          field="recipient"
+          validate-trigger="blur"
+        >
+          <a-input v-model="formData.recipient" placeholder="邮箱1,邮箱2" style="width: 100%" />
+        </a-form-item>
+
+        <a-form-item label="抄送邮箱" tooltip="多个邮箱间用逗号隔开" name="cc" field="cc" validate-trigger="blur">
+          <a-input v-model="formData.cc" placeholder="邮箱1,邮箱2" style="width: 100%" />
+        </a-form-item>
+
+        <a-form-item label="发送周期">
+          <!--
+            关键修改 1: 添加 @change 事件
+            关键修改 2 (可选): 如果下面 JS 逻辑仍不生效,可以加上 :key="formData.sendCycle.join('-')" 强制重渲染组件
+          -->
+          <a-cascader
+            v-model="formData.sendCycle"
+            :options="sendCycleOptions"
+            :path-mode="true"
+            style="width: 100%"
+            placeholder="请选择发送周期"
+            @change="handleCycleChange"
+            allow-clear
+          />
+        </a-form-item>
+
+        <a-form-item label="数据类型">
+          <a-select v-model="formData.func_name" style="width: 100%" placeholder="请选择数据类型">
+            <a-option v-for="item in func_nameOptions" :key="item.value" :value="item.label">{{ item.label }}</a-option>
+          </a-select>
+        </a-form-item>
+
+        <a-form-item label="附件">
+          <div class="attachments">
+            <span class="attachment-item">频停周报.word</span>
+            <span class="attachment-item">频停线路.xlsx</span>
+            <span class="attachment-item">频停台区.xlsx</span>
+          </div>
+        </a-form-item>
+
+        <!-- <a-form-item label="邮件正文">
+          <a-textarea v-model="formData.content" placeholder="请输入邮件正文" :rows="5" style="width: 100%" />
+        </a-form-item> -->
+
+        <div class="form-actions">
+          <a-button @click="handleCancel" style="margin-right: 16px">取消</a-button>
+          <a-button type="primary" :loading="is_loading" @click="handleSubmit">确定</a-button>
+        </div>
+      </a-form>
+    </div>
+  </a-modal>
+</template>
+
+<script setup lang="js">
+import { ref, computed, nextTick, watch } from 'vue'
+import {cycleOptions} from './common_data'
+import {editEmailTask} from '../conversation-dialog/api-dialog'
+import { Message } from '@arco-design/web-vue'
+
+const props = defineProps({
+  show_modal: {
+    type: Boolean,
+    default: false
+  },
+  item: {
+    type: Object,
+    default: () => ({})
+  }
+})
+
+const emit = defineEmits(['close-edit-task-modal','success-edit-task-modal'])
+
+const visible = computed(() => {
+  return props.show_modal;
+})
+
+// 表单引用
+const formRef = ref(null);
+
+const is_loading = ref(false)
+// 发送周期选项
+const sendCycleOptions = ref(cycleOptions);
+
+// 报告类型选项
+const func_nameOptions = ref([
+  { value: 'week_report_1', label: '频停周报' },
+  { value: 'monthly_report_1', label: '配网运营分析月报' },
+  { value: 'week_report_2', label: '配网运营分析周报' },
+  { value: 'monthly_report_2', label: '配网数字化月报' },
+]);
+
+// 表单校验规则
+const formRules = {
+  taskName:[
+    {
+      required:true,
+      message:"主题是必填项",
+      trigger: 'blur'
+    }
+  ],
+  recipient: [
+    {
+      required:true,
+      message:"收件邮箱是必填项",
+      trigger: 'blur'
+    },
+    {
+      validator: ( value, callback) => {
+        if (!value) {
+          callback();
+          return;
+        }
+        const emails = value.split(',').map(email => email.trim()).filter(email => email);
+        const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
+        const allValid = emails.every(email => emailRegex.test(email));
+        if (allValid) {
+          callback();
+        } else {
+          callback(new Error('请输入有效的邮箱地址,多个邮箱请用逗号分隔'));
+        }
+      },
+      trigger: 'blur'
+    }
+  ],
+  cc: [
+    {
+      required:true,
+      message:"抄送邮箱是必填项",
+      trigger: 'blur'
+    },
+    {
+      validator: ( value, callback) => {
+        if (!value) {
+          callback();
+          return;
+        }
+        const emails = value.split(',').map(email => email.trim()).filter(email => email);
+        const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
+        const allValid = emails.every(email => emailRegex.test(email));
+        if (allValid) {
+          callback();
+        } else {
+          callback(new Error('请输入有效的邮箱地址,多个邮箱请用逗号分隔'));
+        }
+      },
+      trigger: 'blur'
+    }
+  ]
+};
+
+// 表单数据
+const formData = ref({
+  taskName: '',
+  recipient: '',
+  cc: '',
+  sendCycle: ['daily', '3'], // 默认每日 3点
+  func_name: '频停周报',
+  // content: '7月28日至今,全省频繁停电共新增预警、催办、督办的线路分别为7条、1条、2条,配变分别为0台、0台、0台。\n截至当前,今年全省频繁停电预警0条、催办17条、督办504条,分别占0.0%、3.26%、96.74%。预警85台、催办1102台、督办24195台,分别占0.33%、4.34%、95.32%.'
+})
+
+// 监听item变化,回填数据到表单
+watch(() => props.item, (newItem) => {
+  if (newItem && newItem.id) {
+    // 处理收件邮箱 - 将数组转换为逗号分隔的字符串
+    let recipientStr = '';
+    if (Array.isArray(newItem.recipient)) {
+      recipientStr = newItem.recipient.join(',');
+    } else if (newItem.recipient) {
+      recipientStr = newItem.recipient;
+    }
+
+    // 处理抄送邮箱 - 将数组转换为逗号分隔的字符串
+    let ccStr = '';
+    if (Array.isArray(newItem.cc)) {
+      ccStr = newItem.cc.join(',');
+    } else if (newItem.cc) {
+      ccStr = newItem.cc;
+    }
+
+    // 处理发送周期
+    let sendCycleArr = [];
+    if (newItem.cycle_type) {
+      sendCycleArr.push(newItem.cycle_type);
+      if (newItem.cycle_value) {
+        sendCycleArr.push(String(newItem.cycle_value));
+      }
+      if (newItem.exec_hour !== undefined && newItem.exec_hour !== null) {
+        sendCycleArr.push(String(newItem.exec_hour));
+      }
+    }
+
+    // 填充表单数据
+    formData.value = {
+      taskName: newItem.task_name || '',
+      recipient: recipientStr,
+      cc: ccStr,
+      sendCycle: sendCycleArr,
+      func_name: newItem.func_name || '频停周报'
+    };
+  }
+}, { immediate: true });
+
+/**
+ * 核心修复逻辑
+ * 当层级深度变化时(如从2层变3层),Cascader 有时不会正确更新显示文本。
+ * 通过先置空再赋值,强制触发组件的内部更新机制。
+ */
+const handleCycleChange = async (value) => {
+  console.log('原始选中值:', value);
+
+  if (!value || value.length === 0) {
+    formData.value.sendCycle = [];
+    return;
+  }
+
+  // 1. 保存新值
+  const newValue = [...value];
+
+  // 2. 关键步骤:先置空,打破当前的引用和内部状态
+  formData.value.sendCycle = [];
+
+  // 3. 等待 DOM 和组件内部状态稍微更新(微任务队列)
+  await nextTick();
+
+  // 4. 再赋回新值,此时组件会重新计算显示路径
+  formData.value.sendCycle = newValue;
+
+  console.log('最终更新后的值:', formData.value.sendCycle);
+};
+
+const handleCancel = () => {
+  emit('close-edit-task-modal')
+}
+
+const handleSubmit = async () => {
+  try{
+    is_loading.value = true;
+    const res = await formRef.value.validate();
+    console.log(res)
+    if(!res){
+      const data = {
+        task_name: formData.value.taskName,
+        recipient: formData.value.recipient,
+        cc: formData.value.cc,
+        sendCycle: formData.value.sendCycle,
+        func_name: formData.value.func_name,
+        is_active: 0,
+        task_id: props.item.id,
+      }
+      if(formData.value.recipient){
+        data.recipient = formData.value.recipient.split(',')
+      }
+      if(formData.value.cc){
+        data.cc = formData.value.cc.split(',')
+      }
+      if(formData.value.sendCycle.length > 0){
+        data.cycle_type = formData.value.sendCycle[0];
+        if(formData.value.sendCycle.length >2){
+          data.cycle_value = formData.value.sendCycle[1];
+          data.exec_hour = formData.value.sendCycle[2];
+        }else {
+          data.exec_hour = formData.value.sendCycle[1];
+        }
+      }
+      console.log(data)
+      await editEmailTaskFun(data);
+    }
+  }catch(errors){
+    console.log(errors)
+    return;
+  }finally {
+    is_loading.value = false;
+  }
+}
+
+const editEmailTaskFun = async (data) => {
+  try{
+    const res = await editEmailTask(data);
+    if(res.code /1=== 200){
+      Message.success('编辑任务成功');
+      emit('success-edit-task-modal');
+    }else {
+      Message.error(res.msg || '编辑任务失败');
+    }
+  }catch(e){
+    Message.error('服务异常');
+  }finally {
+    handleCancel();
+  }
+}
+
+</script>
+
+<style scoped lang="css">
+.container {
+  box-sizing: border-box;
+  padding: 0px 10px 12px 10px;
+  font-family: '思源黑体';
+}
+
+.add-task-form {
+  width: 100%;
+}
+
+.add-task-form :deep(.arco-form-item-label) {
+  text-align: left;
+}
+
+.add-task-form :deep(.arco-form-item-control) {
+  flex: 1;
+  min-width: 200px;
+}
+
+.attachments {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 12px;
+}
+
+.attachment-item {
+  padding: 4px 12px;
+  border-radius: 4px;
+  font-size: 14px;
+  color: #666;
+}
+
+.form-actions {
+  margin-top: 24px;
+  display: flex;
+  justify-content: flex-end;
+}
+</style>
+
+<style>
+.my_custom_task_config {
+  /* 视情况调整高度 */
+}
+</style>

+ 197 - 0
src/modules/task-config/TaskConfigModal.vue

@@ -0,0 +1,197 @@
+<template>
+  <a-modal
+    v-model:visible="visible"
+    width="60vw"
+    title-align="start"
+    :esc-to-close="false"
+    @cancel="handleCancel"
+    class="my_custom_task_config"
+    :footer="false"
+  >
+    <template #title>
+      <div class="title">
+        <div class="title-text">自动化邮件任务管理</div>
+      </div>
+    </template>
+    <div class="container">
+      <div class="tools">
+        <a-input-search
+          v-model="search_name"
+          @input="toSearch"
+          @enter="toSearch"
+          :style="{ width: '320px' }"
+          placeholder="请输入搜索内容"
+        />
+        <a-button class="add-btn" @click="handleAddTaskClick"> <icon-edit />添加任务</a-button>
+      </div>
+      <div class="body-table">
+        <TaskTable
+          :is_loading="is_loading_table"
+          :table_data="table_data"
+          :params="params"
+          @toRefresh="getEmailListFunc"
+          @page-change="changePageChange"
+          @page-size-change="changePageSizeChange"
+          @edit-task-click="handleEditTaskClick"
+        ></TaskTable>
+      </div>
+    </div>
+    <AddTaskModal
+      v-if="show_add_task_modal"
+      :show_modal="show_add_task_modal"
+      @close-add-task-modal="handleAddTaskCancel"
+      @success-add-task-modal="handleAddTaskSuccess"
+    ></AddTaskModal>
+    <EditTaskModal
+      v-if="show_edit_task_modal"
+      :item="edit_task_item"
+      :show_modal="show_edit_task_modal"
+      @close-edit-task-modal="handleEditTaskCancel"
+      @success-edit-task-modal="handleEditTaskSuccess"
+    ></EditTaskModal>
+  </a-modal>
+</template>
+
+<script setup lang="js">
+
+import { ref,computed,onMounted } from 'vue'
+
+import TaskTable from './TaskTable.vue'
+import AddTaskModal from './AddTaskModal.vue'
+import EditTaskModal from './EditTaskModal.vue'
+import {getEmailList} from '../conversation-dialog/api-dialog'
+import {funcDebounce} from '../../utils/utils'
+
+
+const props = defineProps({
+  show_modal: {
+    type: Boolean,
+    default: false
+  }
+})
+
+const params = ref({
+  page: 1,
+  page_size: 10,
+  total: 0
+})
+const table_data = ref([])
+const search_name = ref('')
+
+const is_loading_table =  ref(false)
+
+const emit = defineEmits(['close-task-config-modal'])
+
+const show_add_task_modal = ref(false);
+const show_edit_task_modal = ref(false);
+const edit_task_item = ref({});
+
+
+
+const visible = computed(()=>{
+  return props.show_modal;
+})
+
+const handleAddTaskClick = () => {
+  show_add_task_modal.value = true;
+}
+const handleEditTaskClick = (item) => {
+  show_edit_task_modal.value = true;
+  edit_task_item.value = item;
+  console.log(item)
+
+}
+const handleCancel = () => {
+  emit('close-task-config-modal')
+}
+const handleAddTaskCancel = () => {
+  show_add_task_modal.value = false;
+}
+const handleAddTaskSuccess = () => {
+  show_add_task_modal.value = false;
+  getEmailListFunc()
+}
+
+const handleEditTaskCancel = () => {
+  show_edit_task_modal.value = false;
+}
+const handleEditTaskSuccess = () => {
+  show_edit_task_modal.value = false;
+  getEmailListFunc()
+}
+
+
+const getEmailListFunc = async () => {
+  try {
+    is_loading_table.value = true;
+    const res = await getEmailList({
+      page: params.value.page,
+      page_size: params.value.page_size,
+      select_name: search_name.value
+    })
+    if(res.code === 200) {
+      table_data.value = res.data?.list || []
+
+      params.value.total = res.data?.total || 0;
+    }
+  } catch (error) {
+    console.log(error)
+  }finally {
+    is_loading_table.value = false;
+  }
+}
+
+const toSearch = funcDebounce(async () => {
+  getEmailListFunc()
+}, 100,false)
+
+const changePageChange = (current) => {
+  params.value.page = current;
+  getEmailListFunc()
+}
+const changePageSizeChange = (page_size) => {
+  params.value.page_size = page_size;
+  getEmailListFunc()
+}
+
+
+
+onMounted( () => {
+  getEmailListFunc()
+})
+
+
+
+
+</script>
+<style scoped lang="css">
+.container {
+  height: 690px;
+  box-sizing: border-box;
+  padding: 0px 10px 12px 10px;
+  font-family: '思源黑体';
+}
+.tools {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+.add-btn {
+  margin-left: 10px;
+  background-color: var(--primary-default);
+  color: #fff;
+  display: flex;
+  align-items: center;
+  gap: 5px;
+}
+.add-btn:hover {
+  background-color: var(--primary-default);
+  color: #fff;
+  opacity: 0.8;
+}
+</style>
+<style>
+.my_custom_task_config {
+  height: 100vh;
+}
+</style>

+ 339 - 0
src/modules/task-config/TaskTable.vue

@@ -0,0 +1,339 @@
+<template>
+  <div class="task-table-container">
+    <div class="table-content">
+      <a-table :data="table_data_list" :pagination="false" :bordered="false" :loading="is_loading" :scroll="{ y: 550 }">
+        <template #columns>
+          <a-table-column
+            v-for="item in columns"
+            :key="item.dataIndex"
+            :title="item.title"
+            :data-index="item.dataIndex"
+            :header-cell-style="{ backgroundColor: 'var(--table-th-bg)' }"
+            align="center"
+            :width="item.width"
+          >
+            <template #cell="{ record }">
+              <div v-if="item.dataIndex === 'is_active'">
+                <a-switch
+                  v-model="record[item.dataIndex]"
+                  :checked-value="1"
+                  :unchecked-value="0"
+                  @change="handleStatusChange(record)"
+                  checked-color="var(--primary-default)"
+                  checked-text="开启"
+                  unchecked-text="关闭"
+                />
+              </div>
+              <div v-else-if="item.dataIndex === 'recipient'">
+                <span v-if="record[item.dataIndex]">{{ record[item.dataIndex].join(',') }}</span>
+              </div>
+              <div v-else-if="item.dataIndex === 'operate'" class="operate">
+                <img class="btn" src="../..//assets/chat-smart/send.svg" alt="发送" />
+                <img
+                  class="btn"
+                  src="../..//assets/chat-smart/edit.svg"
+                  alt="编辑"
+                  @click="handleEditTaskClick(record)"
+                />
+              </div>
+              <div v-else>{{ record[item.dataIndex] || '-' }}</div>
+            </template>
+          </a-table-column>
+        </template>
+      </a-table>
+    </div>
+    <div class="footer-pagination">
+      <a-pagination
+        :total="props.params.total"
+        :current="props.params.page"
+        :page-size="props.params.page_size"
+        @change="changePageChange"
+        @page-size-change="changePageSizeChange"
+        show-total
+        show-page-size
+      />
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, computed } from 'vue';
+
+import { editEmailTask } from '../conversation-dialog/api-dialog';
+import { Message } from '@arco-design/web-vue';
+
+const props = defineProps({
+  is_loading: {
+    type: Boolean,
+    default: false
+  },
+  table_data: {
+    type: Array,
+    default: () => []
+  },
+  params: {
+    type: Object,
+    default: () => ({
+      page: 1,
+      page_size: 10,
+      total: 0
+    })
+  }
+});
+
+const emit = defineEmits(['toRefresh', 'page-change', 'page-size-change', 'edit-task-click']);
+
+// 表格列配置
+const columns = [
+  {
+    title: '序号',
+    dataIndex: 'index'
+    // width: 80
+  },
+  {
+    title: '主题',
+    dataIndex: 'task_name'
+    // width: 150
+  },
+  {
+    title: '报告类型',
+    dataIndex: 'func_name'
+    // width: 120
+  },
+  {
+    title: '收件人',
+    dataIndex: 'recipient'
+    // width: 300
+  },
+  {
+    title: '发送周期',
+    dataIndex: 'cycle'
+    // width: 120
+  },
+  // {
+  //   title: '正文',
+  //   dataIndex: 'content'
+  //   // width: 200
+  // },
+  {
+    title: '关闭/开启',
+    dataIndex: 'is_active'
+    // width: 120
+  },
+  {
+    title: '操作',
+    dataIndex: 'operate'
+    // width: 150
+  }
+];
+
+const table_data_list = computed(() => {
+  const weekdays = ['一', '二', '三', '四', '五', '六', '日']; // 索引0对应周日,1对应周一,以此类推
+  return props.table_data.map((item, index) => {
+    let cycle = '';
+    if (item.cycle_type === 'daily') {
+      cycle = `每天 ${item.exec_hour}点`;
+    } else if (item.cycle_type === 'weekly') {
+      const weekDay = weekdays[parseInt(item.cycle_value) - 1]; // 将数字1-7转换为中文星期几
+      cycle = `每周 周${weekDay} ${item.exec_hour}点`;
+    } else if (item.cycle_type === 'monthly') {
+      cycle = `每月 ${item.cycle_value}日 ${item.exec_hour}点`;
+    }
+    return {
+      ...item,
+      index: index + 1,
+      cycle: cycle
+    };
+  });
+});
+// 示例数据
+// const table_data = ref([
+//   {
+//     index: 1,
+//     id: 1,
+//     title: '频停周报',
+//     type: '频停周报',
+//     recipient: 'user@example.com',
+//     send_cycle: '每周一',
+//     content: '请发送最近一个星期的频繁停电记录',
+//     status: 1
+//   },
+//   {
+//     index: 2,
+//     id: 1,
+//     title: '频停周报',
+//     type: '频停周报',
+//     recipient: 'user@example.com',
+//     send_cycle: '每周一',
+//     content: '请发送最近一个星期的频繁停电记录',
+//     status: 1
+//   },
+//   {
+//     index: 3,
+//     id: 1,
+//     title: '频停周报',
+//     type: '频停周报',
+//     recipient: 'user@example.com',
+//     send_cycle: '每周一',
+//     content: '请发送最近一个星期的频繁停电记录',
+//     status: 0
+//   },
+//   {
+//     index: 4,
+//     id: 1,
+//     title: '频停周报',
+//     type: '频停周报',
+//     recipient: 'user@example.com',
+//     send_cycle: '每周一',
+//     content: '请发送最近一个星期的频繁停电记录',
+//     status: 0
+//   },
+//   {
+//     index: 5,
+//     id: 1,
+//     title: '频停周报',
+//     type: '频停周报',
+//     recipient: 'user@example.com',
+//     send_cycle: '每周一',
+//     content: '请发送最近一个星期的频繁停电记录',
+//     status: 0
+//   },
+//   {
+//     index: 6,
+//     id: 1,
+//     title: '频停周报',
+//     type: '频停周报',
+//     recipient: 'user@example.com',
+//     send_cycle: '每周一',
+//     content: '请发送最近一个星期的频繁停电记录',
+//     status: 0
+//   },
+//   {
+//     index: 7,
+//     id: 1,
+//     title: '频停周报',
+//     type: '频停周报',
+//     recipient: 'user@example.com',
+//     send_cycle: '每周一',
+//     content: '请发送最近一个星期的频繁停电记录',
+//     status: 0
+//   },
+//   {
+//     index: 8,
+//     id: 1,
+//     title: '频停周报',
+//     type: '频停周报',
+//     recipient: 'user@example.com',
+//     send_cycle: '每周一',
+//     content: '请发送最近一个星期的频繁停电记录',
+//     status: 0
+//   },
+//   {
+//     index: 9,
+//     id: 1,
+//     title: '频停周报',
+//     type: '频停周报',
+//     recipient: 'user@example.com',
+//     send_cycle: '每周一',
+//     content: '请发送最近一个星期的频繁停电记录',
+//     status: 0
+//   },
+//   {
+//     index: 10,
+//     id: 1,
+//     title: '频停周报',
+//     type: '频停周报',
+//     recipient: 'user@example.com',
+//     send_cycle: '每周一',
+//     content: '请发送最近一个星期的频繁停电记录',
+//     status: 0
+//   },
+//   {
+//     index: 10,
+//     id: 1,
+//     title: '频停周报',
+//     type: '频停周报',
+//     recipient: 'user@example.com',
+//     send_cycle: '每周一',
+//     content: '请发送最近一个星期的频繁停电记录',
+//     status: 0
+//   }
+// ]);
+
+// 分页配置
+// const page = ref({
+//   total: 20,
+//   current: 1,
+//   page_size: 10
+// });
+
+const handleStatusChange = async (newValue) => {
+  console.log(newValue);
+  const data = {
+    ...newValue,
+    task_id: newValue.id
+  };
+  console.log(data);
+  await editEmailTaskFunc(data);
+};
+
+const handleEditTaskClick = (record) => {
+  emit('edit-task-click', record);
+};
+const editEmailTaskFunc = async (record) => {
+  console.log(record);
+  const res = await editEmailTask(record);
+  if (res.code / 1 === 200) {
+    Message.success('修改成功');
+    emit('toRefresh');
+  } else {
+    Message.error(res.msg || '修改失败');
+  }
+};
+const changePageChange = (current) => {
+  // page.value.current = current;
+  emit('page-change', current);
+};
+const changePageSizeChange = (page_size) => {
+  emit('page-size-change', page_size);
+};
+</script>
+
+<style scoped lang="css">
+.task-table-container {
+  width: 100%;
+  padding: 20px 0;
+  box-sizing: border-box;
+  background-color: #ffffff;
+  border-radius: 8px;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+.table-content {
+  width: 100%;
+  max-width: 1200px;
+}
+
+.task-table-container .arco-table {
+  width: 100%;
+}
+.task-table-container .footer-pagination {
+  margin-top: 20px;
+  display: flex;
+  justify-content: flex-end;
+  width: 100%;
+  max-width: 1200px; /* 与表格宽度保持一致 */
+}
+.operate {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  gap: 10px;
+}
+.operate .btn {
+  width: 20px;
+  cursor: pointer;
+}
+</style>

+ 32 - 0
src/modules/task-config/common_data.js

@@ -0,0 +1,32 @@
+export const cycleOptions = [
+  {
+    label: '每日',
+    value: 'daily',
+    children: Array.from({ length: 24 }, (_, j) => ({ label: `${j}点`, value: `${j}` }))
+  },
+  {
+    label: '每周',
+    value: 'weekly',
+    children: [
+      { label: '星期一', value: '1', children: Array.from({ length: 24 }, (_, j) => ({ label: `${j}点`, value: `${j}` })) },
+      { label: '星期二', value: '2', children: Array.from({ length: 24 }, (_, j) => ({ label: `${j}点`, value: `${j}` })) },
+      { label: '星期三', value: '3', children: Array.from({ length: 24 }, (_, j) => ({ label: `${j}点`, value: `${j}` })) },
+      { label: '星期四', value: '4', children: Array.from({ length: 24 }, (_, j) => ({ label: `${j}点`, value: `${j}` })) },
+      { label: '星期五', value: '5', children: Array.from({ length: 24 }, (_, j) => ({ label: `${j}点`, value: `${j}` })) },
+      { label: '星期六', value: '6', children: Array.from({ length: 24 }, (_, j) => ({ label: `${j}点`, value: `${j}` })) },
+      { label: '星期日', value: '7', children: Array.from({ length: 24 }, (_, j) => ({ label: `${j}点`, value: `${j}` })) },
+    ]
+  },
+  {
+    label: '每月',
+    value: 'monthly',
+    children: Array.from({ length: 31 }, (_, i) => ({
+      label: `${i + 1}日`,
+      value: `${i + 1}`,
+      children: Array.from({ length: 24 }, (_, j) => ({
+        label: `${j}点`,
+        value: `${j}`
+      }))
+    }))
+  }
+]

+ 55 - 6
src/views/About.vue

@@ -1,7 +1,7 @@
 <template>
-  <div class="demo">
+  <!-- <div class="demo">
     {{ md }}
-  </div>
+  </div> -->
 
   <!-- <div v-html="render_html"></div> -->
   <divider></divider>
@@ -18,6 +18,14 @@
     <a-option value="2">postgresql</a-option>
     <!-- <a-option v-for="item in options" :key="item.value" :value="item.value">{{ item.label }}</a-option> -->
   </a-select>
+  <a-cascader
+    :path-mode="true"
+    v-model="mine"
+    :options="options"
+    default-value="datunli"
+    expand-trigger="hover"
+    :style="{ width: '320px' }"
+  />
 </template>
 
 <script setup >
@@ -34,14 +42,55 @@ import Line_1 from '../assets/chat-report/折线图.png';
 import bar_line from '../assets/chat-report/组合图.png';
 import TableChart from '../modules/report-template/editor-chart/TableChart.vue';
 
+const mine = ref('');
 const options = [
   {
-    value: '1',
-    label: 'API接口'
+    value: 'beijing',
+    label: 'Beijing',
+    children: [
+      {
+        value: 'chaoyang',
+        label: 'ChaoYang',
+        children: [
+          {
+            value: 'datunli',
+            label: 'Datunli'
+          }
+        ]
+      },
+      {
+        value: 'haidian',
+        label: 'Haidian'
+      },
+      {
+        value: 'dongcheng',
+        label: 'Dongcheng'
+      },
+      {
+        value: 'xicheng',
+        label: 'Xicheng',
+        children: [
+          {
+            value: 'jinrongjie',
+            label: 'Jinrongjie'
+          },
+          {
+            value: 'tianqiao',
+            label: 'Tianqiao'
+          }
+        ]
+      }
+    ]
   },
   {
-    value: '2',
-    label: '数据库类型'
+    value: 'shanghai',
+    label: 'Shanghai',
+    children: [
+      {
+        value: 'huangpu',
+        label: 'Huangpu'
+      }
+    ]
   }
 ];
 const data_source_type = ref('1');

+ 5 - 0
src/views/Agent/AgentCentric.vue

@@ -49,6 +49,8 @@ const getAuthFun = async () => {
         bgsc: res.data.AI_BGSC || 0,
         znws: res.data.AI_ZNWS || 0,
         bgcl: res.data.AI_BGCL || 0,
+        znyj: res.data.AI_BGSC_EMAIL || 0, //邮件自动发送
+
         uninterrupted: res.data.AI_BGSC_DY || 0, //低压不停电作业数据管理
         ten_thousand_households: res.data.AI_BGSC_WH || 0, //万户数管理
         opinion_work_order_target_value: res.data.AI_BGSC_YJ || 0, //意见工单目标值管理
@@ -67,6 +69,7 @@ const getAuthFun = async () => {
         bgsc: res.data.AI_BGSC || 0,
         znws: res.data.AI_ZNWS || 0,
         bgcl: res.data.AI_BGCL || 0,
+        znyj: res.data.AI_BGSC_EMAIL || 0, //邮件自动发送
         uninterrupted: res.data.AI_BGSC_DY || 0, //低压不停电作业数据管理
         ten_thousand_households: res.data.AI_BGSC_WH || 0, //万户数管理
         opinion_work_order_target_value: res.data.AI_BGSC_YJ || 0, //意见工单目标值管理
@@ -87,6 +90,7 @@ const getAuthFun = async () => {
       bgsc: 1,
       znws: 1,
       bgcl: 1,
+      znyj: 1, //邮件自动发送
       uninterrupted: 1, //低压不停电作业数据管理
       ten_thousand_households: 1, //万户数管理
       opinion_work_order_target_value: 1, //意见工单目标值管理
@@ -116,6 +120,7 @@ onMounted(async () => {
       bgsc: 1,
       znws: 1,
       bgcl: 1,
+      znyj: 1, //邮件自动发送
       uninterrupted: 1, //低压不停电作业数据管理
       ten_thousand_households: 1, //万户数管理
       opinion_work_order_target_value: 1, //意见工单目标值管理

+ 2 - 1
src/views/conversation/ChatAsideMenu.vue

@@ -48,7 +48,8 @@ const app_type_map = {
   znwd: '1',
   bgsc: '2',
   znws: '3',
-  bgcl: '4'
+  bgcl: '4',
+  znyj: '5'
 };
 
 const history_list_loading = ref(false);

+ 170 - 1
src/views/conversation/ConversationChatContainer.vue

@@ -93,6 +93,7 @@ import TemplateManage from '../../modules/template-manage/ManageBody.vue';
 import useFetchEventSource from '../../modules/conversation-dialog/use-sse-chat';
 import useFetchEventSourceReport from '../../modules/conversation-dialog/use-sse-report';
 import useFetchEventSourceZnws from '../../modules/conversation-dialog/use-sse-znws';
+import useFetchEventSourceEmail from '../../modules/conversation-dialog/use-sse-email';
 import formatDialogQuestionByClient from '../../modules/conversation-dialog/format-dialog-question-by-client';
 import formatDialogAnswerForLoading from '../../modules/conversation-dialog/format-dialog-answer-for-loading';
 import formatDialogAnswerForException from '../../modules/conversation-dialog/format-dialog-answer-for-exception';
@@ -246,6 +247,11 @@ const WS_session = ref({
   conversation_id: '',
   record_id: ''
 });
+// 会话id阿里智能体(邮件)
+const EMAIL_session = ref({
+  conversation_id: '',
+  record_id: ''
+});
 
 // 问答开始时间-用于发送时显示当前时间
 const qa_start_time = ref('');
@@ -316,6 +322,10 @@ const main_conversation_container = computed(() => {
         return {
           height: 'calc(100% - 160px - 100px - 25px - 15px + 66px)'
         };
+      } else if (chat_type.value === 'znyj') {
+        return {
+          height: 'calc(100% - 160px - 94px - 100px - 25px - 15px - 60px)'
+        };
       } else {
         return {
           height: 'calc(100% - 160px - 100px - 25px - 15px)'
@@ -341,6 +351,10 @@ const StyleMainFooter = computed(() => {
       return {
         height: 'calc(160px - 66px)'
       };
+    } else if (chat_type.value === 'znyj') {
+      return {
+        height: 'calc(160px - 20px)'
+      };
     } else {
       return {
         height: '160px'
@@ -431,6 +445,7 @@ let common_config = {};
 const { startStream, stopStream } = useFetchEventSource();
 const { startReportStream, stopReportStream } = useFetchEventSourceReport();
 const { startZnwsStream, stopZnwsStream } = useFetchEventSourceZnws();
+const { startEmailStream, stopEmailStream } = useFetchEventSourceEmail();
 
 const toSend = (txt) => {
   const options = {
@@ -450,6 +465,8 @@ const handleSubmit = async (options) => {
   let ask_files = options.files || [];
   let event_id = options.event_id; //表格处理专用
   let template_id = options.template_id; //灵活模板选中的模板id
+  let email_type = options.email_type || '';
+
   qa_start_time.value = formatDateTime(new Date());
 
   // shouldAutoScroll.value = true
@@ -640,6 +657,45 @@ const handleSubmit = async (options) => {
     startZnwsStream(query_params, sseOnMessageZnws);
     return;
   }
+  if (chat_type.value == 'znyj') {
+    if (!EMAIL_session.value.conversation_id) {
+      try {
+        const chatResult = await getDialogSessionId_AL({
+          app_id: window.APP_ID_AL_EMAIL,
+          app_version: window.APP_VERSION_AL_EMAIL
+        });
+        if (chatResult.code / 1 === 200) {
+          EMAIL_session.value.conversation_id = chatResult.data.data.uniqueCode;
+          // session_id.value = chatResult.data.request_id;
+        } else {
+          // 失败
+          // 处理异常:直接在界面上显示
+          handleChatRequestionException({
+            message: `code: ${chatResult.errorCode} message: ${chatResult.errorMsg}`
+          });
+          return;
+        }
+      } catch (e) {
+        // 处理错误
+        handleChatRequestionException({
+          message: e.message
+        });
+        return;
+      }
+    }
+    agentUsedStore.updateAgentUsed('znyj');
+    const username = await getUserName();
+    // const power_id = await getUserId();
+    const query_params = {
+      conversation_id: EMAIL_session.value.conversation_id,
+      user_id: convertToLowerCase(username),
+      query: ask_text,
+      app_id: window.APP_ID_AL_WS,
+      email_type: email_type
+    };
+    startEmailStream(query_params, sseOnMessageEmail);
+  }
+
   /**
    * 表格处理情况特殊,需要自定义问题和展示文件列表
    * */
@@ -1163,6 +1219,102 @@ const sseOnMessageZnws = (event) => {
     return;
   }
 };
+// 邮件自动邮件处理
+const sseOnMessageEmail = (event) => {
+  const res_data = JSON.parse(event.data);
+  // let markdown_chart = null;
+  console.log(res_data);
+  // return;
+  if (res_data.metadata && res_data.metadata.markdown_chart) {
+    // console.log(res_data.metadata.markdown_chart);
+    markdown_chart = JSON.parse(JSON.stringify(res_data.metadata.markdown_chart));
+  }
+  if (res_data.end) {
+    stopResponse();
+  }
+  if (res_data.object && res_data.object === 'error') {
+    handleChatRequestionException({
+      message: `code: ${res_data.content.errorCode} message: ${res_data.content.errorMsg}`
+    });
+  }
+  // TODO: 将返回数据填充到页面
+
+  // 获取当前对话中最后一个对话数据
+  // 最后一个对话数据为当前正在进行的对话
+  const last_msg = chat_new_list.value[chat_new_list.value.length - 1];
+  /**
+   * 整理返回的数据
+   *
+   */
+  const currentChat = formatDialogAnswerFromSseReceiveMessageZnws(
+    res_data,
+    res_data?.content,
+    last_msg,
+    res_data.end,
+    markdown_chart
+  );
+  const chat_status = currentChat.status;
+
+  // 处理对话数据
+  const handleChatData = () => {
+    const chat_data = currentChat.data; // 对话相关的数据项
+    // TODO: 为演示使用,判断是报告生成,新增两个字段一个是否有图标,另一个是文档数据
+    // 原来的最后一项弹出
+    chat_new_list.value.pop();
+    // 添加新的最后一项
+    chat_new_list.value.push(chat_data);
+
+    // 滚动到底部
+    scroll2ListBottom();
+  };
+
+  if (chat_status === 0) {
+    // 未开始: 此状态不存在,无需处理
+  }
+
+  if (chat_status === 1) {
+    // 此轮对话仍然在进行中
+    handleChatData();
+    show_stop_sse.value = true;
+  }
+
+  if (chat_status === 2) {
+    // 此轮对话已结束
+    handleChatData();
+
+    // 对话已结束:文件上传、文本输入区域放开使用
+    input_disabled.value = false;
+    input_ing.value = false;
+    show_stop_sse.value = false;
+    emit('updateHistory');
+  }
+
+  if (chat_status === 3) {
+    // 此轮对话出错了
+    // TODO: 错误处理
+
+    // 此轮对话已结束
+    handleChatData();
+    // 对话已结束:文件上传、文本输入区域放开使用
+    input_disabled.value = false;
+    input_ing.value = false;
+    show_stop_sse.value = false;
+    emit('updateHistory');
+  }
+
+  // 处理错误显示
+  if (
+    res_data.result === 'error' ||
+    res_data.result?.content === 'error' ||
+    res_data.result?.content?.outputs === 'error'
+  ) {
+    //sse错误,显示错误信息
+    handleChatRequestionException({
+      message: res_data.error
+    });
+    return;
+  }
+};
 
 // 表格处理内容
 const bgclOnMessage = (event_id, length) => {
@@ -1246,6 +1398,8 @@ const stopResponse = () => {
     stopReportStream();
   } else if (chat_type.value == 'znws') {
     stopZnwsStream();
+  } else if (chat_type.value == 'znyj') {
+    stopEmailStream();
   }
 
   show_stop_sse.value = false;
@@ -1328,6 +1482,10 @@ const toInitData = () => {
     conversation_id: '',
     record_id: ''
   };
+  EMAIL_session.value = {
+    conversation_id: '',
+    record_id: ''
+  };
   // 清空用户输入框
   inputClearFn1();
   // 清空用户提问数据
@@ -1377,6 +1535,10 @@ const getDialogHistoryDetailFun = async () => {
       detail_list = formatDialogHistoryMessages(res.data.rows, '3');
       WS_session.value.conversation_id = res.data.rows[0]?.conversation_id;
       history_item_params.value.total = res.data.total;
+    } else if (chat_type.value == 'znyj') {
+      detail_list = formatDialogHistoryMessages(res.data.rows, '5');
+      EMAIL_session.value.conversation_id = res.data.rows[0]?.conversation_id;
+      history_item_params.value.total = res.data.total;
     } else if (chat_type.value == 'bgcl') {
       detail_list = formatDialogHistoryMessages(res.data.rows, '4');
       history_item_params.value.total = res.data.total;
@@ -1505,7 +1667,8 @@ const agent_icon_map = {
   znwd: '01',
   bgsc: '02',
   znws: '03',
-  bgcl: '04'
+  bgcl: '04',
+  znyj: '05'
 };
 //获取头像
 const getAgentIconsFun = async () => {
@@ -1537,6 +1700,12 @@ const getAgentIconsFun = async () => {
       icon_type: '04',
       icon_id: ''
     });
+    await updateAgentIconsFun({
+      username,
+      user_icon: 'icon_5',
+      icon_type: '05',
+      icon_id: ''
+    });
     getAgentIconsFun();
   } else {
     all_agent_icons.value = res.data.user_icon;

Неке датотеке нису приказане због велике количине промена